From 28f12e0b24c07b6f6aba1f66cefabd99b5c84682 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 12 May 2026 16:08:18 +0200 Subject: [PATCH 01/35] Implement SCA Reachability: detect vulnerable library classes at runtime Adds a new SCA Reachability subsystem that reports which vulnerable library classes were actually loaded at runtime, reducing false positives from static dependency scanning. Gated on DD_APPSEC_SCA_ENABLED. Key components: - Gradle task downloads GHSA enrichments from sca-reachability-database and generates sca_cves.json bundled in the agent jar at build time - ClassFileTransformer (observation-only) detects when vulnerable classes are loaded, resolves JAR versions via pom.properties, and checks semver ranges using ComparableVersion (Maven semantics) - ScaReachabilityCollector bridges the transformer and telemetry without circular dependencies, following the WafMetricCollector pattern - ScaReachabilityPeriodicAction reports hits on each app-dependencies-loaded heartbeat by adding reachability metadata to existing dependency entries --- .github/CODEOWNERS | 2 + .../gradle/sca/GhsaEnrichmentParser.kt | 79 ++++++ .../gradle/sca/GhsaEnrichmentParserTest.kt | 117 +++++++++ .../sca/fixtures/GHSA-empty-symbols.json | 15 ++ .../sca/fixtures/GHSA-mixed-languages.json | 40 +++ .../sca/fixtures/GHSA-multi-package.json | 41 ++++ .../sca/fixtures/GHSA-single-package.json | 33 +++ .../java/datadog/trace/bootstrap/Agent.java | 17 ++ dd-java-agent/appsec/build.gradle | 84 +++++++ .../datadog/appsec/sca/ScaCveDatabase.java | 119 +++++++++ .../java/com/datadog/appsec/sca/ScaEntry.java | 89 +++++++ .../appsec/sca/ScaReachabilitySystem.java | 51 ++++ .../sca/ScaReachabilityTransformer.java | 232 ++++++++++++++++++ .../com/datadog/appsec/sca/ScaSymbol.java | 33 +++ .../appsec/sca/VersionRangeParser.java | 82 +++++++ .../appsec/sca/ScaCveDatabaseTest.java | 110 +++++++++ .../sca/ScaReachabilityTransformerTest.java | 189 ++++++++++++++ .../appsec/sca/VersionRangeParserTest.java | 181 ++++++++++++++ .../telemetry/ScaReachabilityCollector.java | 41 ++++ .../api/telemetry/ScaReachabilityHit.java | 40 +++ telemetry/build.gradle.kts | 1 + .../telemetry/TelemetryRequestBody.java | 10 + .../datadog/telemetry/TelemetrySystem.java | 4 + .../telemetry/dependency/Dependency.java | 21 ++ .../sca/ScaReachabilityPeriodicAction.java | 76 ++++++ ...etryRequestBodyDependencyMetadataTest.java | 94 +++++++ .../ScaReachabilityPeriodicActionTest.java | 138 +++++++++++ 27 files changed, 1939 insertions(+) create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/sca/GhsaEnrichmentParser.kt create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/sca/GhsaEnrichmentParserTest.kt create mode 100644 buildSrc/src/test/resources/sca/fixtures/GHSA-empty-symbols.json create mode 100644 buildSrc/src/test/resources/sca/fixtures/GHSA-mixed-languages.json create mode 100644 buildSrc/src/test/resources/sca/fixtures/GHSA-multi-package.json create mode 100644 buildSrc/src/test/resources/sca/fixtures/GHSA-single-package.json create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaSymbol.java create mode 100644 dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaCveDatabaseTest.java create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/VersionRangeParserTest.java create mode 100644 internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java create mode 100644 internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java create mode 100644 telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java create mode 100644 telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java create mode 100644 telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8efd03d9498..db0432dfc4f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -87,6 +87,8 @@ /dd-trace-api/src/main/java/datadog/trace/api/EventTracker.java @DataDog/asm-java /internal-api/src/main/java/datadog/trace/api/gateway/ @DataDog/asm-java /internal-api/src/main/java/datadog/trace/api/http/ @DataDog/asm-java +/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachability* @DataDog/asm-java +/telemetry/src/main/java/datadog/telemetry/sca/ @DataDog/asm-java **/appsec/ @DataDog/asm-java **/*CallSite*.java @DataDog/asm-java **/*CallSite*.groovy @DataDog/asm-java diff --git a/buildSrc/src/main/kotlin/datadog/gradle/sca/GhsaEnrichmentParser.kt b/buildSrc/src/main/kotlin/datadog/gradle/sca/GhsaEnrichmentParser.kt new file mode 100644 index 00000000000..cecfb98a97a --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/sca/GhsaEnrichmentParser.kt @@ -0,0 +1,79 @@ +package datadog.gradle.sca + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper + +/** + * Parses GHSA enrichment JSON files from the sca-reachability-database into the internal + * sca_cves.json format consumed by SCA Reachability at runtime. + * + * Key transformations: + * - Filters entries to JVM language only + * - Expands multi-package GHSA entries into N records (one per Maven artifact), because + * each artifact may have different version ranges for the same set of class symbols + * - Converts class FQNs to JVM internal format (slashes) so the ClassFileTransformer + * can do O(1) map lookups without per-class string conversion + * - Sets method=null for all symbols — field exists for forward compatibility when the + * database adds method-level symbols in the future (see APPSEC-62260) + */ +object GhsaEnrichmentParser { + + private val mapper = ObjectMapper() + + /** + * Parses a single GHSA enrichment file. + * + * @param ghsaId the GHSA identifier (e.g. "GHSA-645p-88qh-w398"), used as vuln_id + * @param jsonContent the raw JSON content of the enrichment file + * @return list of sca_cves.json entry maps, one per affected Maven artifact + */ + fun parse(ghsaId: String, jsonContent: String): List> { + val root = mapper.readTree(jsonContent) + require(root.isArray) { "GHSA enrichment file $ghsaId must be a JSON array, got ${root.nodeType}" } + + val entries = mutableListOf>() + + for (entry in root) { + if (entry.path("language").asText() != "jvm") continue + + val symbols = extractSymbols(entry) + if (symbols.isEmpty()) continue + + for (pkg in entry.path("package")) { + if (pkg.path("ecosystem").asText() != "maven") continue + val artifact = pkg.path("name").asText().takeIf { it.isNotEmpty() } ?: continue + val versionRanges = pkg.path("version_range").map { it.asText() } + + entries += mapOf( + "vuln_id" to ghsaId, + "artifact" to artifact, + "version_ranges" to versionRanges, + "symbols" to symbols, + ) + } + } + + return entries + } + + private fun extractSymbols(entry: JsonNode): List> { + val symbols = mutableListOf>() + val imports = entry.path("ecosystem_specific").path("imports") + if (imports.isMissingNode || !imports.isArray) return symbols + + for (importGroup in imports) { + for (symbol in importGroup.path("symbols")) { + if (symbol.path("type").asText() != "class") continue + val pkg = symbol.path("value").asText().takeIf { it.isNotEmpty() } ?: continue + val name = symbol.path("name").asText().takeIf { it.isNotEmpty() } ?: continue + + // JVM internal format (slashes) — avoids per-class conversion in the + // ClassFileTransformer hot path at runtime + val internalName = "$pkg.$name".replace('.', '/') + symbols += mapOf("class" to internalName, "method" to null) + } + } + + return symbols + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/sca/GhsaEnrichmentParserTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/sca/GhsaEnrichmentParserTest.kt new file mode 100644 index 00000000000..7f850b325e5 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/sca/GhsaEnrichmentParserTest.kt @@ -0,0 +1,117 @@ +package datadog.gradle.sca + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class GhsaEnrichmentParserTest { + + private fun fixture(name: String): String = + GhsaEnrichmentParserTest::class.java + .getResourceAsStream("/sca/fixtures/$name")!! + .bufferedReader() + .readText() + + @Test + fun `single package entry produces one record`() { + val entries = GhsaEnrichmentParser.parse("GHSA-single-package", fixture("GHSA-single-package.json")) + + assertThat(entries).hasSize(1) + val entry = entries[0] + assertThat(entry["vuln_id"]).isEqualTo("GHSA-single-package") + assertThat(entry["artifact"]).isEqualTo("com.fasterxml.jackson.core:jackson-databind") + assertThat(entry["version_ranges"]).isEqualTo(listOf("< 2.6.7.3", ">= 2.7.0, < 2.7.9.5")) + } + + @Test + fun `class names are converted to JVM internal format with slashes`() { + val entries = GhsaEnrichmentParser.parse("GHSA-single-package", fixture("GHSA-single-package.json")) + + @Suppress("UNCHECKED_CAST") + val symbols = entries[0]["symbols"] as List> + assertThat(symbols).hasSize(2) + assertThat(symbols.map { it["class"] }).containsExactly( + "com/fasterxml/jackson/databind/ObjectMapper", + "com/fasterxml/jackson/databind/ObjectReader", + ) + } + + @Test + fun `method field is always null for class-level symbols`() { + val entries = GhsaEnrichmentParser.parse("GHSA-single-package", fixture("GHSA-single-package.json")) + + @Suppress("UNCHECKED_CAST") + val symbols = entries[0]["symbols"] as List> + assertThat(symbols).allSatisfy { symbol -> + assertThat(symbol["method"]).isNull() + } + } + + @Test + fun `multi-package entry expands to one record per artifact`() { + val entries = GhsaEnrichmentParser.parse("GHSA-multi-package", fixture("GHSA-multi-package.json")) + + assertThat(entries).hasSize(2) + assertThat(entries.map { it["artifact"] }).containsExactlyInAnyOrder( + "org.springframework.boot:spring-boot-starter-web", + "org.springframework:spring-webmvc", + ) + } + + @Test + fun `multi-package entries each have their own version ranges`() { + val entries = GhsaEnrichmentParser.parse("GHSA-multi-package", fixture("GHSA-multi-package.json")) + + val webEntry = entries.first { it["artifact"] == "org.springframework.boot:spring-boot-starter-web" } + assertThat(webEntry["version_ranges"]).isEqualTo(listOf("< 2.5.12", ">= 2.6.0, < 2.6.6")) + + val mvcEntry = entries.first { it["artifact"] == "org.springframework:spring-webmvc" } + assertThat(mvcEntry["version_ranges"]).isEqualTo(listOf(">= 5.3.0, < 5.3.18", "< 5.2.20.RELEASE")) + } + + @Test + fun `multi-package entries share the same symbols`() { + val entries = GhsaEnrichmentParser.parse("GHSA-multi-package", fixture("GHSA-multi-package.json")) + + @Suppress("UNCHECKED_CAST") + val symbols0 = entries[0]["symbols"] as List> + @Suppress("UNCHECKED_CAST") + val symbols1 = entries[1]["symbols"] as List> + assertThat(symbols0.map { it["class"] }).containsExactlyInAnyOrder( + "org/springframework/stereotype/Controller", + "org/springframework/web/bind/annotation/RestController", + ) + assertThat(symbols0.map { it["class"] }).isEqualTo(symbols1.map { it["class"] }) + } + + @Test + fun `non-jvm language entries are ignored`() { + val entries = GhsaEnrichmentParser.parse("GHSA-mixed-languages", fixture("GHSA-mixed-languages.json")) + + assertThat(entries).hasSize(1) + assertThat(entries[0]["artifact"]).isEqualTo("com.thoughtworks.xstream:xstream") + } + + @Test + fun `entries with no symbols produce no output`() { + val entries = GhsaEnrichmentParser.parse("GHSA-empty-symbols", fixture("GHSA-empty-symbols.json")) + + assertThat(entries).isEmpty() + } + + @Test + fun `ghsa id is used as vuln_id without modification`() { + val ghsaId = "GHSA-645p-88qh-w398" + val entries = GhsaEnrichmentParser.parse(ghsaId, fixture("GHSA-single-package.json")) + + assertThat(entries[0]["vuln_id"]).isEqualTo(ghsaId) + } + + @Test + fun `non-json-array input throws IllegalArgumentException`() { + assertThatThrownBy { + GhsaEnrichmentParser.parse("GHSA-bad", """{"language": "jvm"}""") + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("must be a JSON array") + } +} diff --git a/buildSrc/src/test/resources/sca/fixtures/GHSA-empty-symbols.json b/buildSrc/src/test/resources/sca/fixtures/GHSA-empty-symbols.json new file mode 100644 index 00000000000..e856f6d2f9c --- /dev/null +++ b/buildSrc/src/test/resources/sca/fixtures/GHSA-empty-symbols.json @@ -0,0 +1,15 @@ +[ + { + "language": "jvm", + "package": [ + { + "ecosystem": "maven", + "name": "org.example:some-lib", + "version_range": ["< 1.0.0"] + } + ], + "ecosystem_specific": { + "imports": [] + } + } +] diff --git a/buildSrc/src/test/resources/sca/fixtures/GHSA-mixed-languages.json b/buildSrc/src/test/resources/sca/fixtures/GHSA-mixed-languages.json new file mode 100644 index 00000000000..5f85a1295a1 --- /dev/null +++ b/buildSrc/src/test/resources/sca/fixtures/GHSA-mixed-languages.json @@ -0,0 +1,40 @@ +[ + { + "language": "python", + "package": [ + { + "ecosystem": "pypi", + "name": "requests", + "version_range": ["< 2.28.0"] + } + ], + "ecosystem_specific": { + "imports": [ + { + "symbols": [ + {"type": "function", "value": "requests", "name": "get"} + ] + } + ] + } + }, + { + "language": "jvm", + "package": [ + { + "ecosystem": "maven", + "name": "com.thoughtworks.xstream:xstream", + "version_range": ["< 1.4.16"] + } + ], + "ecosystem_specific": { + "imports": [ + { + "symbols": [ + {"type": "class", "value": "com.thoughtworks.xstream", "name": "XStream"} + ] + } + ] + } + } +] diff --git a/buildSrc/src/test/resources/sca/fixtures/GHSA-multi-package.json b/buildSrc/src/test/resources/sca/fixtures/GHSA-multi-package.json new file mode 100644 index 00000000000..133a7e1ccf2 --- /dev/null +++ b/buildSrc/src/test/resources/sca/fixtures/GHSA-multi-package.json @@ -0,0 +1,41 @@ +[ + { + "language": "jvm", + "package": [ + { + "ecosystem": "maven", + "name": "org.springframework.boot:spring-boot-starter-web", + "version_range": [ + "< 2.5.12", + ">= 2.6.0, < 2.6.6" + ] + }, + { + "ecosystem": "maven", + "name": "org.springframework:spring-webmvc", + "version_range": [ + ">= 5.3.0, < 5.3.18", + "< 5.2.20.RELEASE" + ] + } + ], + "ecosystem_specific": { + "imports": [ + { + "symbols": [ + { + "type": "class", + "value": "org.springframework.stereotype", + "name": "Controller" + }, + { + "type": "class", + "value": "org.springframework.web.bind.annotation", + "name": "RestController" + } + ] + } + ] + } + } +] diff --git a/buildSrc/src/test/resources/sca/fixtures/GHSA-single-package.json b/buildSrc/src/test/resources/sca/fixtures/GHSA-single-package.json new file mode 100644 index 00000000000..3f9c2ea01aa --- /dev/null +++ b/buildSrc/src/test/resources/sca/fixtures/GHSA-single-package.json @@ -0,0 +1,33 @@ +[ + { + "language": "jvm", + "package": [ + { + "ecosystem": "maven", + "name": "com.fasterxml.jackson.core:jackson-databind", + "version_range": [ + "< 2.6.7.3", + ">= 2.7.0, < 2.7.9.5" + ] + } + ], + "ecosystem_specific": { + "imports": [ + { + "symbols": [ + { + "type": "class", + "value": "com.fasterxml.jackson.databind", + "name": "ObjectMapper" + }, + { + "type": "class", + "value": "com.fasterxml.jackson.databind", + "name": "ObjectReader" + } + ] + } + ] + } + } +] diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 5288f92dbe3..c30f46c68bc 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -682,6 +682,7 @@ public void execute() { } maybeStartAppSec(scoClass, sco); + maybeStartScaReachability(instrumentation); maybeStartCiVisibility(instrumentation, scoClass, sco); maybeStartLLMObs(instrumentation, scoClass, sco); // start debugger before remote config to subscribe to it before starting to poll @@ -1079,6 +1080,22 @@ private static boolean isSupportedAppSecArch() { return true; } + private static void maybeStartScaReachability(Instrumentation instrumentation) { + if (!Config.get().isAppSecScaEnabled()) { + return; + } + StaticEventLogger.begin("ScaReachability"); + try { + final Class scaClass = + AGENT_CLASSLOADER.loadClass("com.datadog.appsec.sca.ScaReachabilitySystem"); + final Method startMethod = scaClass.getMethod("start", Instrumentation.class); + startMethod.invoke(null, instrumentation); + } catch (final Throwable ex) { + log.warn("Not starting SCA Reachability subsystem: {}", ex.getMessage()); + } + StaticEventLogger.end("ScaReachability"); + } + private static void maybeStartIast(Instrumentation instrumentation) { if (iastEnabled || !iastFullyDisabled) { diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 2d1fb0abffc..4724f891a4a 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -47,7 +47,91 @@ tasks.named("jar", Jar) { archiveClassifier = 'unbundled' } +// --------------------------------------------------------------------------- +// SCA Reachability: downloads GHSA enrichments and generates sca_cves.json +// --------------------------------------------------------------------------- + +def SCA_ENRICHMENTS_API = + 'https://api.github.com/repos/DataDog/sca-reachability-database/contents/enrichments' + +// Opens an authenticated connection to a GitHub URL with consistent headers and timeouts. +// Throws GradleException on non-200 response — no fallback by design. +// Set GITHUB_TOKEN to raise the unauthenticated rate limit (60 req/hr → 5000 req/hr). +def githubConnect = { String url, String token -> + def connection = (HttpURLConnection) new URL(url).openConnection() + connection.setRequestProperty('Accept', 'application/vnd.github+json') + connection.setRequestProperty('X-GitHub-Api-Version', '2022-11-28') + if (token) { + connection.setRequestProperty('Authorization', "Bearer ${token}") + } + connection.connectTimeout = 10_000 + connection.readTimeout = 30_000 + int code = connection.responseCode + if (code != 200) { + throw new GradleException( + "GitHub API returned HTTP ${code} for ${url}.\n" + + "Unauthenticated rate limit is 60 req/hr. Set GITHUB_TOKEN to raise it.") + } + connection +} + +// Fetches a GitHub URL and parses the response body as JSON. +def githubFetch = { String url, String token -> + def conn = githubConnect(url, token) + try { + new JsonSlurper().parse(conn.inputStream) + } finally { + conn.disconnect() + } +} + +// Fetches a GitHub URL and returns the raw response body as a String. +def githubFetchRaw = { String url, String token -> + def conn = githubConnect(url, token) + try { + conn.inputStream.text + } finally { + conn.disconnect() + } +} + +tasks.register('generateScaCvesJson') { + description = 'Downloads GHSA enrichments from sca-reachability-database and generates sca_cves.json' + group = 'build' + + def outputDir = layout.buildDirectory.dir('generated-resources/sca').get().asFile + def outputFile = new File(outputDir, 'sca_cves.json') + + outputs.file(outputFile) + // Re-runs only when output is missing or -PrefreshSca is passed. + // The database changes rarely; avoiding a network call on every local build is intentional. + outputs.upToDateWhen { outputFile.exists() && !project.hasProperty('refreshSca') } + + doLast { + def token = System.getenv('GITHUB_TOKEN') + + logger.lifecycle('Fetching GHSA enrichment index from GitHub...') + def fileList = githubFetch(SCA_ENRICHMENTS_API, token) as List + def ghsaFiles = fileList.findAll { it.name?.endsWith('.json') && it.type == 'file' } + logger.lifecycle("Found ${ghsaFiles.size()} enrichment files") + + def entries = [] + ghsaFiles.each { fileInfo -> + def ghsaId = (fileInfo.name as String).replace('.json', '') + def rawContent = githubFetchRaw(fileInfo.download_url as String, token) + // Transformation is handled by GhsaEnrichmentParser (tested separately in buildSrc) + entries.addAll(datadog.gradle.sca.GhsaEnrichmentParser.INSTANCE.parse(ghsaId, rawContent)) + } + + outputDir.mkdirs() + outputFile.text = JsonOutput.toJson([version: 1, entries: entries]) + logger.lifecycle("sca_cves.json: ${entries.size()} entries from ${ghsaFiles.size()} GHSA files") + } +} + tasks.named("processResources", ProcessResources) { + dependsOn('generateScaCvesJson') + from(layout.buildDirectory.dir('generated-resources/sca')) doLast { fileTree(dir: outputs.files.asPath, includes: ['**/*.json']).each { it.text = JsonOutput.toJson(new JsonSlurper().parse(it)) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java new file mode 100644 index 00000000000..8c657a32a00 --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java @@ -0,0 +1,119 @@ +package com.datadog.appsec.sca; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Loads {@code sca_cves.json} from the classpath and builds the runtime index used by {@link + * ScaReachabilityTransformer}. + * + *

The primary index maps JVM internal class names (slashes) to the list of {@link ScaEntry} + * objects that reference them. The transformer does an O(1) lookup on every class load using this + * map. + */ +public final class ScaCveDatabase { + + private static final Logger log = LoggerFactory.getLogger(ScaCveDatabase.class); + private static final String RESOURCE_PATH = "/sca_cves.json"; + + private final Map> index; + + private ScaCveDatabase(Map> index) { + this.index = index; + } + + /** + * Loads and parses {@code sca_cves.json} from the classpath. + * + * @return a populated database, or an empty one if the resource is missing or malformed + */ + public static ScaCveDatabase load() { + InputStream stream = ScaCveDatabase.class.getResourceAsStream(RESOURCE_PATH); + if (stream == null) { + log.info( + "SCA Reachability: {} not found on classpath — no vulnerabilities will be tracked", + RESOURCE_PATH); + return new ScaCveDatabase(Collections.emptyMap()); + } + try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + return parse(reader); + } catch (Exception e) { + log.error( + "SCA Reachability: failed to parse {} — no vulnerabilities will be tracked", + RESOURCE_PATH, + e); + return new ScaCveDatabase(Collections.emptyMap()); + } + } + + static ScaCveDatabase parse(java.io.Reader reader) throws IOException { + Moshi moshi = new Moshi.Builder().build(); + Type rootType = Types.newParameterizedType(Map.class, String.class, Object.class); + JsonAdapter> adapter = moshi.adapter(rootType); + + String content = readAll(reader); + Map root = adapter.fromJson(content); + if (root == null) { + throw new IOException("sca_cves.json is empty"); + } + + List rawEntries = (List) root.get("entries"); + if (rawEntries == null) { + return new ScaCveDatabase(Collections.emptyMap()); + } + + Map> index = new HashMap<>(); + int entryCount = 0; + + for (Object rawEntry : rawEntries) { + Map entryMap = (Map) rawEntry; + ScaEntry entry = ScaEntry.fromMap(entryMap); + if (entry == null) continue; + entryCount++; + + for (ScaSymbol symbol : entry.symbols()) { + index.computeIfAbsent(symbol.className(), k -> new ArrayList<>()).add(entry); + } + } + + log.debug( + "SCA Reachability: loaded {} entries, {} unique class symbols", entryCount, index.size()); + return new ScaCveDatabase(Collections.unmodifiableMap(index)); + } + + /** Returns the entries associated with the given JVM internal class name, or null if none. */ + public List entriesForClass(String internalClassName) { + return index.get(internalClassName); + } + + public boolean isEmpty() { + return index.isEmpty(); + } + + public int size() { + return index.size(); + } + + private static String readAll(java.io.Reader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + char[] buf = new char[8192]; + int n; + while ((n = reader.read(buf)) != -1) { + sb.append(buf, 0, n); + } + return sb.toString(); + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java new file mode 100644 index 00000000000..9d9e33a17f8 --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java @@ -0,0 +1,89 @@ +package com.datadog.appsec.sca; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** One entry from sca_cves.json: a vulnerability affecting a specific Maven artifact. */ +public final class ScaEntry { + + private static final Logger log = LoggerFactory.getLogger(ScaEntry.class); + + private final String vulnId; + private final String artifact; + private final List versionRanges; + private final List symbols; + + public ScaEntry( + String vulnId, String artifact, List versionRanges, List symbols) { + this.vulnId = vulnId; + this.artifact = artifact; + this.versionRanges = Collections.unmodifiableList(versionRanges); + this.symbols = Collections.unmodifiableList(symbols); + } + + /** GHSA identifier, e.g. {@code "GHSA-645p-88qh-w398"}. */ + public String vulnId() { + return vulnId; + } + + /** Maven coordinate, e.g. {@code "com.fasterxml.jackson.core:jackson-databind"}. */ + public String artifact() { + return artifact; + } + + /** + * Version range strings from sca_cves.json, e.g. {@code ["< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"]}. + */ + public List versionRanges() { + return versionRanges; + } + + public List symbols() { + return symbols; + } + + /** Returns true if the given version falls within any of this entry's version ranges. */ + public boolean isVersionVulnerable(String version) { + return VersionRangeParser.matchesAny(version, versionRanges); + } + + @Nullable + static ScaEntry fromMap(Map map) { + try { + String vulnId = (String) map.get("vuln_id"); + String artifact = (String) map.get("artifact"); + List rawRanges = (List) map.get("version_ranges"); + List rawSymbols = (List) map.get("symbols"); + + if (vulnId == null || artifact == null || rawRanges == null || rawSymbols == null) { + log.debug("SCA Reachability: skipping malformed entry: {}", map); + return null; + } + + List versionRanges = new ArrayList<>(rawRanges.size()); + for (Object r : rawRanges) { + versionRanges.add((String) r); + } + + List symbols = new ArrayList<>(rawSymbols.size()); + for (Object rawSymbol : rawSymbols) { + Map symbolMap = (Map) rawSymbol; + String className = (String) symbolMap.get("class"); + String method = (String) symbolMap.get("method"); + if (className == null) continue; + symbols.add(new ScaSymbol(className, method)); + } + + if (symbols.isEmpty()) return null; + return new ScaEntry(vulnId, artifact, versionRanges, symbols); + } catch (Exception e) { + log.debug("SCA Reachability: skipping malformed entry", e); + return null; + } + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java new file mode 100644 index 00000000000..3babc5346b7 --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -0,0 +1,51 @@ +package com.datadog.appsec.sca; + +import java.lang.instrument.Instrumentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Entry point for the SCA Reachability subsystem. Called from {@code Agent.java} via reflection + * (same pattern as {@code AppSecSystem} and {@code IastSystem}). + * + *

Responsibilities: + * + *

    + *
  1. Load {@code sca_cves.json} from the classpath. + *
  2. Build the class-name index. + *
  3. Register {@link ScaReachabilityTransformer} with the JVM. + *
  4. Scan already-loaded classes so that libraries loaded before agent startup are detected. + *
+ */ +public final class ScaReachabilitySystem { + + private static final Logger log = LoggerFactory.getLogger(ScaReachabilitySystem.class); + + private ScaReachabilitySystem() {} + + /** + * Starts the SCA Reachability subsystem. + * + *

Called by reflection from {@code Agent.maybeStartScaReachability()} — the method signature + * must remain {@code public static void start(Instrumentation)}. + */ + public static void start(Instrumentation instrumentation) { + ScaCveDatabase database = ScaCveDatabase.load(); + if (database.isEmpty()) { + log.info("SCA Reachability: no vulnerability data found — subsystem inactive"); + return; + } + log.info("SCA Reachability: loaded {} vulnerable class symbols", database.size()); + + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(database); + + // canRetransform=true is required so that future method-level symbols (when added to the + // database) can trigger retransformation of already-loaded classes via retransformClasses(). + // For current class-level symbols, retransformation is not used — see + // checkAlreadyLoadedClasses. + instrumentation.addTransformer(transformer, true); + + transformer.checkAlreadyLoadedClasses(instrumentation); + log.debug("SCA Reachability: startup scan complete"); + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java new file mode 100644 index 00000000000..1f16cafca68 --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -0,0 +1,232 @@ +package com.datadog.appsec.sca; + +import datadog.telemetry.dependency.Dependency; +import datadog.telemetry.dependency.DependencyResolver; +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Observation-only {@link ClassFileTransformer} that detects when classes from vulnerable libraries + * are loaded and reports reachability hits via {@link ScaReachabilityCollector}. + * + *

Design principles (see APPSEC-62260 and .claude-invariants.md): + * + *

    + *
  • Always returns {@code null} — never modifies bytecode for class-level symbols. + *
  • Never throws — any error in {@link #transform} is caught silently to avoid breaking class + * loading. + *
  • All shared state uses concurrent collections — {@link #transform} is called from multiple + * class-loading threads simultaneously. + *
  • Version resolution is cached per JAR URL — each JAR is read at most once. + *
  • Each (vulnId, artifact) pair is reported at most once — RFC requires a single occurrence. + *
  • Path B (JDK classes such as {@code java.sql.PreparedStatement}) is handled only in {@link + * #checkAlreadyLoadedClasses}, not in {@link #transform}, because JDK classes are always + * loaded at startup. If a JDK class relevant to a CVE were loaded lazily after startup, the + * detection would be missed. This is a known, documented trade-off. + *
+ */ +public final class ScaReachabilityTransformer implements ClassFileTransformer { + + private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); + + private final ScaCveDatabase database; + + /** Cache: JAR URL → resolved dependencies (empty list = JAR has no pom.properties). */ + private final ConcurrentHashMap> jarCache = new ConcurrentHashMap<>(); + + /** Deduplication set: "vulnId|artifact" pairs already reported. */ + private final Set reportedHits = ConcurrentHashMap.newKeySet(); + + public ScaReachabilityTransformer(ScaCveDatabase database) { + this.database = database; + } + + // --------------------------------------------------------------------------- + // ClassFileTransformer + // --------------------------------------------------------------------------- + + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + try { + // Filter array types (e.g. "[Ljava/sql/PreparedStatement;"). + if (className == null || className.charAt(0) == '[') { + return null; + } + + // JDK/bootstrap classes (protectionDomain == null) are handled at startup in + // checkAlreadyLoadedClasses() via Path B. Skip here to avoid per-bootstrap-class overhead. + if (protectionDomain == null) { + return null; + } + + List entries = database.entriesForClass(className); + if (entries == null || entries.isEmpty()) { + return null; + } + + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return null; // runtime-generated class (dynamic proxy, lambda, etc.) + } + URL location = codeSource.getLocation(); + if (location == null) { + return null; + } + + processPathA(className, location, entries); + } catch (Throwable t) { + // Never propagate from transform() — it would break the class being loaded. + log.debug("SCA Reachability: error processing class {}", className, t); + } + return null; // observation only — never modify bytecode for class-level symbols + } + + // --------------------------------------------------------------------------- + // Startup scan for already-loaded classes + // --------------------------------------------------------------------------- + + /** + * Checks classes already loaded before this transformer was registered. + * + *

Path A: 3rd-party classes — version resolved from the class's own JAR via {@link + * ProtectionDomain}. + * + *

Path B: JDK/standard-library classes (e.g. {@code java.sql.PreparedStatement}) — {@code + * ProtectionDomain} is null, so we scan the system classloader's URL chain for the associated + * Maven artifact. + * + *

Assumption: JDK-sourced symbols in vulnerability data are loaded at startup, not + * lazily during normal application operation. If an application defers JDK class loading past + * agent startup (e.g. lazy JDBC initialisation), Path B hits for those classes will be missed. + * See APPSEC-62260 for design rationale. + */ + public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { + for (Class clazz : instrumentation.getAllLoadedClasses()) { + String internalName = clazz.getName().replace('.', '/'); + if (internalName.charAt(0) == '[') { + continue; + } + List entries = database.entriesForClass(internalName); + if (entries == null || entries.isEmpty()) { + continue; + } + + ProtectionDomain pd = clazz.getProtectionDomain(); + URL location = locationOf(pd); + try { + if (location == null) { + processPathB(internalName, entries); // JDK class + } else { + processPathA(internalName, location, entries); // 3rd-party class + } + } catch (Exception e) { + // Never abort the scan — a failure on one class must not skip the remaining ones. + log.debug("SCA Reachability: error scanning already-loaded class {}", internalName, e); + } + } + } + + // --------------------------------------------------------------------------- + // Internal matching logic + // --------------------------------------------------------------------------- + + /** Path A: class came from a 3rd-party JAR — match artifact + check version. */ + private void processPathA(String internalClassName, URL jarUrl, List entries) { + List deps = resolveDependencies(jarUrl); + for (ScaEntry entry : entries) { + for (Dependency dep : deps) { + if (entry.artifact().equals(dep.name) && entry.isVersionVulnerable(dep.version)) { + reportHit(entry, dep.version, internalClassName); + } + } + } + } + + /** Path B: class came from the JDK — find the vulnerable artifact in the classloader chain. */ + private void processPathB(String internalClassName, List entries) { + for (ScaEntry entry : entries) { + String version = findArtifactVersionInClasspath(entry.artifact()); + if (version != null && entry.isVersionVulnerable(version)) { + reportHit(entry, version, internalClassName); + } + } + } + + private String findArtifactVersionInClasspath(String artifactName) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + while (cl != null) { + if (cl instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) cl).getURLs()) { + for (Dependency dep : resolveDependencies(url)) { + if (artifactName.equals(dep.name) && dep.version != null) { + return dep.version; + } + } + } + } + cl = cl.getParent(); + } + return null; + } + + private void reportHit(ScaEntry entry, String version, String internalClassName) { + String dedupKey = entry.vulnId() + "|" + entry.artifact(); + if (!reportedHits.add(dedupKey)) { + return; // already reported this (vulnId, artifact) pair — RFC: single occurrence sufficient + } + String dotClassName = internalClassName.replace('/', '.'); + log.debug( + "SCA Reachability: {} reached in {}:{} via class {}", + entry.vulnId(), + entry.artifact(), + version, + dotClassName); + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit(entry.vulnId(), entry.artifact(), version, dotClassName)); + } + + private List resolveDependencies(URL url) { + List cached = jarCache.get(url); + if (cached != null) { + return cached; + } + List resolved; + try { + URI uri = url.toURI(); + resolved = DependencyResolver.resolve(uri); + if (resolved == null) { + resolved = Collections.emptyList(); + } + } catch (Exception e) { + log.debug("SCA Reachability: could not resolve {}", url, e); + resolved = Collections.emptyList(); + } + List existing = jarCache.putIfAbsent(url, resolved); + return existing != null ? existing : resolved; + } + + private static URL locationOf(ProtectionDomain pd) { + if (pd == null) return null; + CodeSource cs = pd.getCodeSource(); + if (cs == null) return null; + return cs.getLocation(); + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaSymbol.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaSymbol.java new file mode 100644 index 00000000000..537a208ae6b --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaSymbol.java @@ -0,0 +1,33 @@ +package com.datadog.appsec.sca; + +import javax.annotation.Nullable; + +/** A single symbol from sca_cves.json: a class (and optionally a method) to watch for. */ +public final class ScaSymbol { + + private final String className; // JVM internal format: "com/foo/Bar" + @Nullable private final String method; // null = class-level; non-null = future method-level + + public ScaSymbol(String className, @Nullable String method) { + this.className = className; + this.method = method; + } + + /** JVM internal class name with slashes, e.g. {@code "com/foo/Bar"}. */ + public String className() { + return className; + } + + /** + * Method name for method-level tracking, or null for class-level. Currently always null since the + * database only has class-level symbols. + */ + @Nullable + public String method() { + return method; + } + + public boolean isClassLevel() { + return method == null; + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java new file mode 100644 index 00000000000..cfcaec396e5 --- /dev/null +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java @@ -0,0 +1,82 @@ +package com.datadog.appsec.sca; + +import datadog.trace.util.ComparableVersion; +import java.util.List; + +/** + * Checks whether a version string matches GHSA version range expressions. + * + *

Range string format (per sca-reachability-database enrichments): + * + *

    + *
  • {@code "< 2.6.7.3"} — strictly less than + *
  • {@code "<= 2.6.7.3"} — less than or equal + *
  • {@code "> 2.6.7.3"} — strictly greater than + *
  • {@code ">= 2.6.7.3"} — greater than or equal + *
  • {@code "= 2.6.7.3"} — exact match + *
  • {@code ">= 2.7.0, < 2.7.9.5"} — AND of two conditions (comma-separated) + *
+ * + *

Multiple range strings in a list are evaluated as OR: a version is affected if it matches ANY + * of the ranges. + * + *

Version comparison uses {@link ComparableVersion} (Maven 3.9.9 semantics), which correctly + * handles qualifiers such as {@code .RELEASE}, {@code .GA}, {@code .FINAL}, and 4-part versions. + */ +public final class VersionRangeParser { + + private VersionRangeParser() {} + + /** + * Returns true if {@code version} matches at least one of the provided range strings. + * + * @param version the version to test (e.g. {@code "2.8.5"} or {@code "5.2.19.RELEASE"}) + * @param versionRanges list of range strings from sca_cves.json + * @return true if the version falls within any range + */ + public static boolean matchesAny(String version, List versionRanges) { + if (version == null || version.isEmpty() || versionRanges == null || versionRanges.isEmpty()) { + return false; + } + ComparableVersion v = new ComparableVersion(version); + for (String range : versionRanges) { + if (matchesRange(v, range)) { + return true; + } + } + return false; + } + + /** + * Returns true if {@code version} matches a single range string. Multiple conditions within a + * single string (comma-separated) are evaluated as AND. + */ + static boolean matchesRange(ComparableVersion version, String versionRange) { + String[] conditions = versionRange.split(","); + for (String condition : conditions) { + if (!matchesCondition(version, condition.trim())) { + return false; + } + } + return true; + } + + private static boolean matchesCondition(ComparableVersion version, String condition) { + if (condition.startsWith(">=")) { + return version.compareTo(new ComparableVersion(condition.substring(2).trim())) >= 0; + } + if (condition.startsWith("<=")) { + return version.compareTo(new ComparableVersion(condition.substring(2).trim())) <= 0; + } + if (condition.startsWith(">")) { + return version.compareTo(new ComparableVersion(condition.substring(1).trim())) > 0; + } + if (condition.startsWith("<")) { + return version.compareTo(new ComparableVersion(condition.substring(1).trim())) < 0; + } + if (condition.startsWith("=")) { + return version.compareTo(new ComparableVersion(condition.substring(1).trim())) == 0; + } + throw new IllegalArgumentException("Unrecognised version condition: '" + condition + "'"); + } +} diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaCveDatabaseTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaCveDatabaseTest.java new file mode 100644 index 00000000000..791130f43fe --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaCveDatabaseTest.java @@ -0,0 +1,110 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringReader; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ScaCveDatabaseTest { + + private static final String MINIMAL_JSON = + "{\"version\":1,\"entries\":[" + + "{\"vuln_id\":\"GHSA-test-1234-5678\"," + + "\"artifact\":\"com.example:lib\"," + + "\"version_ranges\":[\"< 2.0.0\"]," + + "\"symbols\":[" + + "{\"class\":\"com/example/Foo\",\"method\":null}," + + "{\"class\":\"com/example/Bar\",\"method\":null}" + + "]}]}"; + + @Test + void loadsFromJson() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(MINIMAL_JSON)); + + assertFalse(db.isEmpty()); + assertEquals(2, db.size()); // 2 unique class names + } + + @Test + void indexedByClassName() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(MINIMAL_JSON)); + + List entries = db.entriesForClass("com/example/Foo"); + assertNotNull(entries); + assertEquals(1, entries.size()); + assertEquals("GHSA-test-1234-5678", entries.get(0).vulnId()); + assertEquals("com.example:lib", entries.get(0).artifact()); + } + + @Test + void unknownClassReturnsNull() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(MINIMAL_JSON)); + + assertNull(db.entriesForClass("com/example/Unknown")); + } + + @Test + void emptyEntriesProducesEmptyDatabase() throws Exception { + String json = "{\"version\":1,\"entries\":[]}"; + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(json)); + + assertTrue(db.isEmpty()); + } + + @Test + void malformedEntryIsSkipped() throws Exception { + String json = + "{\"version\":1,\"entries\":[" + + "{\"vuln_id\":null,\"artifact\":\"com.example:lib\"," + + "\"version_ranges\":[\"< 2.0.0\"],\"symbols\":[{\"class\":\"com/example/Foo\",\"method\":null}]}," + + "{\"vuln_id\":\"GHSA-good-0000-0000\",\"artifact\":\"com.example:other\"," + + "\"version_ranges\":[\"< 1.0.0\"],\"symbols\":[{\"class\":\"com/example/Good\",\"method\":null}]}" + + "]}"; + + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(json)); + + assertNull(db.entriesForClass("com/example/Foo")); + assertNotNull(db.entriesForClass("com/example/Good")); + } + + @Test + void multipleEntriesForSameClass() throws Exception { + String json = + "{\"version\":1,\"entries\":[" + + "{\"vuln_id\":\"GHSA-aaaa-0001-0001\",\"artifact\":\"com.example:lib\"," + + "\"version_ranges\":[\"< 2.0.0\"],\"symbols\":[{\"class\":\"com/example/Shared\",\"method\":null}]}," + + "{\"vuln_id\":\"GHSA-bbbb-0002-0002\",\"artifact\":\"com.example:lib\"," + + "\"version_ranges\":[\"< 3.0.0\"],\"symbols\":[{\"class\":\"com/example/Shared\",\"method\":null}]}" + + "]}"; + + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(json)); + + List entries = db.entriesForClass("com/example/Shared"); + assertNotNull(entries); + assertEquals(2, entries.size()); + } + + @Test + void loadFromClasspathSucceeds() { + // Verifies the real sca_cves.json generated by generateScaCvesJson is valid and loadable + ScaCveDatabase db = ScaCveDatabase.load(); + + assertFalse(db.isEmpty(), "sca_cves.json should be on the classpath and contain entries"); + assertTrue(db.size() > 0); + } + + @Test + void jacksonDatabindObjectMapperIsIndexed() { + // Spot-check a known entry from the real database + ScaCveDatabase db = ScaCveDatabase.load(); + + List entries = db.entriesForClass("com/fasterxml/jackson/databind/ObjectMapper"); + assertNotNull(entries, "jackson-databind ObjectMapper should be in the database"); + assertFalse(entries.isEmpty()); + } +} diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java new file mode 100644 index 00000000000..9c63cd445eb --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java @@ -0,0 +1,189 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import java.io.File; +import java.io.StringReader; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScaReachabilityTransformerTest { + + private static final String JACKSON_JSON = + "{\"version\":1,\"entries\":[{" + + "\"vuln_id\":\"GHSA-test-jackson\"," + + "\"artifact\":\"com.fasterxml.jackson.core:jackson-databind\"," + + "\"version_ranges\":[\"< 2.9.0\"]," + + "\"symbols\":[{\"class\":\"com/fasterxml/jackson/databind/ObjectMapper\",\"method\":null}]" + + "}]}"; + + private ScaReachabilityTransformer transformer; + + @BeforeEach + void setUp() throws Exception { + // Drain any hits left from previous tests + ScaReachabilityCollector.INSTANCE.drain(); + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + transformer = new ScaReachabilityTransformer(db); + } + + // --- transform() return value --- + + @Test + void transformAlwaysReturnsNull() throws Exception { + ProtectionDomain pd = protectionDomainFor(jarUrl("jackson-databind-2.8.5.jar")); + byte[] result = + transformer.transform( + null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); + assertNull(result, "transform() must never return non-null for class-level symbols"); + } + + @Test + void transformReturnsNullForArrayTypes() { + byte[] result = + transformer.transform( + null, "[Lcom/fasterxml/jackson/databind/ObjectMapper;", null, null, new byte[0]); + assertNull(result); + } + + @Test + void transformReturnsNullForNullClassName() { + byte[] result = transformer.transform(null, null, null, null, new byte[0]); + assertNull(result); + } + + @Test + void transformReturnsNullForNullProtectionDomain() { + // JDK class — protectionDomain is null; handled at startup via Path B + byte[] result = + transformer.transform( + null, "com/fasterxml/jackson/databind/ObjectMapper", null, null, new byte[0]); + assertNull(result); + assertTrue( + ScaReachabilityCollector.INSTANCE.drain().isEmpty(), + "No hit expected for JDK-sourced class in transform()"); + } + + @Test + void transformReturnsNullForClassNotInDatabase() throws Exception { + ProtectionDomain pd = protectionDomainFor(jarUrl("some-other-lib.jar")); + byte[] result = + transformer.transform(null, "com/example/UnrelatedClass", null, pd, new byte[0]); + assertNull(result); + assertTrue(ScaReachabilityCollector.INSTANCE.drain().isEmpty()); + } + + // --- hit detection --- + + @Test + void detectsVulnerableClassFromRealJar() throws Exception { + // Use the actual jackson-databind JAR on the test classpath as the location + URL jacksonJar = findJarOnClasspath("jackson-databind"); + if (jacksonJar == null) { + // If the JAR is not on the test classpath, skip this test + return; + } + ProtectionDomain pd = protectionDomainFor(jacksonJar); + transformer.transform( + null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); + + List hits = ScaReachabilityCollector.INSTANCE.drain(); + // Only assert if the version is actually vulnerable (< 2.9.0) + // We can't assert a specific hit here since the test classpath version may vary + assertTrue(hits.size() <= 1, "At most one hit per (vulnId, artifact) pair"); + } + + @Test + void deduplicatesHitsForSameVulnAndArtifact() throws Exception { + URL fakeJar = getClass().getResource("/"); // any URL will do; DependencyResolver returns empty + if (fakeJar == null) return; + + ProtectionDomain pd = protectionDomainFor(fakeJar); + // Call transform twice for the same class — should produce at most one hit + transformer.transform( + null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); + transformer.transform( + null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); + + List hits = ScaReachabilityCollector.INSTANCE.drain(); + assertTrue( + hits.size() <= 1, "Deduplication must ensure at most one hit per (vulnId, artifact)"); + } + + // --- checkAlreadyLoadedClasses --- + + @Test + void checkAlreadyLoadedClasses_completesWithoutThrowingForUnresolvableClass() throws Exception { + // Verify the method runs without throwing for a class not in the database + // (ScaReachabilityTransformer itself is a class guaranteed to be loaded) + java.lang.instrument.Instrumentation inst = + fakeInstrumentationReturning(ScaReachabilityTransformer.class); + + transformer.checkAlreadyLoadedClasses(inst); + + // No hit — this class is not in our test DB (which only has jackson) + assertTrue(ScaReachabilityCollector.INSTANCE.drain().isEmpty()); + } + + @Test + void checkAlreadyLoadedClasses_doesNotAbortOnException() throws Exception { + // A class in the DB whose CodeSource URL points to a non-existent JAR should not abort + // processing of subsequent classes. We pass two classes: one that will fail resolution + // (fake JAR), and one that is not in the DB at all — both should be processed without throw. + java.lang.instrument.Instrumentation inst = + fakeInstrumentationReturning( + // ScaReachabilityTransformer is NOT in the DB → quick exit, no error + ScaReachabilityTransformer.class, + // Object.class has null protectionDomain → Path B → no artifact found → no hit + Object.class); + + // Must complete without throwing even when individual class processing fails + transformer.checkAlreadyLoadedClasses(inst); + ScaReachabilityCollector.INSTANCE.drain(); + } + + private static java.lang.instrument.Instrumentation fakeInstrumentationReturning( + Class... classes) { + return (java.lang.instrument.Instrumentation) + java.lang.reflect.Proxy.newProxyInstance( + ScaReachabilityTransformerTest.class.getClassLoader(), + new Class[] {java.lang.instrument.Instrumentation.class}, + (proxy, method, args) -> { + if ("getAllLoadedClasses".equals(method.getName())) { + return classes; + } + return null; + }); + } + + // --- helpers --- + + private static ProtectionDomain protectionDomainFor(URL location) throws Exception { + CodeSource cs = new CodeSource(location, (java.security.cert.Certificate[]) null); + return new ProtectionDomain(cs, null); + } + + private static URL jarUrl(String name) throws Exception { + return new File("/tmp/" + name).toURI().toURL(); + } + + private static URL findJarOnClasspath(String partialName) { + String cp = System.getProperty("java.class.path", ""); + for (String entry : cp.split(File.pathSeparator)) { + if (entry.contains(partialName) && entry.endsWith(".jar")) { + try { + return new File(entry).toURI().toURL(); + } catch (Exception ignored) { + } + } + } + return null; + } +} diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/VersionRangeParserTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/VersionRangeParserTest.java new file mode 100644 index 00000000000..1e74d9de5d6 --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/VersionRangeParserTest.java @@ -0,0 +1,181 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class VersionRangeParserTest { + + // --- matchesAny: null / empty guards --- + + @Test + void nullVersionReturnsFalse() { + assertFalse(VersionRangeParser.matchesAny(null, Arrays.asList("< 2.0.0"))); + } + + @Test + void emptyVersionReturnsFalse() { + assertFalse(VersionRangeParser.matchesAny("", Arrays.asList("< 2.0.0"))); + } + + @Test + void nullRangesReturnsFalse() { + assertFalse(VersionRangeParser.matchesAny("1.0.0", null)); + } + + @Test + void emptyRangesReturnsFalse() { + assertFalse(VersionRangeParser.matchesAny("1.0.0", Collections.emptyList())); + } + + // --- single-condition operators --- + + @Test + void lessThan_belowBound() { + assertTrue(VersionRangeParser.matchesAny("2.6.7.2", Arrays.asList("< 2.6.7.3"))); + } + + @Test + void lessThan_atBound() { + assertFalse(VersionRangeParser.matchesAny("2.6.7.3", Arrays.asList("< 2.6.7.3"))); + } + + @Test + void lessThan_aboveBound() { + assertFalse(VersionRangeParser.matchesAny("2.6.7.4", Arrays.asList("< 2.6.7.3"))); + } + + @Test + void lessThanOrEqual_atBound() { + assertTrue(VersionRangeParser.matchesAny("2.6.7.3", Arrays.asList("<= 2.6.7.3"))); + } + + @Test + void lessThanOrEqual_aboveBound() { + assertFalse(VersionRangeParser.matchesAny("2.6.7.4", Arrays.asList("<= 2.6.7.3"))); + } + + @Test + void greaterThan_aboveBound() { + assertTrue(VersionRangeParser.matchesAny("9.5.1", Arrays.asList("> 9.5.0"))); + } + + @Test + void greaterThan_atBound() { + assertFalse(VersionRangeParser.matchesAny("9.5.0", Arrays.asList("> 9.5.0"))); + } + + @Test + void greaterThanOrEqual_atBound() { + assertTrue(VersionRangeParser.matchesAny("9.5.0", Arrays.asList(">= 9.5.0"))); + } + + @Test + void exactMatch_matches() { + assertTrue(VersionRangeParser.matchesAny("9.5.0", Arrays.asList("= 9.5.0"))); + } + + @Test + void exactMatch_doesNotMatch() { + assertFalse(VersionRangeParser.matchesAny("9.5.1", Arrays.asList("= 9.5.0"))); + } + + // --- compound condition (AND within one string) --- + + @Test + void compoundRange_withinBounds() { + assertTrue(VersionRangeParser.matchesAny("2.7.5", Arrays.asList(">= 2.7.0, < 2.7.9.5"))); + } + + @Test + void compoundRange_realGhsaJackson() { + List ranges = Arrays.asList("< 2.6.7.3", ">= 2.7.0, < 2.7.9.5", ">= 2.8.0, < 2.8.11.3"); + assertTrue(VersionRangeParser.matchesAny("2.8.5", ranges)); + assertTrue(VersionRangeParser.matchesAny("2.7.5", ranges)); + assertTrue(VersionRangeParser.matchesAny("2.6.0", ranges)); + assertFalse(VersionRangeParser.matchesAny("2.9.7", ranges)); + } + + @Test + void compoundRange_atLowerBound() { + assertTrue(VersionRangeParser.matchesAny("2.7.0", Arrays.asList(">= 2.7.0, < 2.7.9.5"))); + } + + @Test + void compoundRange_atUpperBound() { + assertFalse(VersionRangeParser.matchesAny("2.7.9.5", Arrays.asList(">= 2.7.0, < 2.7.9.5"))); + } + + @Test + void compoundRange_belowLowerBound() { + assertFalse(VersionRangeParser.matchesAny("2.6.9", Arrays.asList(">= 2.7.0, < 2.7.9.5"))); + } + + // --- OR across multiple range strings --- + + @Test + void multipleRanges_matchesFirstRange() { + List ranges = Arrays.asList("< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"); + assertTrue(VersionRangeParser.matchesAny("2.6.0", ranges)); + } + + @Test + void multipleRanges_matchesSecondRange() { + List ranges = Arrays.asList("< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"); + assertTrue(VersionRangeParser.matchesAny("2.7.5", ranges)); + } + + @Test + void multipleRanges_matchesNeitherRange() { + List ranges = Arrays.asList("< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"); + assertFalse(VersionRangeParser.matchesAny("2.6.8", ranges)); + } + + // --- Maven qualifier handling (Gap 10) --- + + @Test + void releaseQualifier_belowBound() { + assertTrue(VersionRangeParser.matchesAny("5.2.19.RELEASE", Arrays.asList("< 5.2.20.RELEASE"))); + } + + @Test + void releaseQualifier_atBound() { + assertFalse(VersionRangeParser.matchesAny("5.2.20.RELEASE", Arrays.asList("< 5.2.20.RELEASE"))); + } + + @Test + void releaseQualifier_equivalentToPlain() { + // 5.2.20.RELEASE == 5.2.20 in Maven versioning + assertFalse(VersionRangeParser.matchesAny("5.2.20", Arrays.asList("< 5.2.20.RELEASE"))); + } + + @Test + void releaseQualifier_compoundRange() { + assertTrue(VersionRangeParser.matchesAny("5.3.10", Arrays.asList(">= 5.3.0, < 5.3.18"))); + assertFalse( + VersionRangeParser.matchesAny("5.2.20.RELEASE", Arrays.asList(">= 5.3.0, < 5.3.18"))); + } + + // --- 4-part versions --- + + @Test + void fourPartVersion() { + assertTrue(VersionRangeParser.matchesAny("2.6.7.2", Arrays.asList("< 2.6.7.3"))); + assertFalse(VersionRangeParser.matchesAny("2.6.7.3", Arrays.asList("< 2.6.7.3"))); + assertFalse(VersionRangeParser.matchesAny("2.6.7.4", Arrays.asList("< 2.6.7.3"))); + } + + // --- error handling --- + + @Test + void unknownOperatorThrows() { + assertThrows( + IllegalArgumentException.class, + () -> VersionRangeParser.matchesAny("1.0.0", Arrays.asList("~ 2.0.0"))); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java new file mode 100644 index 00000000000..d9c2fc93fdd --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java @@ -0,0 +1,41 @@ +package datadog.trace.api.telemetry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Singleton queue bridging the SCA reachability transformer (appsec module) and the periodic + * telemetry action (telemetry module) without creating a circular dependency. + * + *

Pattern mirrors {@code WafMetricCollector}: both modules depend on {@code internal-api}, which + * owns this class. The transformer enqueues hits; the periodic action drains them. + */ +public final class ScaReachabilityCollector { + + public static final ScaReachabilityCollector INSTANCE = new ScaReachabilityCollector(); + + private final BlockingQueue hits = new LinkedBlockingQueue<>(); + + private ScaReachabilityCollector() {} + + /** Called by {@code ScaReachabilityTransformer} when a vulnerable class is detected. */ + public void addHit(ScaReachabilityHit hit) { + hits.offer(hit); + } + + /** + * Called by {@code ScaReachabilityPeriodicAction} on each telemetry heartbeat. Drains and returns + * all pending hits. + */ + public List drain() { + if (hits.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(hits.size()); + hits.drainTo(result); + return result; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java new file mode 100644 index 00000000000..72c5b7f21e7 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java @@ -0,0 +1,40 @@ +package datadog.trace.api.telemetry; + +/** + * A single SCA reachability detection: a vulnerable class from a known artifact was loaded at + * runtime. Produced by {@code ScaReachabilityTransformer} and consumed by {@code + * ScaReachabilityPeriodicAction} to build the telemetry payload. + */ +public final class ScaReachabilityHit { + + private final String vulnId; + private final String artifact; + private final String version; + private final String className; // dot-notation FQN, e.g. "com.foo.Bar" + + public ScaReachabilityHit(String vulnId, String artifact, String version, String className) { + this.vulnId = vulnId; + this.artifact = artifact; + this.version = version; + this.className = className; + } + + /** GHSA identifier, e.g. {@code "GHSA-645p-88qh-w398"}. */ + public String vulnId() { + return vulnId; + } + + /** Maven coordinate, e.g. {@code "com.fasterxml.jackson.core:jackson-databind"}. */ + public String artifact() { + return artifact; + } + + public String version() { + return version; + } + + /** Fully-qualified class name in dot notation, e.g. {@code "com.foo.Bar"}. */ + public String className() { + return className; + } +} diff --git a/telemetry/build.gradle.kts b/telemetry/build.gradle.kts index 1b66facc063..17578a55877 100644 --- a/telemetry/build.gradle.kts +++ b/telemetry/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { api(libs.moshi) testImplementation(project(":utils:test-utils")) + testImplementation(libs.bundles.mockito) testImplementation(group = "org.jboss", name = "jboss-vfs", version = "3.2.16.Final") } diff --git a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java index 3c3cf3d35e8..65cfd7d166e 100644 --- a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java +++ b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java @@ -271,6 +271,16 @@ public void writeDependency(Dependency d) throws IOException { bodyWriter.name("hash").value(d.hash); // optional bodyWriter.name("name").value(d.name); bodyWriter.name("version").value(d.version); // optional + if (d.reachabilityMetadata != null && !d.reachabilityMetadata.isEmpty()) { + bodyWriter.name("metadata").beginArray(); + for (String value : d.reachabilityMetadata) { + bodyWriter.beginObject(); + bodyWriter.name("type").value("reachability"); + bodyWriter.name("value").value(value); // stringified JSON per RFC + bodyWriter.endObject(); + } + bodyWriter.endArray(); + } bodyWriter.endObject(); } diff --git a/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java b/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java index d49d9c21bcc..0d6a001425c 100644 --- a/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java +++ b/telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java @@ -19,6 +19,7 @@ import datadog.telemetry.metric.WafMetricPeriodicAction; import datadog.telemetry.products.ProductChangeAction; import datadog.telemetry.rum.RumPeriodicAction; +import datadog.telemetry.sca.ScaReachabilityPeriodicAction; import datadog.trace.api.Config; import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.civisibility.config.BazelMode; @@ -89,6 +90,9 @@ static Thread createTelemetryRunnable( if (Config.get().isApiSecurityEndpointCollectionEnabled()) { actions.add(new EndpointPeriodicAction()); } + if (Config.get().isAppSecScaEnabled()) { + actions.add(new ScaReachabilityPeriodicAction()); + } TelemetryRunnable telemetryRunnable = new TelemetryRunnable(telemetryService, actions); return AgentThreadFactory.newAgentThread( diff --git a/telemetry/src/main/java/datadog/telemetry/dependency/Dependency.java b/telemetry/src/main/java/datadog/telemetry/dependency/Dependency.java index fbf30ae7ffe..7d8b36bf489 100644 --- a/telemetry/src/main/java/datadog/telemetry/dependency/Dependency.java +++ b/telemetry/src/main/java/datadog/telemetry/dependency/Dependency.java @@ -46,11 +46,32 @@ public final class Dependency { public final String source; public final String hash; + /** + * Optional SCA reachability metadata. Each entry is a stringified JSON object conforming to the + * RFC reachability payload: {@code + * {"id":"GHSA-xxx","reached":[{"path":"...","symbol":"","line":1}]}}. Null for regular + * dependencies; non-null only when injected by {@code ScaReachabilityPeriodicAction}. Not + * serialized by the telemetry pipeline unless explicitly written by {@link + * datadog.telemetry.TelemetryRequestBody#writeDependency}. + */ + @Nullable public final List reachabilityMetadata; + public Dependency(String name, String version, String source, @Nullable String hash) { + this(name, version, source, hash, null); + } + + public Dependency( + String name, + String version, + String source, + @Nullable String hash, + @Nullable List reachabilityMetadata) { this.name = name; this.version = version; this.source = source; this.hash = hash; + this.reachabilityMetadata = + reachabilityMetadata != null ? Collections.unmodifiableList(reachabilityMetadata) : null; } @Override diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java new file mode 100644 index 00000000000..9327faf6154 --- /dev/null +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -0,0 +1,76 @@ +package datadog.telemetry.sca; + +import datadog.telemetry.TelemetryRunnable; +import datadog.telemetry.TelemetryService; +import datadog.telemetry.dependency.Dependency; +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Drains the {@link ScaReachabilityCollector} on each telemetry heartbeat and reports reachability + * hits as {@code app-dependencies-loaded} entries with {@code metadata} of type {@code + * "reachability"}. + * + *

Hits are grouped by {@code (artifact, version)} so that multiple CVEs affecting the same + * library version produce a single dependency entry with multiple metadata values, matching the RFC + * payload format. + * + *

Registered in {@link datadog.telemetry.TelemetrySystem} when {@code DD_APPSEC_SCA_ENABLED} is + * true. + */ +public final class ScaReachabilityPeriodicAction + implements TelemetryRunnable.TelemetryPeriodicAction { + + @Override + public void doIteration(TelemetryService telService) { + List hits = ScaReachabilityCollector.INSTANCE.drain(); + if (hits.isEmpty()) { + return; + } + + // Group hits by (artifact, version) — multiple CVEs for the same dep go in one entry. + Map> byArtifactVersion = new LinkedHashMap<>(); + for (ScaReachabilityHit hit : hits) { + String key = hit.artifact() + "@" + hit.version(); + byArtifactVersion.computeIfAbsent(key, k -> new ArrayList<>()).add(hit); + } + + for (Map.Entry> entry : byArtifactVersion.entrySet()) { + List group = entry.getValue(); + ScaReachabilityHit first = group.get(0); + + // Build one stringified JSON metadata value per CVE in this group. + List metadataValues = new ArrayList<>(group.size()); + for (ScaReachabilityHit hit : group) { + metadataValues.add(buildMetadataValue(hit)); + } + + Dependency dep = + new Dependency(first.artifact(), first.version(), null, null, metadataValues); + telService.addDependency(dep); + } + } + + /** + * Builds the stringified JSON value for one reachability hit, per RFC: + * + *

{@code {"id":"GHSA-xxx","reached":[{"path":"com.foo.Bar","symbol":"","line":1}]}}
+   * 
+ * + *

Class-level symbols always use {@code ""} as the symbol name (JVM standard name for + * the class initializer) and {@code 1} as a placeholder line number, because class-load detection + * does not capture a specific call site. + */ + static String buildMetadataValue(ScaReachabilityHit hit) { + // Manual JSON construction — values are safe (GHSA IDs and FQN class names contain no quotes). + return "{\"id\":\"" + + hit.vulnId() + + "\",\"reached\":[{\"path\":\"" + + hit.className() + + "\",\"symbol\":\"\",\"line\":1}]}"; + } +} diff --git a/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java b/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java new file mode 100644 index 00000000000..a617221c475 --- /dev/null +++ b/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java @@ -0,0 +1,94 @@ +package datadog.telemetry; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.telemetry.api.RequestType; +import datadog.telemetry.dependency.Dependency; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import okio.Buffer; +import org.junit.jupiter.api.Test; + +/** + * Verifies that {@link TelemetryRequestBody#writeDependency} correctly serializes the optional + * {@code metadata} array introduced for SCA Reachability. + */ +class TelemetryRequestBodyDependencyMetadataTest { + + @Test + void writeDependency_includesMetadataArrayWhenPresent() throws IOException { + String metadataValue = + "{\"id\":\"GHSA-645p-88qh-w398\"," + + "\"reached\":[{\"path\":\"com.fasterxml.jackson.databind.ObjectMapper\"," + + "\"symbol\":\"\",\"line\":1}]}"; + Dependency dep = + new Dependency( + "com.fasterxml.jackson.core:jackson-databind", + "2.8.5", + null, + null, + Collections.singletonList(metadataValue)); + + String json = serializeDependency(dep); + + assertTrue(json.contains("\"metadata\""), "metadata array must be present"); + assertTrue(json.contains("\"type\":\"reachability\""), "type field must be reachability"); + assertTrue(json.contains("\"value\":"), "value field must be present"); + assertTrue(json.contains("GHSA-645p-88qh-w398"), "GHSA ID must appear in value"); + } + + @Test + void writeDependency_includesAllMetadataEntriesForMultipleCves() throws IOException { + Dependency dep = + new Dependency( + "com.example:lib", + "1.0.0", + null, + null, + Arrays.asList( + "{\"id\":\"GHSA-aaa-1111-2222\",\"reached\":[]}", + "{\"id\":\"GHSA-bbb-3333-4444\",\"reached\":[]}")); + + String json = serializeDependency(dep); + + assertTrue(json.contains("GHSA-aaa-1111-2222"), "first CVE must be present"); + assertTrue(json.contains("GHSA-bbb-3333-4444"), "second CVE must be present"); + } + + @Test + void writeDependency_omitsMetadataFieldWhenNull() throws IOException { + Dependency dep = new Dependency("com.example:lib", "1.0.0", null, null); + + String json = serializeDependency(dep); + + assertFalse(json.contains("\"metadata\""), "metadata field must be absent when null"); + } + + @Test + void writeDependency_omitsMetadataFieldWhenEmptyList() throws IOException { + Dependency dep = + new Dependency("com.example:lib", "1.0.0", null, null, Collections.emptyList()); + + String json = serializeDependency(dep); + + assertFalse(json.contains("\"metadata\""), "metadata field must be absent when list is empty"); + } + + private static String serializeDependency(Dependency dep) throws IOException { + TelemetryRequestBody req = new TelemetryRequestBody(RequestType.APP_DEPENDENCIES_LOADED); + req.beginRequest(false); + req.beginDependencies(); + req.writeDependency(dep); + req.endDependencies(); + req.endRequest(); + + Buffer buf = new Buffer(); + req.writeTo(buf); + byte[] bytes = new byte[(int) buf.size()]; + buf.read(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java new file mode 100644 index 00000000000..53534e1c505 --- /dev/null +++ b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java @@ -0,0 +1,138 @@ +package datadog.telemetry.sca; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import datadog.telemetry.TelemetryService; +import datadog.telemetry.dependency.Dependency; +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class ScaReachabilityPeriodicActionTest { + + private TelemetryService telService; + private ScaReachabilityPeriodicAction action; + + @BeforeEach + void setUp() { + ScaReachabilityCollector.INSTANCE.drain(); // clear any leftovers + telService = mock(TelemetryService.class); + action = new ScaReachabilityPeriodicAction(); + } + + @Test + void doesNothingWhenNoHits() { + action.doIteration(telService); + verify(telService, never()).addDependency(org.mockito.Mockito.any()); + } + + @Test + void reportsSingleHit() { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit( + "GHSA-test-1234-5678", "com.example:lib", "1.0.0", "com.example.Foo")); + + action.doIteration(telService); + + ArgumentCaptor captor = forClass(Dependency.class); + verify(telService, times(1)).addDependency(captor.capture()); + + Dependency dep = captor.getValue(); + assertEquals("com.example:lib", dep.name); + assertEquals("1.0.0", dep.version); + assertNull(dep.hash); + assertNotNull(dep.reachabilityMetadata); + assertEquals(1, dep.reachabilityMetadata.size()); + } + + @Test + void metadataValueContainsClinit() { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-xxx", "com.example:lib", "1.0.0", "com.example.Foo")); + + action.doIteration(telService); + + ArgumentCaptor captor = forClass(Dependency.class); + verify(telService).addDependency(captor.capture()); + String value = captor.getValue().reachabilityMetadata.get(0); + + assertTrue(value.contains("\"id\":\"GHSA-xxx\"")); + assertTrue(value.contains("\"path\":\"com.example.Foo\"")); + assertTrue(value.contains("\"symbol\":\"\"")); + assertTrue(value.contains("\"line\":1")); + } + + @Test + void groupsTwoCvesForSameArtifactVersionIntoOneEntry() { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-cve-1", "com.example:lib", "1.0.0", "com.example.Foo")); + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-cve-2", "com.example:lib", "1.0.0", "com.example.Bar")); + + action.doIteration(telService); + + ArgumentCaptor captor = forClass(Dependency.class); + verify(telService, times(1)).addDependency(captor.capture()); + + Dependency dep = captor.getValue(); + assertEquals(2, dep.reachabilityMetadata.size()); + assertTrue(dep.reachabilityMetadata.stream().anyMatch(v -> v.contains("GHSA-cve-1"))); + assertTrue(dep.reachabilityMetadata.stream().anyMatch(v -> v.contains("GHSA-cve-2"))); + } + + @Test + void separateEntriesForDifferentArtifacts() { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-a", "com.example:lib-a", "1.0.0", "com.example.A")); + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-b", "com.example:lib-b", "2.0.0", "com.example.B")); + + action.doIteration(telService); + + verify(telService, times(2)).addDependency(org.mockito.Mockito.any()); + } + + @Test + void drainsClearsPreviousHits() { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit("GHSA-x", "com.example:lib", "1.0.0", "com.example.X")); + + action.doIteration(telService); + verify(telService, times(1)).addDependency(org.mockito.Mockito.any()); + + // Second iteration with no new hits — nothing to report + TelemetryService telService2 = mock(TelemetryService.class); + action.doIteration(telService2); + verify(telService2, never()).addDependency(org.mockito.Mockito.any()); + } + + @Test + void buildMetadataValue_format() { + ScaReachabilityHit hit = + new ScaReachabilityHit( + "GHSA-645p-88qh-w398", + "com.fasterxml.jackson.core:jackson-databind", + "2.8.5", + "com.fasterxml.jackson.databind.ObjectMapper"); + + String value = ScaReachabilityPeriodicAction.buildMetadataValue(hit); + + assertEquals( + "{\"id\":\"GHSA-645p-88qh-w398\"," + + "\"reached\":[{" + + "\"path\":\"com.fasterxml.jackson.databind.ObjectMapper\"," + + "\"symbol\":\"\"," + + "\"line\":1}]}", + value); + } +} From e607887e99d1d18aacb1a15c1ef7eeb5ad183aa4 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 10:23:34 +0200 Subject: [PATCH 02/35] Commit sca_cves.json as versioned resource; update generateScaCvesJson task The Gradle task now writes to src/main/resources/ and runs only when -PrefreshSca is passed or the file is absent, so CI builds never need network access to the private sca-reachability-database repo. --- dd-java-agent/appsec/build.gradle | 20 ++++++++++--------- .../appsec/src/main/resources/sca_cves.json | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 dd-java-agent/appsec/src/main/resources/sca_cves.json diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 4724f891a4a..4d1e4e03e7a 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -96,16 +96,20 @@ def githubFetchRaw = { String url, String token -> } tasks.register('generateScaCvesJson') { - description = 'Downloads GHSA enrichments from sca-reachability-database and generates sca_cves.json' + description = 'Downloads GHSA enrichments from sca-reachability-database and updates src/main/resources/sca_cves.json. ' + + 'Run with -PrefreshSca to force a refresh. ' + + 'sca_cves.json is committed to the repo so CI does not need network access to this private repo.' group = 'build' - def outputDir = layout.buildDirectory.dir('generated-resources/sca').get().asFile - def outputFile = new File(outputDir, 'sca_cves.json') + // Output lives in src/main/resources so it is versioned and picked up by processResources + // without any extra wiring. CI builds use the committed copy; only maintainers who need + // to update the database run this task (with -PrefreshSca or when the file is absent). + def outputFile = file('src/main/resources/sca_cves.json') outputs.file(outputFile) - // Re-runs only when output is missing or -PrefreshSca is passed. - // The database changes rarely; avoiding a network call on every local build is intentional. - outputs.upToDateWhen { outputFile.exists() && !project.hasProperty('refreshSca') } + onlyIf { + project.hasProperty('refreshSca') || !outputFile.exists() + } doLast { def token = System.getenv('GITHUB_TOKEN') @@ -123,15 +127,13 @@ tasks.register('generateScaCvesJson') { entries.addAll(datadog.gradle.sca.GhsaEnrichmentParser.INSTANCE.parse(ghsaId, rawContent)) } - outputDir.mkdirs() outputFile.text = JsonOutput.toJson([version: 1, entries: entries]) logger.lifecycle("sca_cves.json: ${entries.size()} entries from ${ghsaFiles.size()} GHSA files") + logger.lifecycle("Remember to commit src/main/resources/sca_cves.json after updating the database.") } } tasks.named("processResources", ProcessResources) { - dependsOn('generateScaCvesJson') - from(layout.buildDirectory.dir('generated-resources/sca')) doLast { fileTree(dir: outputs.files.asPath, includes: ['**/*.json']).each { it.text = JsonOutput.toJson(new JsonSlurper().parse(it)) diff --git a/dd-java-agent/appsec/src/main/resources/sca_cves.json b/dd-java-agent/appsec/src/main/resources/sca_cves.json new file mode 100644 index 00000000000..0cccdbc111a --- /dev/null +++ b/dd-java-agent/appsec/src/main/resources/sca_cves.json @@ -0,0 +1 @@ +{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]}]} \ No newline at end of file From fb9d0117ad765751c83587e90f12079658333775 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 10:40:48 +0200 Subject: [PATCH 03/35] Fix Path B classpath scan for Java 9+: fall back to java.class.path On Java 9+, the system classloader (jdk.internal.loader.ClassLoaders$AppClassLoader) no longer extends URLClassLoader, so the URLClassLoader chain walk misses all main classpath entries. Add a fallback that reads java.class.path to cover this case, deduplicating with a HashSet to avoid scanning the same JAR twice. --- .../sca/ScaReachabilityTransformer.java | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 1f16cafca68..16f798e78b5 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -4,6 +4,7 @@ import datadog.telemetry.dependency.DependencyResolver; import datadog.trace.api.telemetry.ScaReachabilityCollector; import datadog.trace.api.telemetry.ScaReachabilityHit; +import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.net.URI; @@ -12,6 +13,7 @@ import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -171,19 +173,53 @@ private void processPathB(String internalClassName, List entries) { } private String findArtifactVersionInClasspath(String artifactName) { + Set scanned = new HashSet<>(); + + // Walk URLClassLoader chain (covers Java 8 system classloader and custom classloaders on 9+) ClassLoader cl = Thread.currentThread().getContextClassLoader(); while (cl != null) { if (cl instanceof URLClassLoader) { for (URL url : ((URLClassLoader) cl).getURLs()) { - for (Dependency dep : resolveDependencies(url)) { - if (artifactName.equals(dep.name) && dep.version != null) { - return dep.version; + if (scanned.add(url)) { + String version = findArtifactInUrl(artifactName, url); + if (version != null) { + return version; } } } } cl = cl.getParent(); } + + // Fallback for Java 9+: system classloader (jdk.internal.loader.ClassLoaders$AppClassLoader) + // no longer extends URLClassLoader, so the loop above misses the main classpath. The + // java.class.path system property always contains the classpath entries in this case. + String classpath = System.getProperty("java.class.path", ""); + for (String entry : classpath.split(File.pathSeparator)) { + if (entry.isEmpty()) { + continue; + } + try { + URL url = new File(entry).toURI().toURL(); + if (scanned.add(url)) { + String version = findArtifactInUrl(artifactName, url); + if (version != null) { + return version; + } + } + } catch (Exception e) { + log.debug("SCA Reachability: could not scan classpath entry {}", entry, e); + } + } + return null; + } + + private String findArtifactInUrl(String artifactName, URL url) { + for (Dependency dep : resolveDependencies(url)) { + if (artifactName.equals(dep.name) && dep.version != null) { + return dep.version; + } + } return null; } From 62b290d2464bf39f17d2c9038b3016bfcf2e18bc Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 10:43:25 +0200 Subject: [PATCH 04/35] Add Java 9+ test for Path B classpath fallback; make method package-private Test verifies: (1) system classloader is not URLClassLoader on Java 9+, and (2) findArtifactVersionInClasspath finds artifacts via java.class.path fallback. Applies to Java 9 and all subsequent JDKs (permanent JDK design change). --- .../sca/ScaReachabilityTransformer.java | 3 +- .../ScaReachabilityTransformerJava9Test.java | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 16f798e78b5..5cdb786ac67 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -172,7 +172,8 @@ private void processPathB(String internalClassName, List entries) { } } - private String findArtifactVersionInClasspath(String artifactName) { + // package-private for testing + String findArtifactVersionInClasspath(String artifactName) { Set scanned = new HashSet<>(); // Walk URLClassLoader chain (covers Java 8 system classloader and custom classloaders on 9+) diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java new file mode 100644 index 00000000000..a981e74321d --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java @@ -0,0 +1,72 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.StringReader; +import java.net.URLClassLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +/** + * Verifies that Path B classpath scanning works on Java 9+, where the system classloader + * (jdk.internal.loader.ClassLoaders$AppClassLoader) no longer extends {@link URLClassLoader}. + * + *

The change was introduced in Java 9 (Project Jigsaw) and is permanent in all subsequent JDK + * versions (11, 17, 21, …). Without the {@code java.class.path} fallback, Path B would silently + * fail to find vulnerable artifacts on any modern JDK. + */ +class ScaReachabilityTransformerJava9Test { + + private static final String JACKSON_JSON = + "{\"version\":1,\"entries\":[{" + + "\"vuln_id\":\"GHSA-test-jackson\"," + + "\"artifact\":\"com.fasterxml.jackson.core:jackson-databind\"," + + "\"version_ranges\":[\"< 999.0.0\"]," + + "\"symbols\":[{\"class\":\"com/fasterxml/jackson/databind/ObjectMapper\",\"method\":null}]" + + "}]}"; + + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + void systemClassLoaderIsNotUrlClassLoaderOnJava9Plus() { + // This is the root cause of the bug: the URLClassLoader chain walk misses the system + // classloader on Java 9+. Verify our assumption so the test has a clear failure message + // if the JDK ever reverts this (extremely unlikely). + assertFalse( + ClassLoader.getSystemClassLoader() instanceof URLClassLoader, + "On Java 9+, the system classloader must not be a URLClassLoader — " + + "this is the invariant that makes the java.class.path fallback necessary"); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + void findArtifactVersionInClasspath_findsArtifactViaJavaClassPathOnJava9Plus() throws Exception { + // jackson-databind is on the test classpath (testImplementation dependency). + // On Java 9+, it would NOT be found by the URLClassLoader chain because the system + // classloader is not a URLClassLoader. The java.class.path fallback must find it. + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db); + + String version = + transformer.findArtifactVersionInClasspath("com.fasterxml.jackson.core:jackson-databind"); + + assertNotNull( + version, + "jackson-databind must be found via java.class.path fallback on Java 9+. " + + "java.class.path=" + + System.getProperty("java.class.path", "")); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + void findArtifactVersionInClasspath_returnsNullForUnknownArtifact() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db); + + String version = transformer.findArtifactVersionInClasspath("com.example:nonexistent-artifact"); + + assertNull(version, "Unknown artifacts must return null"); + } +} From 93d58f20f10f0935c1fe71d9eb94a05e33e2cc6b Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 10:58:48 +0200 Subject: [PATCH 05/35] Implement method-level symbol detection with ASM bytecode injection When sca_cves.json contains symbols with method != null, the transformer injects a static callback at method entry using ASM. The callback fires the first time the method is called and reports via ScaReachabilityCallback (bootstrap classloader, accessible from any application class). Key changes: - ScaReachabilityCallback in agent-bootstrap: bootstrap-visible callback with runtime dedup (vulnId|artifact|methodName) and handler registration - ScaReachabilityTransformer: injectMethodCallbacks() uses ByteBuddy ASM to inject INVOKESTATIC at first line number of each target method; processClass() routes class-level vs method-level symbols separately - ScaReachabilityHit: adds symbolName + line fields; existing constructor defaults to /1 for class-level hits (backward compatible) - ScaReachabilityPeriodicAction: buildMetadataValue() now uses hit.symbolName() and hit.line() instead of hardcoded values - 6 tests: ASM injection, callback fires on method call only, dedup, multiple methods, safe method not reported, class-level unaffected --- .../appsec/sca/ScaReachabilityCallback.java | 69 ++++++ dd-java-agent/appsec/build.gradle | 1 + .../appsec/sca/ScaReachabilitySystem.java | 9 + .../sca/ScaReachabilityTransformer.java | 232 ++++++++++++++++- .../sca/ScaReachabilityMethodLevelTest.java | 234 ++++++++++++++++++ .../api/telemetry/ScaReachabilityHit.java | 28 +++ .../sca/ScaReachabilityPeriodicAction.java | 15 +- 7 files changed, 573 insertions(+), 15 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java new file mode 100644 index 00000000000..4486ee188d0 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java @@ -0,0 +1,69 @@ +package datadog.trace.bootstrap.appsec.sca; + +/** + * Bootstrap-classloader callback for SCA Reachability method-level detection. + * + *

Bytecode injected into application classes by {@code ScaReachabilityTransformer} calls {@link + * #onMethodHit} statically. Because this class lives in the bootstrap classloader, it is visible + * from any application class regardless of classloader hierarchy. + * + *

The actual handler is registered at agent startup by {@code ScaReachabilitySystem.start()}. + */ +public final class ScaReachabilityCallback { + + /** Receives method-level reachability hits from instrumented application code. */ + public interface Handler { + void onMethodHit( + String vulnId, + String artifact, + String version, + String dotClassName, + String methodName, + int line); + } + + private static volatile Handler handler; + + /** Runtime dedup: "vulnId|artifact|methodName" tuples already reported. */ + private static final java.util.Set reported = + java.util.Collections.newSetFromMap( + new java.util.concurrent.ConcurrentHashMap()); + + /** + * Called by {@code ScaReachabilitySystem} to wire up the real reporting implementation. Passing + * {@code null} clears both the handler and the dedup set (used in tests). + */ + public static void register(Handler h) { + handler = h; + if (h == null) { + reported.clear(); + } + } + + /** + * Called from bytecode injected into the entry point of a vulnerable method. Deduplicates at + * runtime so the handler is called at most once per (vulnId, artifact, methodName) triple, + * regardless of how many times the method is invoked. + * + *

Arguments are constants baked into the instrumented class at transform time — they never + * originate from user input and are safe to use as-is in the telemetry payload. + */ + public static void onMethodHit( + String vulnId, + String artifact, + String version, + String dotClassName, + String methodName, + int line) { + Handler h = handler; + if (h == null) { + return; + } + String key = vulnId + "|" + artifact + "|" + methodName; + if (reported.add(key)) { + h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); + } + } + + private ScaReachabilityCallback() {} +} diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 4d1e4e03e7a..3f9f6e249ce 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation libs.moshi compileOnly project(':dd-java-agent:agent-bootstrap') + compileOnly libs.bytebuddy // for net.bytebuddy.jar.asm.* used by ScaReachabilityTransformer testImplementation project(':dd-java-agent:agent-bootstrap') testImplementation libs.bytebuddy testImplementation project(':remote-config:remote-config-core') diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index 3babc5346b7..f665899f325 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -1,5 +1,8 @@ package com.datadog.appsec.sca; +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; import java.lang.instrument.Instrumentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +40,12 @@ public static void start(Instrumentation instrumentation) { } log.info("SCA Reachability: loaded {} vulnerable class symbols", database.size()); + // Register the method-level callback so injected bytecode can report hits back to telemetry. + ScaReachabilityCallback.register( + (vulnId, artifact, version, dotClassName, methodName, line) -> + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(database); // canRetransform=true is required so that future method-level symbols (when added to the diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 5cdb786ac67..bb9fffbe270 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -4,6 +4,7 @@ import datadog.telemetry.dependency.DependencyResolver; import datadog.trace.api.telemetry.ScaReachabilityCollector; import datadog.trace.api.telemetry.ScaReachabilityHit; +import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; @@ -12,11 +13,21 @@ import java.net.URLClassLoader; import java.security.CodeSource; import java.security.ProtectionDomain; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.utility.OpenedClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,12 +104,63 @@ public byte[] transform( return null; } - processPathA(className, location, entries); + return processClass(className, location, entries, classfileBuffer); } catch (Throwable t) { // Never propagate from transform() — it would break the class being loaded. log.debug("SCA Reachability: error processing class {}", className, t); } - return null; // observation only — never modify bytecode for class-level symbols + return null; + } + + /** + * Handles both class-level and method-level symbols for a single class load event. + * + *

    + *
  • Class-level ({@code symbol.method() == null}): reports a hit immediately via {@link + * ScaReachabilityCollector} with symbol {@code ""}. + *
  • Method-level ({@code symbol.method() != null}): injects a static callback into the method + * bytecode via ASM. The callback is invoked the first time the method is called and reports + * via {@link ScaReachabilityCallback}. Returns modified bytecode; {@code null} if only + * class-level symbols were present. + *
+ */ + private byte[] processClass( + String className, URL jarUrl, List entries, byte[] classfileBuffer) { + List deps = resolveDependencies(jarUrl); + + // Collect method-level callbacks to inject, keyed by method name + Map> methodCallbacks = new HashMap<>(); + + for (ScaEntry entry : entries) { + for (Dependency dep : deps) { + if (!entry.artifact().equals(dep.name) || !entry.isVersionVulnerable(dep.version)) { + continue; + } + for (ScaSymbol symbol : entry.symbols()) { + if (!symbol.className().equals(className)) { + continue; + } + if (symbol.isClassLevel()) { + reportHit(entry, dep.version, className, "", 1); + } else { + methodCallbacks + .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) + .add( + new MethodCallbackSpec( + entry.vulnId(), + entry.artifact(), + dep.version, + className.replace('/', '.'), + symbol.method())); + } + } + } + } + + if (methodCallbacks.isEmpty()) { + return null; + } + return injectMethodCallbacks(classfileBuffer, methodCallbacks); } // --------------------------------------------------------------------------- @@ -156,7 +218,14 @@ private void processPathA(String internalClassName, URL jarUrl, List e for (ScaEntry entry : entries) { for (Dependency dep : deps) { if (entry.artifact().equals(dep.name) && entry.isVersionVulnerable(dep.version)) { - reportHit(entry, dep.version, internalClassName); + // Only class-level symbols are reported at class load time. + // Method-level symbols are handled by processClass() via ASM injection. + for (ScaSymbol symbol : entry.symbols()) { + if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { + reportHit(entry, dep.version, internalClassName, "", 1); + break; // one hit per entry is sufficient + } + } } } } @@ -167,11 +236,149 @@ private void processPathB(String internalClassName, List entries) { for (ScaEntry entry : entries) { String version = findArtifactVersionInClasspath(entry.artifact()); if (version != null && entry.isVersionVulnerable(version)) { - reportHit(entry, version, internalClassName); + for (ScaSymbol symbol : entry.symbols()) { + if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { + reportHit(entry, version, internalClassName, "", 1); + break; + } + } } } } + // --------------------------------------------------------------------------- + // Method-level bytecode injection (ASM) + // --------------------------------------------------------------------------- + + private static final String CALLBACK_OWNER = + "datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback"; + private static final String CALLBACK_METHOD = "onMethodHit"; + private static final String CALLBACK_DESC = + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"; + + // package-private for testing + byte[] injectMethodCallbacks( + byte[] classfileBuffer, Map> callbacksPerMethod) { + ClassReader cr = new ClassReader(classfileBuffer); + ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); + cr.accept(new MethodCallbackInjector(cw, callbacksPerMethod), ClassReader.EXPAND_FRAMES); + return cw.toByteArray(); + } + + private class MethodCallbackInjector extends ClassVisitor { + private final Map> callbacksPerMethod; + + MethodCallbackInjector( + ClassVisitor cv, Map> callbacksPerMethod) { + super(OpenedClassReader.ASM_API, cv); + this.callbacksPerMethod = callbacksPerMethod; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + List specs = callbacksPerMethod.get(name); + if (specs == null || specs.isEmpty()) { + return mv; + } + return new MethodEntryInjector(mv, specs); + } + } + + private class MethodEntryInjector extends MethodVisitor { + private final List specs; + private boolean injected = false; + + MethodEntryInjector(MethodVisitor mv, List specs) { + super(OpenedClassReader.ASM_API, mv); + this.specs = specs; + } + + @Override + public void visitLineNumber(int line, Label start) { + if (!injected) { + injected = true; + injectCallbacks(line); + } + super.visitLineNumber(line, start); + } + + @Override + public void visitCode() { + super.visitCode(); + // Fallback for methods without debug info (no visitLineNumber calls). + // We override the first instruction visitor to inject if not yet done. + } + + @Override + public void visitInsn(int opcode) { + ensureInjected(); + super.visitInsn(opcode); + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + ensureInjected(); + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String descriptor, boolean isInterface) { + ensureInjected(); + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + ensureInjected(); + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + private void ensureInjected() { + if (!injected) { + injected = true; + injectCallbacks(1); // no debug info — use line 1 as placeholder + } + } + + private void injectCallbacks(int line) { + for (MethodCallbackSpec spec : specs) { + String dedupKey = spec.vulnId + "|" + spec.artifact + "|" + spec.methodName; + if (!reportedHits.add(dedupKey)) { + continue; // already reported this method hit + } + mv.visitLdcInsn(spec.vulnId); + mv.visitLdcInsn(spec.artifact); + mv.visitLdcInsn(spec.version); + mv.visitLdcInsn(spec.dotClassName); + mv.visitLdcInsn(spec.methodName); + mv.visitIntInsn(Opcodes.SIPUSH, line); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, CALLBACK_OWNER, CALLBACK_METHOD, CALLBACK_DESC, false); + } + } + } + + /** Immutable spec for a single method-level callback to inject. */ + static final class MethodCallbackSpec { + final String vulnId; + final String artifact; + final String version; + final String dotClassName; + final String methodName; + + MethodCallbackSpec( + String vulnId, String artifact, String version, String dotClassName, String methodName) { + this.vulnId = vulnId; + this.artifact = artifact; + this.version = version; + this.dotClassName = dotClassName; + this.methodName = methodName; + } + } + // package-private for testing String findArtifactVersionInClasspath(String artifactName) { Set scanned = new HashSet<>(); @@ -224,20 +431,25 @@ private String findArtifactInUrl(String artifactName, URL url) { return null; } - private void reportHit(ScaEntry entry, String version, String internalClassName) { - String dedupKey = entry.vulnId() + "|" + entry.artifact(); + private void reportHit( + ScaEntry entry, String version, String internalClassName, String symbolName, int line) { + // Dedup key includes symbol name so class-level and method-level hits for the same + // vulnerability are tracked independently. + String dedupKey = entry.vulnId() + "|" + entry.artifact() + "|" + symbolName; if (!reportedHits.add(dedupKey)) { - return; // already reported this (vulnId, artifact) pair — RFC: single occurrence sufficient + return; // already reported this (vulnId, artifact, symbol) tuple } String dotClassName = internalClassName.replace('/', '.'); log.debug( - "SCA Reachability: {} reached in {}:{} via class {}", + "SCA Reachability: {} reached in {}:{} via {}#{}", entry.vulnId(), entry.artifact(), version, - dotClassName); + dotClassName, + symbolName); ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit(entry.vulnId(), entry.artifact(), version, dotClassName)); + new ScaReachabilityHit( + entry.vulnId(), entry.artifact(), version, dotClassName, symbolName, line)); } private List resolveDependencies(URL url) { diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java new file mode 100644 index 00000000000..31643418b72 --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -0,0 +1,234 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityHit; +import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for method-level symbol detection ({@code method != null} in sca_cves.json). + * + *

Strategy: test {@link ScaReachabilityTransformer#injectMethodCallbacks} directly to verify the + * ASM injection mechanism, decoupled from JAR version resolution. The version resolution path is + * covered by {@link ScaReachabilityTransformerTest}. + */ +class ScaReachabilityMethodLevelTest { + + /** Target class that the transformer will instrument in tests. */ + public static class TargetClass { + public String vulnerableMethod() { + return "executed"; + } + + public String safeMethod() { + return "safe"; + } + } + + private ScaCveDatabase db; + private ScaReachabilityTransformer transformer; + + @BeforeEach + void setUp() throws Exception { + ScaReachabilityCollector.INSTANCE.drain(); + // Register the same handler as ScaReachabilitySystem.start() does in production + ScaReachabilityCallback.register( + (vulnId, artifact, version, dotClassName, methodName, line) -> + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); + db = ScaCveDatabase.parse(new StringReader("{\"version\":1,\"entries\":[]}")); + transformer = new ScaReachabilityTransformer(db); + } + + @AfterEach + void tearDown() { + ScaReachabilityCollector.INSTANCE.drain(); + ScaReachabilityCallback.register(null); + } + + // --------------------------------------------------------------------------- + // ASM injection: injectMethodCallbacks() + // --------------------------------------------------------------------------- + + @Test + void injectMethodCallbacks_returnsModifiedBytecode() throws Exception { + byte[] original = bytecodeOf(TargetClass.class); + Map> callbacks = + singleCallback("vulnerableMethod"); + + byte[] modified = transformer.injectMethodCallbacks(original, callbacks); + + assertNotNull(modified, "injectMethodCallbacks must return non-null modified bytecode"); + } + + @Test + void injectMethodCallbacks_callbackFiredOnMethodCall() throws Exception { + byte[] original = bytecodeOf(TargetClass.class); + Map> callbacks = + singleCallback("vulnerableMethod"); + + byte[] modified = transformer.injectMethodCallbacks(original, callbacks); + Class cls = loadModified(modified); + Object instance = cls.getDeclaredConstructor().newInstance(); + cls.getMethod("vulnerableMethod").invoke(instance); + + List hits = ScaReachabilityCollector.INSTANCE.drain(); + assertEquals(1, hits.size()); + ScaReachabilityHit hit = hits.get(0); + assertEquals("GHSA-method-0001", hit.vulnId()); + assertEquals("com.example:test-lib", hit.artifact()); + assertEquals("1.2.3", hit.version()); + assertEquals("vulnerableMethod", hit.symbolName(), "symbolName must be the actual method name"); + assertTrue(hit.line() >= 1, "line must be >= 1"); + } + + @Test + void injectMethodCallbacks_noCallbackForSafeMethod() throws Exception { + byte[] original = bytecodeOf(TargetClass.class); + Map> callbacks = + singleCallback("vulnerableMethod"); + + byte[] modified = transformer.injectMethodCallbacks(original, callbacks); + Class cls = loadModified(modified); + Object instance = cls.getDeclaredConstructor().newInstance(); + cls.getMethod("safeMethod").invoke(instance); // call only the safe method + + assertTrue( + ScaReachabilityCollector.INSTANCE.drain().isEmpty(), + "No hit expected when only non-instrumented methods are called"); + } + + @Test + void injectMethodCallbacks_deduplicatesOnMultipleCalls() throws Exception { + byte[] original = bytecodeOf(TargetClass.class); + Map> callbacks = + singleCallback("vulnerableMethod"); + + byte[] modified = transformer.injectMethodCallbacks(original, callbacks); + Class cls = loadModified(modified); + Object instance = cls.getDeclaredConstructor().newInstance(); + Method m = cls.getMethod("vulnerableMethod"); + + m.invoke(instance); + m.invoke(instance); + m.invoke(instance); + + assertEquals( + 1, + ScaReachabilityCollector.INSTANCE.drain().size(), + "Hit must be reported only once regardless of how many times the method is called"); + } + + @Test + void injectMethodCallbacks_injectsMultipleMethodsIndependently() throws Exception { + byte[] original = bytecodeOf(TargetClass.class); + Map> callbacks = new HashMap<>(); + callbacks.put( + "vulnerableMethod", + Collections.singletonList( + spec("GHSA-m1", "com.example:lib", "1.0.0", "T", "vulnerableMethod"))); + callbacks.put( + "safeMethod", + Collections.singletonList(spec("GHSA-m2", "com.example:lib", "1.0.0", "T", "safeMethod"))); + + byte[] modified = transformer.injectMethodCallbacks(original, callbacks); + Class cls = loadModified(modified); + Object instance = cls.getDeclaredConstructor().newInstance(); + + cls.getMethod("vulnerableMethod").invoke(instance); + cls.getMethod("safeMethod").invoke(instance); + + List hits = ScaReachabilityCollector.INSTANCE.drain(); + assertEquals(2, hits.size(), "Each instrumented method produces its own hit"); + assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("vulnerableMethod"))); + assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("safeMethod"))); + } + + // --------------------------------------------------------------------------- + // transform(): class-level symbols still report via Path A + // --------------------------------------------------------------------------- + + @Test + void transformReturnsNullForClassLevelSymbol() throws Exception { + String json = + "{\"version\":1,\"entries\":[{" + + "\"vuln_id\":\"GHSA-cls\",\"artifact\":\"com.example:lib\"," + + "\"version_ranges\":[\"< 999.0.0\"]," + + "\"symbols\":[{\"class\":\"" + + TargetClass.class.getName().replace('.', '/') + + "\",\"method\":null}]" + + "}]}"; + ScaCveDatabase classDb = ScaCveDatabase.parse(new StringReader(json)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(classDb); + + byte[] result = + t.transform( + null, + TargetClass.class.getName().replace('.', '/'), + null, + TargetClass.class.getProtectionDomain(), + bytecodeOf(TargetClass.class)); + + assertNull(result, "transform() must return null (observation only) for class-level symbols"); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static Map> singleCallback( + String methodName) { + Map> m = new HashMap<>(); + m.put( + methodName, + Collections.singletonList( + spec( + "GHSA-method-0001", + "com.example:test-lib", + "1.2.3", + TargetClass.class.getName(), + methodName))); + return m; + } + + private static ScaReachabilityTransformer.MethodCallbackSpec spec( + String vulnId, String artifact, String version, String dotClass, String method) { + return new ScaReachabilityTransformer.MethodCallbackSpec( + vulnId, artifact, version, dotClass, method); + } + + private static byte[] bytecodeOf(Class clazz) throws Exception { + String path = clazz.getName().replace('.', '/') + ".class"; + try (InputStream is = clazz.getClassLoader().getResourceAsStream(path)) { + assertNotNull(is, "Cannot load bytecode for " + clazz.getName()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n); + return buf.toByteArray(); + } + } + + private static Class loadModified(byte[] bytecode) { + return new ClassLoader(ScaReachabilityMethodLevelTest.class.getClassLoader()) { + Class define() { + return defineClass(null, bytecode, 0, bytecode.length); + } + }.define(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java index 72c5b7f21e7..b21f95dc380 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java @@ -11,12 +11,27 @@ public final class ScaReachabilityHit { private final String artifact; private final String version; private final String className; // dot-notation FQN, e.g. "com.foo.Bar" + private final String symbolName; // "" for class-level; method name for method-level + private final int line; // 1 as placeholder for class-level; actual first line for method-level + /** Convenience constructor for class-level hits (symbolName = {@code ""}, line = 1). */ public ScaReachabilityHit(String vulnId, String artifact, String version, String className) { + this(vulnId, artifact, version, className, "", 1); + } + + public ScaReachabilityHit( + String vulnId, + String artifact, + String version, + String className, + String symbolName, + int line) { this.vulnId = vulnId; this.artifact = artifact; this.version = version; this.className = className; + this.symbolName = symbolName; + this.line = line; } /** GHSA identifier, e.g. {@code "GHSA-645p-88qh-w398"}. */ @@ -37,4 +52,17 @@ public String version() { public String className() { return className; } + + /** + * JVM symbol name: {@code ""} for class-level hits, or the method name (e.g. {@code + * "readValue"}) for method-level hits. + */ + public String symbolName() { + return symbolName; + } + + /** First source line of the detected symbol. {@code 1} for class-level (placeholder). */ + public int line() { + return line; + } } diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java index 9327faf6154..05c6882e637 100644 --- a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -61,16 +61,21 @@ public void doIteration(TelemetryService telService) { *

{@code {"id":"GHSA-xxx","reached":[{"path":"com.foo.Bar","symbol":"","line":1}]}}
    * 
* - *

Class-level symbols always use {@code ""} as the symbol name (JVM standard name for - * the class initializer) and {@code 1} as a placeholder line number, because class-load detection - * does not capture a specific call site. + *

For class-level hits, {@code symbol} is {@code ""} and {@code line} is {@code 1} + * (placeholder). For method-level hits, {@code symbol} is the actual method name and {@code line} + * is the first line of the method definition. */ static String buildMetadataValue(ScaReachabilityHit hit) { - // Manual JSON construction — values are safe (GHSA IDs and FQN class names contain no quotes). + // Manual JSON construction — values are safe (GHSA IDs, FQN class names, and method names + // contain no quotes or characters that require JSON escaping). return "{\"id\":\"" + hit.vulnId() + "\",\"reached\":[{\"path\":\"" + hit.className() - + "\",\"symbol\":\"\",\"line\":1}]}"; + + "\",\"symbol\":\"" + + hit.symbolName() + + "\",\"line\":" + + hit.line() + + "}]}"; } } From a5ccd800213d1e55debb99b42d2b0d20a2b33157 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 11:14:03 +0200 Subject: [PATCH 06/35] Retransform classes for method-level detection: already-loaded and version-unresolved Two cases required deferred retransformation: 1. Classes already loaded at startup (before transformer registered): the bytecode callback cannot be injected without retransformClasses() 2. Classes where DependencyResolver returned empty deps at load time (version not yet resolvable): empty results are now not cached to allow retries ScaReachabilityTransformer now stores Instrumentation and exposes performPendingRetransforms() called on each telemetry heartbeat via a Runnable callback in ScaReachabilityCollector.periodicWorkCallback. Classes are queued via: - pendingRetransform (Class queue) from checkAlreadyLoadedClasses - pendingRetransformNames (String set) from processClass on empty deps --- .../appsec/sca/ScaReachabilitySystem.java | 8 +- .../sca/ScaReachabilityTransformer.java | 116 +++++++++++++++++- .../sca/ScaReachabilityMethodLevelTest.java | 4 +- .../ScaReachabilityTransformerJava9Test.java | 4 +- .../sca/ScaReachabilityTransformerTest.java | 2 +- .../telemetry/ScaReachabilityCollector.java | 8 ++ .../sca/ScaReachabilityPeriodicAction.java | 7 ++ 7 files changed, 138 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index f665899f325..c10804eb988 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -46,7 +46,8 @@ public static void start(Instrumentation instrumentation) { ScaReachabilityCollector.INSTANCE.addHit( new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); - ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(database); + ScaReachabilityTransformer transformer = + new ScaReachabilityTransformer(database, instrumentation); // canRetransform=true is required so that future method-level symbols (when added to the // database) can trigger retransformation of already-loaded classes via retransformClasses(). @@ -56,5 +57,10 @@ public static void start(Instrumentation instrumentation) { transformer.checkAlreadyLoadedClasses(instrumentation); log.debug("SCA Reachability: startup scan complete"); + + // Register the periodic retransform callback so the telemetry heartbeat can retry + // method-level instrumentation for classes that could not be processed at load time. + ScaReachabilityCollector.INSTANCE.periodicWorkCallback = + transformer::performPendingRetransforms; } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index bb9fffbe270..0c581d3b4ab 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.ClassWriter; @@ -56,15 +57,29 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); private final ScaCveDatabase database; + private final Instrumentation instrumentation; - /** Cache: JAR URL → resolved dependencies (empty list = JAR has no pom.properties). */ + /** Cache: JAR URL → resolved dependencies. Only non-empty results are cached to allow retries. */ private final ConcurrentHashMap> jarCache = new ConcurrentHashMap<>(); - /** Deduplication set: "vulnId|artifact" pairs already reported. */ + /** Deduplication set: "vulnId|artifact|symbol" tuples already reported. */ private final Set reportedHits = ConcurrentHashMap.newKeySet(); - public ScaReachabilityTransformer(ScaCveDatabase database) { + /** + * Classes whose bytecode needs (re)transformation for method-level symbol injection: + * + *

    + *
  • Classes already loaded at startup before this transformer was registered. + *
  • Classes where JAR version resolution returned no results at load time and needs a retry. + *
+ * + * Drained and processed by {@link #performPendingRetransforms()} on each telemetry heartbeat. + */ + private final ConcurrentLinkedQueue> pendingRetransform = new ConcurrentLinkedQueue<>(); + + public ScaReachabilityTransformer(ScaCveDatabase database, Instrumentation instrumentation) { this.database = database; + this.instrumentation = instrumentation; } // --------------------------------------------------------------------------- @@ -130,8 +145,15 @@ private byte[] processClass( // Collect method-level callbacks to inject, keyed by method name Map> methodCallbacks = new HashMap<>(); + boolean hasUnresolvedMethodLevelSymbols = false; for (ScaEntry entry : entries) { + // Check if this entry has method-level symbols for this class before version resolution, + // so we can schedule a retry if deps are unavailable now. + boolean entryHasMethodLevelSymbol = + entry.symbols().stream() + .anyMatch(s -> s.className().equals(className) && !s.isClassLevel()); + for (Dependency dep : deps) { if (!entry.artifact().equals(dep.name) || !entry.isVersionVulnerable(dep.version)) { continue; @@ -155,6 +177,20 @@ private byte[] processClass( } } } + + // If deps were empty (version not resolved yet) and this entry has method-level symbols, + // flag for retry. The class will be retransformed on the next periodic action heartbeat. + if (deps.isEmpty() && entryHasMethodLevelSymbol) { + hasUnresolvedMethodLevelSymbols = true; + } + } + + if (hasUnresolvedMethodLevelSymbols) { + // Schedule retransformation: when the periodic action fires, it will call + // performPendingRetransforms() which will retry version resolution and inject bytecode. + // We enqueue via classBeingRedefined is null here — we'll locate the Class object later + // in performPendingRetransforms() via instrumentation.getAllLoadedClasses(). + scheduleRetransformByName(className); } if (methodCallbacks.isEmpty()) { @@ -163,6 +199,13 @@ private byte[] processClass( return injectMethodCallbacks(classfileBuffer, methodCallbacks); } + /** Stores a class name (internal format) for deferred retransformation. */ + private final Set pendingRetransformNames = ConcurrentHashMap.newKeySet(); + + private void scheduleRetransformByName(String internalClassName) { + pendingRetransformNames.add(internalClassName); + } + // --------------------------------------------------------------------------- // Startup scan for already-loaded classes // --------------------------------------------------------------------------- @@ -201,6 +244,16 @@ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { } else { processPathA(internalName, location, entries); // 3rd-party class } + // If any entry for this class has method-level symbols, the class needs retransformation + // so the bytecode callback can be injected. We can't modify bytecode here (we're just + // scanning) — retransformation is deferred to performPendingRetransforms(). + boolean needsMethodLevelInstrumentation = + entries.stream() + .flatMap(e -> e.symbols().stream()) + .anyMatch(s -> s.className().equals(internalName) && !s.isClassLevel()); + if (needsMethodLevelInstrumentation) { + pendingRetransform.add(clazz); + } } catch (Exception e) { // Never abort the scan — a failure on one class must not skip the remaining ones. log.debug("SCA Reachability: error scanning already-loaded class {}", internalName, e); @@ -208,6 +261,54 @@ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { } } + /** + * Retransforms classes that could not be instrumented for method-level detection earlier: + * + *
    + *
  1. Classes already loaded before the transformer was registered (populated in {@link + * #checkAlreadyLoadedClasses}). + *
  2. Classes whose JAR version could not be resolved at load time (populated in {@link + * #processClass} when {@code DependencyResolver} returns an empty list — the version may be + * available by the time this periodic callback fires). + *
+ * + *

Called by {@code ScaReachabilityPeriodicAction} on each telemetry heartbeat via the {@code + * periodicWorkCallback} registered in {@link ScaReachabilityCollector}. + */ + public void performPendingRetransforms() { + // Drain the direct Class queue (from checkAlreadyLoadedClasses) + List> toRetransform = new ArrayList<>(); + Class clazz; + while ((clazz = pendingRetransform.poll()) != null) { + toRetransform.add(clazz); + } + + // Resolve any classes queued by name (from processClass timing failures) + if (!pendingRetransformNames.isEmpty()) { + for (Class loaded : instrumentation.getAllLoadedClasses()) { + String name = loaded.getName().replace('.', '/'); + if (pendingRetransformNames.remove(name)) { + toRetransform.add(loaded); + } + } + } + + if (toRetransform.isEmpty()) { + return; + } + + try { + instrumentation.retransformClasses(toRetransform.toArray(new Class[0])); + log.debug( + "SCA Reachability: retransformed {} class(es) for method-level detection", + toRetransform.size()); + } catch (Exception e) { + log.debug("SCA Reachability: retransformClasses failed", e); + // Re-queue on failure so the next heartbeat can retry + pendingRetransform.addAll(toRetransform); + } + } + // --------------------------------------------------------------------------- // Internal matching logic // --------------------------------------------------------------------------- @@ -468,8 +569,13 @@ private List resolveDependencies(URL url) { log.debug("SCA Reachability: could not resolve {}", url, e); resolved = Collections.emptyList(); } - List existing = jarCache.putIfAbsent(url, resolved); - return existing != null ? existing : resolved; + // Only cache non-empty results: empty means the JAR had no pom.properties, which may be + // a transient failure. Not caching allows the periodic retransform to retry successfully. + if (!resolved.isEmpty()) { + List existing = jarCache.putIfAbsent(url, resolved); + return existing != null ? existing : resolved; + } + return resolved; } private static URL locationOf(ProtectionDomain pd) { diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java index 31643418b72..bfb7372a5e6 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -52,7 +52,7 @@ void setUp() throws Exception { ScaReachabilityCollector.INSTANCE.addHit( new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); db = ScaCveDatabase.parse(new StringReader("{\"version\":1,\"entries\":[]}")); - transformer = new ScaReachabilityTransformer(db); + transformer = new ScaReachabilityTransformer(db, null); } @AfterEach @@ -174,7 +174,7 @@ void transformReturnsNullForClassLevelSymbol() throws Exception { + "\",\"method\":null}]" + "}]}"; ScaCveDatabase classDb = ScaCveDatabase.parse(new StringReader(json)); - ScaReachabilityTransformer t = new ScaReachabilityTransformer(classDb); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(classDb, null); byte[] result = t.transform( diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java index a981e74321d..a62b41f8394 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerJava9Test.java @@ -47,7 +47,7 @@ void findArtifactVersionInClasspath_findsArtifactViaJavaClassPathOnJava9Plus() t // On Java 9+, it would NOT be found by the URLClassLoader chain because the system // classloader is not a URLClassLoader. The java.class.path fallback must find it. ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); - ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db); + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db, null); String version = transformer.findArtifactVersionInClasspath("com.fasterxml.jackson.core:jackson-databind"); @@ -63,7 +63,7 @@ void findArtifactVersionInClasspath_findsArtifactViaJavaClassPathOnJava9Plus() t @EnabledForJreRange(min = JRE.JAVA_9) void findArtifactVersionInClasspath_returnsNullForUnknownArtifact() throws Exception { ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); - ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db); + ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(db, null); String version = transformer.findArtifactVersionInClasspath("com.example:nonexistent-artifact"); diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java index 9c63cd445eb..b71b1abb359 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java @@ -31,7 +31,7 @@ void setUp() throws Exception { // Drain any hits left from previous tests ScaReachabilityCollector.INSTANCE.drain(); ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); - transformer = new ScaReachabilityTransformer(db); + transformer = new ScaReachabilityTransformer(db, null); } // --- transform() return value --- diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java index d9c2fc93fdd..43f29496bad 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java @@ -19,6 +19,14 @@ public final class ScaReachabilityCollector { private final BlockingQueue hits = new LinkedBlockingQueue<>(); + /** + * Optional periodic work hook registered by {@code ScaReachabilityTransformer}. Called by {@code + * ScaReachabilityPeriodicAction} on each heartbeat to trigger retransformation of classes that + * could not be instrumented earlier (method-level symbols on already-loaded classes, or classes + * where JAR version resolution failed at load time). + */ + public volatile Runnable periodicWorkCallback; + private ScaReachabilityCollector() {} /** Called by {@code ScaReachabilityTransformer} when a vulnerable class is detected. */ diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java index 05c6882e637..df0f9f2bd34 100644 --- a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -27,6 +27,13 @@ public final class ScaReachabilityPeriodicAction @Override public void doIteration(TelemetryService telService) { + // Trigger pending retransformations (method-level symbols on already-loaded classes, or + // classes where JAR version resolution failed at load time and needs a retry). + Runnable work = ScaReachabilityCollector.INSTANCE.periodicWorkCallback; + if (work != null) { + work.run(); + } + List hits = ScaReachabilityCollector.INSTANCE.drain(); if (hits.isEmpty()) { return; From f8f9d023b7963c4310fab25a962c55d36260064a Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 12:02:34 +0200 Subject: [PATCH 07/35] Fix: remove incorrect dedup from injectCallbacks; update invariants retransformClasses() always starts from the ORIGINAL class file bytes, not from the previously-transformed bytes. A dedup check in injectCallbacks() that blocked re-injection on the second pass caused the callback to be removed (the class was returned to its original, un-instrumented state). The authoritative dedup for method-level hits is ScaReachabilityCallback.reported (bootstrap-side), which persists across retransformations regardless of how many times transform() is called on the same class. Also update .claude-invariants.md: retransformClasses is now used (for method-level only), the cache constraint clarified, and the dedup invariant documents the two-level approach (transformer for class-level, bootstrap for method-level). --- .claude-invariants.md | 345 ++++++++++++++++++ .../sca/ScaReachabilityTransformer.java | 9 +- 2 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 .claude-invariants.md diff --git a/.claude-invariants.md b/.claude-invariants.md new file mode 100644 index 00000000000..1723702864a --- /dev/null +++ b/.claude-invariants.md @@ -0,0 +1,345 @@ +# Pre-code Invariants: SCA Reachability + +**Generado:** 2026-05-12 +**Tarea:** Implementar SCA Reachability en dd-trace-java: Gradle task que descarga GHSA JSONs y genera `sca_cves.json`; `ClassFileTransformer` en `dd-java-agent/appsec/` que detecta class load de clases vulnerables; reporte via `app-dependencies-loaded` telemetry con campo `metadata` (`{type:"reachability", value: stringified JSON}`). Feature flag: `DD_APPSEC_SCA_ENABLED`. +**Ejemplo de referencia:** `telemetry/src/main/java/datadog/telemetry/dependency/LocationsCollectingTransformer.java` (patron ClassFileTransformer observation-only), `dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java` (patron startup con Instrumentation) +**Frameworks analizados:** telemetry pipeline, AppSec startup, ClassFileTransformer JVM API, Gradle build system, ComparableVersion + +--- + +## 1. Patron canonico para este tipo de cambio + +### Patron ClassFileTransformer observation-only (referencia: LocationsCollectingTransformer) + +```java +// Nunca modificar el bytecode - siempre devolver null +// Nunca lanzar excepciones - catch all internamente +// Null-check obligatorio en protectionDomain Y codeSource +public byte[] transform( + ClassLoader loader, + String className, // FORMATO INTERNO: "com/foo/Bar" con slashes + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + + if (protectionDomain == null) return null; // bootstrap classes + CodeSource cs = protectionDomain.getCodeSource(); + if (cs == null) return null; // runtime-generated classes + + // className llega en formato interno - convertir para lookup: className.replace('/', '.') + if (!vulnerableClasses.containsKey(className)) return null; // early return rapido + + URL location = cs.getLocation(); + if (location == null) return null; + + // ... queue hit para procesamiento asincrono + return null; // SIEMPRE null - no modificamos bytecode +} +``` + +### Patron de startup (referencia: IastSystem / Agent.java) + +```java +// En Agent.java execute(): +maybeStartAppSec(scoClass, sco); +maybeStartScaReachability(instrumentation, scoClass, sco); // NUEVO - despues de AppSec +maybeStartCiVisibility(instrumentation, scoClass, sco); + +// En maybeStartScaReachability(): +private static void maybeStartScaReachability( + Instrumentation instrumentation, Class scoClass, Object sco) { + if (!Config.get().isAppSecScaEnabled()) return; + // ... cargar sca_cves.json, construir indices, registrar transformer + instrumentation.addTransformer(transformer, true); // true = canRetransform + // Para clases ya cargadas: + checkAlreadyLoadedClasses(instrumentation, transformer); +} +``` + +--- + +## 2. Invariantes por categoria + +### Telemetria: Dependency class y serializacion + +| Invariante | Por que | Fuente | +|---|---|---| +| `Dependency` es `final` - no se puede subclasear | Diseno intencional de la clase | `Dependency.java` | +| `Dependency` tiene exactamente 4 campos publicos: `name`, `version`, `source`, `hash` | Ver declaracion de clase | `Dependency.java:44-47` | +| El campo `source` NO se serializa en el JSON de telemetria | Solo `hash`, `name`, `version` van al payload | `TelemetryRequestBody.java:269-274` | +| La serializacion usa **Moshi JsonWriter** (NO Jackson) | Ver imports de `TelemetryRequestBody` | `TelemetryRequestBody.java:264` | +| Para anadir `metadata`: hay que anadir campo a `Dependency` Y actualizar `writeDependency()` | `Dependency` es inmutable - nuevo constructor con metadata opcional | Investigacion | +| El campo `metadata.value` en el payload DEBE ser un JSON string (no objeto) | RFC lo especifica explicitamente: "MUST be serialized into a JSON string" | RFC | +| Heartbeat interval: 60s por defecto | Los hits de SCA se reportan con hasta 60s de delay | `TelemetryRunnable` / Config | +| Limite de 2000 dependencias en extended heartbeat | `ExtendedHeartbeatData` cap | `ExtendedHeartbeatData.java` | +| `DependencyService.installOn(instrumentation)` se llama DENTRO de `startTelemetry()` | `startTelemetry()` se ejecuta DESPUES de `maybeStartAppSec()` | `Agent.java:684,692` / `TelemetrySystem.java:45` | + +### Condicion fundamental de instrumentacion/reporte + +**Una clase que aparece en `sca_cves.json` NO se instrumenta ni reporta por defecto. Solo se actua sobre ella cuando se cumplen las TRES condiciones simultaneamente:** + +1. **El nombre de la clase esta en el indice** — `database.entriesForClass(className)` devuelve entries no vacios. +2. **La dependencia esta cargada** — `DependencyResolver.resolve(jarUrl)` (o el scan del classpath en Path B) devuelve al menos un `Dependency` cuyo `name` coincide con `entry.artifact()`. +3. **La version es vulnerable** — `entry.isVersionVulnerable(dep.version)` devuelve `true`, es decir, la version del JAR cargado cae dentro de los rangos de `sca_cves.json`. + +La presencia de una clase en el indice es condicion necesaria pero NO suficiente. Si la aplicacion no tiene cargada la libreria vulnerable (o tiene una version parcheada), el transformer no hace nada para esa clase aunque su nombre aparezca en el indice. + +**Implicacion para el indice de `ScaCveDatabase`:** el indice mapea class name → entries para lookup rapido O(1), pero entries puede contener vulnerabilidades de multiples artifacts distintos. El filtrado por artifact+version ocurre siempre en `processClass()` / `processPathA()` / `processPathB()`, NUNCA en el indice mismo. + +**Implicacion para el retransform periodico:** las clases en `pendingRetransform` / `pendingRetransformNames` se retransforman pero si en el momento del retransform la version sigue sin resolverse o no coincide, `transform()` devuelve null sin reportar nada. No es un bug — es el comportamiento correcto. + +### ClassFileTransformer: formato y threading + +| Invariante | Por que | Fuente | +|---|---|---| +| `className` en `transform()` llega en formato INTERNO con slashes: `"com/foo/Bar"` | Formato JVM interno, no dot-separated | `ClassFileTransformer` JVM spec | +| El map de lookup DEBE usar formato con slashes como clave | Para evitar conversion en el hot path | `IastSecurityControlTransformer.java:36` | +| Devolver `null` = no modificar bytecode (el JVM conserva el original) | Documentado en `LocationsCollectingTransformer.java:35` | `LocationsCollectingTransformer.java:35` | +| `protectionDomain` puede ser null (clases del bootstrap classloader) | Bootstrap classes no tienen ProtectionDomain | `LocationsCollectingTransformer.java:31` | +| `protectionDomain.getCodeSource()` puede devolver null (clases generadas en runtime) | Proxies dinamicos, lambdas, etc. | `LocationsCollectingTransformer.java:40` | +| `transform()` se llama desde multiples threads concurrentemente | JVM class loading es multi-threaded | JVM spec | +| Para detectar clases ya cargadas: `instrumentation.getAllLoadedClasses()` filtradas por el set | Necesario con `canRetransform=true` | `Instrumentation` javadoc | +| `addTransformer(transformer, true)` requerido para poder retransformar clases ya cargadas | El segundo parametro es `canRetransform` | `Instrumentation.addTransformer()` | +| Al retransformar clases ya cargadas, llamar `instrumentation.retransformClasses(classes[])` | Dispara el transformer sobre clases ya loaded | `ConfigurationUpdater.java` (Debugger) | + +### AppSec startup: punto de inyeccion + +| Invariante | Por que | Fuente | +|---|---|---| +| `AppSecSystem.start()` NO recibe `Instrumentation` | Solo recibe `SubscriptionService` y `SharedCommunicationObjects` | `AppSecSystem.java:49` | +| El patron correcto es una funcion `maybeStartScaReachability(Instrumentation, ...)` separada en `Agent.java` | Mismo patron que IAST (`maybeStartIast(instrumentation)`) | `Agent.java:670,1082` | +| Inyectar DESPUES de `maybeStartAppSec()` en `execute()` (linea 684) | AppSec debe estar iniciado antes; `instrumentation` disponible en `execute()` via campo de la clase externa | `Agent.java:684-693` | +| `Config.get().isAppSecScaEnabled()` es la gate correcta | Ya existe y hace null-check de `Boolean` | `Config.java:5761` | +| La llamada en `Agent.java` usa reflexion para `AppSecSystem.start()` | Ver `startAppSec()` - usa `getMethod().invoke()` | `Agent.java:1055-1064` | + +### Version matching + +| Invariante | Por que | Fuente | +|---|---|---| +| `ComparableVersion` ya existe en `internal-api` - backport de Apache Maven 3.9.9 | No hace falta dependencia externa | `internal-api/src/main/java/datadog/trace/util/ComparableVersion.java` | +| `isWithin(start, end)` comprueba `[start, end)` - inclusivo start, exclusivo end | Ver implementacion: `compareTo(start) >= 0 && compareTo(end) < 0` | `ComparableVersion.java:130-132` | +| Los rangos GHSA usan formato: `"< 2.6.7.3"`, `">= 2.7.0, < 2.7.9.5"`, `"= 9.5.0"` | Formato propio del database | GHSA enrichments JSONs | +| Hay que implementar un `VersionRangeParser` para estos strings - no existe en el codebase | No hay utilidad de parsing de rangos | Investigacion | +| Versiones de 4 partes (`2.6.7.3`) son soportadas por `ComparableVersion` | Backport de Maven que las soporta nativamente | `ComparableVersion.java` | +| Para rango `"< X"`: equivale a `isWithin(ZERO, X)` o `compareTo(X) < 0` | isWithin no cubre este caso directamente | Logica | +| Para rango `"= X"`: equivale a `compareTo(X) == 0` | isWithin es half-open, no sirve para exact match | Logica | + +### Gradle: bundling de recursos generados + +| Invariante | Por que | Fuente | +|---|---|---| +| `appsec/build.gradle` YA tiene hook `processResources` con minificacion JSON | Ver `doLast { fileTree... JsonOutput.toJson... }` | `dd-java-agent/appsec/build.gradle:41-56` | +| El patron para recursos generados: `sourceSets.main.output.dir(generatedDir, builtBy: task)` | Ver ejemplo en `mule-4.5/build.gradle:253-272` | `mule-4.5/build.gradle` | +| NO hay plugin de descarga externo configurado - usar Java `URL` en tarea Gradle o anadir `de.undercouch.gradle-download-plugin` | No se usa en el resto del proyecto | `build.gradle.kts` raiz | +| `buildSrc` tiene Jackson disponible para procesamiento JSON en tareas Gradle | Ver `buildSrc/build.gradle.kts` dependencies | `buildSrc/build.gradle.kts` | +| Recursos en `src/main/resources/` o directorio generado son accesibles via `getResourceAsStream()` | Comportamiento estandar de JAR | Java spec | + +### Formato GHSA: parsing del database + +| Invariante | Por que | Fuente | +|---|---|---| +| Cada fichero GHSA es un **array JSON** (no objeto): `[{...}]` | Ver cualquier fichero del database | GHSA database | +| Solo procesar entradas con `"language": "jvm"` | Hay otros lenguajes (python, etc.) en el mismo repo | GHSA database | +| Nombre completo de clase = `value + "." + name` (e.g., `"com.fasterxml.jackson.databind"` + `"ObjectMapper"` = `"com.fasterxml.jackson.databind.ObjectMapper"`) | Separacion en el formato GHSA | GHSA `GHSA-645p-88qh-w398.json` | +| Los ficheros NO contienen CVE ID directamente - solo el GHSA ID es el nombre del fichero | Para el `id` en el payload, usar GHSA ID o enriquecer con CVE API | GHSA database | +| Version ranges estan en `package[].version_range[]` (array de strings) | Cada paquete afectado puede tener multiples rangos | GHSA database | +| Simbolos en `ecosystem_specific.imports[].symbols[]` - puede haber multiples arrays `imports` | Iterar todos los imports, no solo el primero | GHSA database | + +--- + +## 3. Reglas "nunca hagas X" + +- **NUNCA** modifiques el bytecode para class-level symbols - siempre devuelve `null`. Para method-level SÍ devuelve bytecode modificado (inyeccion ASM). +- **NUNCA** lances excepciones desde `transform()` - cualquier error debe ser capturado internamente; lanzar aqui romperia la carga de la clase. +- **NUNCA** hagas I/O bloqueante en el hot path de `transform()` (no abrir JARs en cada llamada) - usar cache `ConcurrentHashMap>`. Solo cachear resultados NO vacios — los vacios permiten reintento en el retransform periodico. +- **NUNCA** pongas dedup en `injectCallbacks()` del ASM MethodVisitor**: `retransformClasses()` parte siempre de los bytes ORIGINALES del classfile. Si el dedup bloquea la reinyeccion, la retransformacion devuelve los bytes originales sin callback — el callback queda eliminado. La unica dedup autorizada para method-level es `ScaReachabilityCallback.reported` (bootstrap), que persiste entre retransformaciones. Para class-level la dedup vive en `reportedHits` del transformer (se aplica en `reportHit()`, nunca en la ruta ASM). +- **NUNCA** reportes el mismo (vulnId, artifact, symbolName) mas de una vez. Dedup por nivel: class-level → `reportedHits` en transformer; method-level → `ScaReachabilityCallback.reported` en bootstrap. +- **NUNCA** uses el `source` field de `Dependency` para metadata - no se serializa en el JSON de telemetria. +- **NUNCA** uses Moshi fuera del modulo `telemetry/` para serializar - dentro del modulo appsec usar `JsonOutput.toJson()` (Groovy/Gradle) o implementar manualmente para el stringified JSON del payload. +- **NUNCA** uses `isWithin()` para exact match (`= X`) - `isWithin` es half-open `[start, end)`. +- **NUNCA** registres el transformer con `canRetransform=false` si quieres detectar clases ya cargadas - necesitas `true` para poder llamar `retransformClasses()`. +- **NUNCA** escribas al span/trace - el RFC prohibe explicitamente esto: "This RFC does not write anything to traces/spans." + +--- + +## 4. Edge cases que los tests revelan + +- **Clases bootstrap**: cargadas por el bootstrap classloader tienen `protectionDomain == null` - deben ser ignoradas silenciosamente. +- **Lambdas y proxies dinamicos**: pueden tener `codeSource == null` - ignorar. +- **JARs sin pom.properties**: la version puede no resolverse. Estos JARs no deben ser reportados (sin version = sin match de rango). +- **Version desconocida vs version en rango**: distinguir entre "no se pudo obtener la version" y "la version es X pero no esta en el rango afectado". +- **Multiples CVEs por clase**: una clase puede aparecer en varios CVEs si la misma clase pertenece a una libreria con multiples vulnerabilidades - hay que reportar TODOS los CVEs para esa clase. +- **Multiples classloaders** (Gap 6 — no requiere cambios): en app servers la misma clase se carga por ClassLoaders distintos con JARs potencialmente en versiones distintas. El diseno lo maneja correctamente por composicion: (1) el version check es por ProtectionDomain/URL, no por nombre de clase; (2) el cache de version es por URL, distingue JARs distintos; (3) el set de ya-reportados es por `(artifact, vuln_id)` — reportar una vez es suficiente segun el RFC aunque multiples classloaders tengan la version vulnerable. Documentar con comentario de codigo que aclare este comportamiento intencional. +- **Estado compartido entre transformer y periodic action** (Gap 9): NO se necesita `ScaReachabilityService` separado. El transformer es instancia unica y lleva su propio estado: `Map> index` (inmutable), `ConcurrentHashMap> versionCache`, `Set reportedHits = ConcurrentHashMap.newKeySet()`. La unica pieza compartida con el exterior es `ScaReachabilityCollector.INSTANCE` (internal-api, Gap 8) donde el transformer deposita los hits y la periodic action los drena. +- **Multiples version_range por paquete**: una CVE puede afectar a rangos discontinuos de versiones (ej: `< 2.6.7.3` Y `>= 2.7.0, < 2.7.9.5`). El check es OR: la version esta afectada si cae en CUALQUIERA de los rangos. +- **`line` field**: RESUELTO — backend acepta `1` como placeholder para class-level detection. +- **Tipos array**: el transformer recibe `[Ljava/sql/PreparedStatement;` y similares. Filtrar con early return: `if (className == null || className.charAt(0) == '[') return null` antes del lookup. +- **`retransformClasses()` — solo para method-level, nunca para class-level**: dispara TODOS los transformers registrados (Byte Buddy, IAST, Debugger...), no solo el nuestro. Para el startup scan de class-level usar `getAllLoadedClasses()` + `Class.getProtectionDomain()` directamente (sin pipeline de transformacion). Para method-level SÍ se usa (via `performPendingRetransforms()`) porque necesitamos modificar el bytecode — el coste es acotado a las clases vulnerables especificas. +- **Dos caminos de matching segun el origen de la clase** (Gap 4 — resuelto): + + **Camino A - Clase 3rd-party** (ej: `com.fasterxml.jackson.databind.ObjectMapper`): + ``` + class load → protectionDomain.getCodeSource() → jackson-databind-2.8.5.jar + → leer pom.properties del JAR → groupId:artifactId + version + → comprobar si artifact coincide con el del CVE Y version esta en rango + → reportar si match + ``` + + **Camino B - Clase JDK/estandar** (ej: `java.sql.PreparedStatement`, `protectionDomain == null`): + ``` + startup scan → getAllLoadedClasses() → para clases JDK en el set vulnerable: + → escanear URLs del classloader del sistema buscando el artifact asociado + → leer version del artifact JAR via pom.properties + → reportar si version en rango + ``` + +- **Path B SOLO en startup scan, nunca en transformer ongoing** (Gap 5 — decision de diseno): + Las clases JDK (`java.sql.*`, `javax.sql.*`) se cargan siempre en startup o muy temprano, nunca de forma lazy durante la ejecucion normal. Por tanto Path B no necesita ejecutarse en el transformer ongoing — solo en `checkAlreadyLoadedClasses()` al arranque. + **IMPORTANTE**: esta es una asuncion que debe documentarse en comentario de codigo y en la PR description. Si en el futuro hubiera evidencia de clases JDK cargadas de forma lazy (ej: JDBC con lazy init), habria que revisar este diseno y anadir Path B al transformer con un mecanismo de retry en la periodic action. + - En el transformer ongoing: si `protectionDomain == null` → `return null` inmediatamente (no es clase 3rd-party, ya cubierta en startup) + - Para Path B en startup: escanear `ClassLoader` del sistema via `URLClassLoader.getURLs()` subiendo la cadena de parents. No depender de `DependencyService` (puede no estar listo). + +--- + +## 5. Checklist de implementacion especifico + +### Fase 1: sca_cves.json (Gradle task) + +#### IDs de vulnerabilidad +- **GHSA ID = identificador de vulnerabilidad**. Se usa directamente como `vuln_id` en `sca_cves.json` y en el payload de telemetria. No se necesita conversion a CVE ID. +- El GHSA ID se extrae del nombre del fichero: `GHSA-645p-88qh-w398.json` → `"GHSA-645p-88qh-w398"` +- No hay llamadas a la GitHub Advisory API, no hay cache de mapping, no hay dependencia de red en el Gradle task para IDs. + +#### Enumeracion de ficheros GHSA +- Para listar todos los ficheros del directorio usar la GitHub Contents API: `GET https://api.github.com/repos/DataDog/sca-reachability-database/contents/enrichments` +- Devuelve array de objetos con `name` (nombre del fichero) y `download_url` (URL raw para descargarlo) +- Usar `GITHUB_TOKEN` si disponible para evitar rate limiting (60 req/hora sin token) +- Si la API no esta disponible: `GradleException` con mensaje claro (no hay fallback) + +#### Brechas entre enrichments y RFC contract (requieren decision del equipo — ver APPSEC-62260) +- **Sin IDs en el fichero**: GHSA ID viene del nombre del fichero — RESUELTO: usar GHSA ID directamente. +- **Root structure distinta**: enrichments son `[{...}]` (array), RFC espera `{"targets":[...]}`. Normalizar en el task. +- **N packages por GHSA**: una entrada puede afectar a 5+ artifacts distintos con rangos de version diferentes (ej: Spring4Shell). El task expande cada entrada en N registros en `sca_cves.json`, uno por artifact. +- **Clases de la JDK como simbolos**: el enrichment de PostgreSQL lista `java.sql.PreparedStatement`, `java.sql.Connection`, etc. (clases JDK, no del driver). Decision pendiente: ¿filtrar `java.*`/`javax.*`? — pregunta abierta en Jira. +- **Solo class-level symbols**: 131/131 simbolos JVM son `type:"class"`, ningun metodo. Decision pendiente: ¿soportar class-load como signal? — pregunta abierta en Jira. + +#### Formato de sca_cves.json — soporta class-level Y method-level desde el primer dia + +Method-level llegara en el futuro (timeline desconocido). El formato debe soportar ambos para no migrar despues: +```json +{ + "version": 1, + "entries": [ + { + "vuln_id": "GHSA-645p-88qh-w398", + "artifact": "com.fasterxml.jackson.core:jackson-databind", + "version_ranges": ["< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"], + "symbols": [ + {"class": "com/fasterxml/jackson/databind/ObjectMapper", "method": null}, + {"class": "com/fasterxml/jackson/databind/ObjectMapper", "method": "readValue"} + ] + } + ] +} +``` +- `method: null` → class-level: transformer devuelve null (observation only), reporta `symbol=""`, `line=1` +- `method: "readValue"` → method-level (futuro): transformer inyecta callback en bytecode con ASM, reporta `symbol="readValue"` + linea real +- Clases en formato interno con slashes (evita conversion en hot path) +- Actualmente el database solo genera entradas con `method: null`; el campo existe para extensibilidad + +#### Checklist de implementacion +- [ ] Task lista ficheros via GitHub Contents API (`GET .../contents/enrichments`), descarga cada uno +- [ ] Solo procesa entradas con `"language": "jvm"` +- [ ] Expande entradas multi-package en N registros (uno por artifact) +- [ ] Por cada simbolo GHSA: genera `{"class": "com/foo/Bar", "method": null}` (siempre null hoy, campo reservado para metodos futuros) +- [ ] Clase en formato interno: `(value + "." + name).replace('.', '/')` +- [ ] Itera TODOS los `imports[]` de `ecosystem_specific` +- [ ] Output: `sca_cves.json` en `build/generated-resources/main/` +- [ ] Wired en `processResources` de `appsec/build.gradle` (sigue el patron existente del hook) +- [ ] JSON minificado (sigue el patron de `JsonOutput.toJson()` del hook existente) + +### Fase 2: Runtime - Version matching +- [ ] `VersionRangeParser` parsea strings GHSA: operadores `<`, `<=`, `>`, `>=`, `=`, separados por coma +- [ ] Usa `ComparableVersion` para comparacion (no reimplementar) +- [ ] `isWithin(start, end)` para rangos `[start, end)` +- [ ] `compareTo() < 0` para `< X` +- [ ] `compareTo() == 0` para `= X` +- [ ] Handles version null/empty (no match) +- [ ] **Gap 10 (version qualifiers) — no requiere codigo extra**: `ComparableVersion` normaliza `RELEASE`/`GA`/`FINAL` → release limpio por ser backport de Maven 3.9.9. `5.2.20.RELEASE == 5.2.20`. Escribir test unitario que cubra explicitamente: `5.2.19.RELEASE < 5.2.20.RELEASE`, `5.2.20.RELEASE == 5.2.20`, `5.2.20 < 5.2.20.RELEASE` (false). + +### Fase 3: ClassFileTransformer +- [ ] Index principal: `Map>` keyed por class name en formato INTERNO (slashes) +- [ ] `SymbolInfo` contiene: `vulnId`, `artifact`, `versionRanges`, `method` (null = class-level) +- [ ] `transform()`: primero filtrar `className == null || className.charAt(0) == '['` → `return null` +- [ ] `transform()`: si `protectionDomain == null` → `return null` (clase JDK/bootstrap, cubierta en startup scan) +- [ ] `transform()`: si `codeSource == null || location == null` → `return null` +- [ ] `transform()` bifurca segun `method`: + - `method == null` → class-level (Camino A): `return null`, encola hit para version check + - `method != null` → method-level (futuro): inyectar callback con ASM, `return transformedBytecode` +- [ ] `transform()` no lanza excepciones (catch all — nunca romper carga de clase) +- [ ] Cache de version por JAR URL: `ConcurrentHashMap>` +- [ ] Version resolution sincrona desde pom.properties del JAR (una vez por JAR, cacheado) +- [ ] Check de ya-reportado: `ConcurrentHashSet` de pares "artifact:vuln_id" ya enviados +- [ ] `addTransformer(transformer, true)` (necesario para method-level futuro que si modifica bytecode) +- [ ] `checkAlreadyLoadedClasses()` al startup: + - Camino A: `getAllLoadedClasses()` + `Class.getProtectionDomain()` — NO `retransformClasses()` + - Camino B: para clases JDK en el set, escanear `URLClassLoader.getURLs()` buscando el artifact asociado + - Comentario de codigo obligatorio: "Assumption: JDK classes (protectionDomain == null) are loaded at startup. If lazy JDBC init or similar patterns cause JDK classes to load after startup, Path B hits would be missed. See APPSEC-62260 for design rationale." + +### Fase 4: Agent startup wiring (Gap 7 — patron de reflexion) +- [ ] `maybeStartScaReachability(Instrumentation)` en `Agent.java`, llamada desde `execute()` despues de `maybeStartAppSec()` +- [ ] Gateada por `Config.get().isAppSecScaEnabled()` +- [ ] Usa `AGENT_CLASSLOADER.loadClass("com.datadog.appsec.sca.ScaReachabilitySystem").getMethod("start", Instrumentation.class).invoke(null, instrumentation)` +- [ ] `Instrumentation` no tiene problema de classloader — es interfaz JDK (bootstrap), visible desde ambos lados +- [ ] `ScaReachabilitySystem.start()` debe ser `public static void start(Instrumentation)` +- [ ] NO necesita `SubscriptionService` ni `SharedCommunicationObjects` — SCA no usa el gateway de AppSec +- [ ] Envolver en `StaticEventLogger.begin/end("ScaReachability")` siguiendo el patron de otros subsistemas + +### Fase 5: Telemetry (Gap 8 — patron canonico via internal-api) + +El patron es identico a `WafMetricCollector` / `WafMetricPeriodicAction`. NO hay circular dependency: +- `telemetry` → `internal-api` (ya existe), `appsec` → `internal-api` (ya existe), `telemetry` → `appsec` NUNCA. + +**Modulo `internal-api`** — `ScaReachabilityCollector` (singleton, misma estructura que `WafMetricCollector`): +- [ ] Queue de `ReachabilityHit` (BlockingQueue) +- [ ] `addHit(ReachabilityHit)` llamado por `ScaReachabilityService` en appsec +- [ ] `drain()` llamado por `ScaReachabilityPeriodicAction` en telemetry + +**Modulo `telemetry`** — `ScaReachabilityPeriodicAction implements TelemetryPeriodicAction`: +- [ ] Lee de `ScaReachabilityCollector.get().drain()` +- [ ] Convierte cada hit en `Dependency` con metadata y llama `telService.addDependency()` +- [ ] Registrada en `TelemetrySystem.createTelemetryRunnable()` con `if (Config.get().isAppSecScaEnabled())` + +**Modulo `telemetry`** — extension de `Dependency`: +- [ ] Campo `reachabilityMetadata` (nullable `List`) anadido a `Dependency` +- [ ] Constructor backward-compatible: nuevo constructor con metadata, existente delega con `null` +- [ ] `TelemetryRequestBody.writeDependency()` escribe `metadata` array si no null/vacio + +**Formato del payload:** +- [ ] Formato metadata: `[{"type":"reachability","value":""}]` +- [ ] Stringified JSON: `{"id":"GHSA-xxx","reached":[{"path":"com.foo.Class","symbol":"","line":1}]}` +- [ ] `path` = FQN con puntos, `symbol` = `""`, `line` = 1 + +--- + +## 6. Preguntas abiertas + +1. **CVE vs GHSA ID**: RESUELTO — usar GHSA ID directamente como identificador. + +2. **`line` field**: RESUELTO — el backend acepta `1` como placeholder para class-level detection. + +3. **`symbol` para class-level**: RESUELTO — usar `""` (estandar JVM, aparece en stack traces reales). + +4. **Method-level symbols**: RESUELTO — formato `sca_cves.json` y transformer diseñados para soportar ambos desde el primer dia; implementacion method-level queda como stub. + +5. **N packages por GHSA**: RESUELTO — expandir cada entrada multi-package en N registros independientes en `sca_cves.json` (uno por artifact). Cumple el contrato RFC: cada entrada tiene exactamente un `dependency_name`. + +4. **Version resolution en transformer hot path**: Leer pom.properties del JAR en `transform()` puede ser un bottleneck si hay muchas clases del mismo JAR siendo cargadas simultaneamente. Confirmar si el approach de cache-by-URL es suficiente o si necesitamos una cola async como `DependencyService`. + +5. **Periodicidad del reporte**: Las reachability hits se reportan en el siguiente heartbeat (60s delay). Confirmar si esto es aceptable o si necesitamos un flush mas agresivo. + +--- + +## Historial + +- 2026-05-12 - Creado para tarea: SCA Reachability - Fase completa (Gradle task + ClassFileTransformer + telemetry extension) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 0c581d3b4ab..c02919ecf39 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -445,11 +445,12 @@ private void ensureInjected() { } private void injectCallbacks(int line) { + // No dedup check here: retransformClasses() always starts from the original class bytes, + // so the callback must be re-injected on every transformation pass. Deduplication of + // actual runtime reports is handled by ScaReachabilityCallback.reported (bootstrap-side), + // which persists across retransformations and prevents duplicate hits regardless of how + // many times the class is retransformed. for (MethodCallbackSpec spec : specs) { - String dedupKey = spec.vulnId + "|" + spec.artifact + "|" + spec.methodName; - if (!reportedHits.add(dedupKey)) { - continue; // already reported this method hit - } mv.visitLdcInsn(spec.vulnId); mv.visitLdcInsn(spec.artifact); mv.visitLdcInsn(spec.version); From 82ea8065d9613f7978882b3195b536b26a0bf352 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 12:08:51 +0200 Subject: [PATCH 08/35] pr-review: fix null guard, encapsulate periodicWorkCallback, update Javadoc, add retransform tests - performPendingRetransforms(): early return when instrumentation is null (unit test safety) - ScaReachabilityCollector: encapsulate periodicWorkCallback as private with getter/setter - ScaReachabilityTransformer class Javadoc: update dedup description from (vulnId,artifact) pair to (vulnId,artifact,symbolName) tuple; document two-level dedup strategy - Add 3 tests for performPendingRetransforms(): no-op with null inst, retransformClasses called for pending queue, no-op when both queues empty --- .../appsec/sca/ScaReachabilitySystem.java | 4 +- .../sca/ScaReachabilityTransformer.java | 10 ++- .../sca/ScaReachabilityTransformerTest.java | 64 +++++++++++++++++++ .../telemetry/ScaReachabilityCollector.java | 10 ++- .../sca/ScaReachabilityPeriodicAction.java | 2 +- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index c10804eb988..a797b52337b 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -60,7 +60,7 @@ public static void start(Instrumentation instrumentation) { // Register the periodic retransform callback so the telemetry heartbeat can retry // method-level instrumentation for classes that could not be processed at load time. - ScaReachabilityCollector.INSTANCE.periodicWorkCallback = - transformer::performPendingRetransforms; + ScaReachabilityCollector.INSTANCE.setPeriodicWorkCallback( + transformer::performPendingRetransforms); } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index c02919ecf39..5c9b4eac92b 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -45,7 +45,9 @@ *

  • All shared state uses concurrent collections — {@link #transform} is called from multiple * class-loading threads simultaneously. *
  • Version resolution is cached per JAR URL — each JAR is read at most once. - *
  • Each (vulnId, artifact) pair is reported at most once — RFC requires a single occurrence. + *
  • Each (vulnId, artifact, symbolName) tuple is reported at most once — RFC requires a single + * occurrence. Class-level dedup lives in {@link #reportedHits}; method-level dedup lives in + * {@code ScaReachabilityCallback.reported} (bootstrap-side, persists across retransforms). *
  • Path B (JDK classes such as {@code java.sql.PreparedStatement}) is handled only in {@link * #checkAlreadyLoadedClasses}, not in {@link #transform}, because JDK classes are always * loaded at startup. If a JDK class relevant to a CVE were loaded lazily after startup, the @@ -75,7 +77,8 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { * * Drained and processed by {@link #performPendingRetransforms()} on each telemetry heartbeat. */ - private final ConcurrentLinkedQueue> pendingRetransform = new ConcurrentLinkedQueue<>(); + // package-private for testing + final ConcurrentLinkedQueue> pendingRetransform = new ConcurrentLinkedQueue<>(); public ScaReachabilityTransformer(ScaCveDatabase database, Instrumentation instrumentation) { this.database = database; @@ -276,6 +279,9 @@ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { * periodicWorkCallback} registered in {@link ScaReachabilityCollector}. */ public void performPendingRetransforms() { + if (instrumentation == null) { + return; // no-op when instrumentation is unavailable (e.g. in unit tests) + } // Drain the direct Class queue (from checkAlreadyLoadedClasses) List> toRetransform = new ArrayList<>(); Class clazz; diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java index b71b1abb359..2eead9933e8 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java @@ -1,5 +1,7 @@ package com.datadog.appsec.sca; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -163,6 +165,68 @@ private static java.lang.instrument.Instrumentation fakeInstrumentationReturning }); } + // --- performPendingRetransforms --- + + @Test + void performPendingRetransforms_noOpWhenNullInstrumentation() { + // Transformer constructed with null instrumentation (test context) must not throw + transformer.performPendingRetransforms(); + // No exception = pass + } + + @Test + void performPendingRetransforms_callsRetransformClassesForPendingQueue() throws Exception { + List> retransformed = new java.util.ArrayList<>(); + java.lang.instrument.Instrumentation inst = + (java.lang.instrument.Instrumentation) + java.lang.reflect.Proxy.newProxyInstance( + ScaReachabilityTransformerTest.class.getClassLoader(), + new Class[] {java.lang.instrument.Instrumentation.class}, + (proxy, method, args) -> { + if ("getAllLoadedClasses".equals(method.getName())) { + return new Class[0]; + } + if ("retransformClasses".equals(method.getName())) { + retransformed.addAll(java.util.Arrays.asList((Class[]) args[0])); + return null; + } + return null; + }); + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, inst); + + // Simulate checkAlreadyLoadedClasses adding a class to the pending queue + t.pendingRetransform.add(ScaReachabilityTransformer.class); + t.performPendingRetransforms(); + + assertEquals(1, retransformed.size()); + assertEquals(ScaReachabilityTransformer.class, retransformed.get(0)); + } + + @Test + void performPendingRetransforms_noOpWhenQueuesEmpty() throws Exception { + List methodsCalled = new java.util.ArrayList<>(); + java.lang.instrument.Instrumentation inst = + (java.lang.instrument.Instrumentation) + java.lang.reflect.Proxy.newProxyInstance( + ScaReachabilityTransformerTest.class.getClassLoader(), + new Class[] {java.lang.instrument.Instrumentation.class}, + (proxy, method, args) -> { + methodsCalled.add(method.getName()); + return method.getName().equals("getAllLoadedClasses") ? new Class[0] : null; + }); + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, inst); + + t.performPendingRetransforms(); + + // With empty queues: getAllLoadedClasses is skipped (pendingRetransformNames is empty), + // retransformClasses is never called + assertFalse( + methodsCalled.contains("retransformClasses"), + "retransformClasses must not be called when both queues are empty"); + } + // --- helpers --- private static ProtectionDomain protectionDomainFor(URL location) throws Exception { diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java index 43f29496bad..0b5e82ab70a 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityCollector.java @@ -25,7 +25,15 @@ public final class ScaReachabilityCollector { * could not be instrumented earlier (method-level symbols on already-loaded classes, or classes * where JAR version resolution failed at load time). */ - public volatile Runnable periodicWorkCallback; + private volatile Runnable periodicWorkCallback; + + public void setPeriodicWorkCallback(Runnable callback) { + periodicWorkCallback = callback; + } + + public Runnable getPeriodicWorkCallback() { + return periodicWorkCallback; + } private ScaReachabilityCollector() {} diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java index df0f9f2bd34..dbbc95d9576 100644 --- a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -29,7 +29,7 @@ public final class ScaReachabilityPeriodicAction public void doIteration(TelemetryService telService) { // Trigger pending retransformations (method-level symbols on already-loaded classes, or // classes where JAR version resolution failed at load time and needs a retry). - Runnable work = ScaReachabilityCollector.INSTANCE.periodicWorkCallback; + Runnable work = ScaReachabilityCollector.INSTANCE.getPeriodicWorkCallback(); if (work != null) { work.run(); } From 39eef441e87329c53c7b08309cb35fd1a66329db Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 12:42:58 +0200 Subject: [PATCH 09/35] Fix two Codex review issues: java.nio in premain and transitive JAR resolution P1: Replace StandardCharsets.UTF_8 with "UTF-8" string in ScaCveDatabase.load(). java.nio.* is forbidden during premain (bootstrap_design_guidelines.md) because it can trigger premature provider initialization before the app configures the runtime. P2: Add classpath fallback in resolveVersionForArtifact() for entries where the vulnerable artifact is an aggregator/starter POM whose watched classes live in a transitive dependency JAR (e.g., spring-boot-starter-web watches @Controller but @Controller is defined in spring-context.jar, not the starter). The new helper first checks the class's own JAR, then falls back to findArtifactVersionInClasspath with a hit cache (classpathArtifactCache). processPathA uses the same helper for consistency. --- .../datadog/appsec/sca/ScaCveDatabase.java | 5 +- .../sca/ScaReachabilityTransformer.java | 122 ++++++++++++------ 2 files changed, 86 insertions(+), 41 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java index 8c657a32a00..819ba430858 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java @@ -7,7 +7,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; +// Note: java.nio.* is forbidden during premain (bootstrap_design_guidelines.md). +// Use the string charset name "UTF-8" instead of StandardCharsets.UTF_8. import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -48,7 +49,7 @@ public static ScaCveDatabase load() { RESOURCE_PATH); return new ScaCveDatabase(Collections.emptyMap()); } - try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + try (InputStreamReader reader = new InputStreamReader(stream, "UTF-8")) { return parse(reader); } catch (Exception e) { log.error( diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 5c9b4eac92b..e5e53c820b6 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -64,6 +64,15 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { /** Cache: JAR URL → resolved dependencies. Only non-empty results are cached to allow retries. */ private final ConcurrentHashMap> jarCache = new ConcurrentHashMap<>(); + /** + * Cache: artifact name → classpath-resolved version. Used when the class's own JAR does not + * contain the vulnerable artifact (e.g., Spring Boot starters whose watched classes live in + * transitive dependency JARs). Only non-null results are cached; null means "not yet found" and + * will be retried on the next periodic retransform. + */ + private final ConcurrentHashMap classpathArtifactCache = + new ConcurrentHashMap<>(); + /** Deduplication set: "vulnId|artifact|symbol" tuples already reported. */ private final Set reportedHits = ConcurrentHashMap.newKeySet(); @@ -144,47 +153,50 @@ public byte[] transform( */ private byte[] processClass( String className, URL jarUrl, List entries, byte[] classfileBuffer) { - List deps = resolveDependencies(jarUrl); + List classJarDeps = resolveDependencies(jarUrl); // Collect method-level callbacks to inject, keyed by method name Map> methodCallbacks = new HashMap<>(); boolean hasUnresolvedMethodLevelSymbols = false; for (ScaEntry entry : entries) { - // Check if this entry has method-level symbols for this class before version resolution, - // so we can schedule a retry if deps are unavailable now. boolean entryHasMethodLevelSymbol = entry.symbols().stream() .anyMatch(s -> s.className().equals(className) && !s.isClassLevel()); - for (Dependency dep : deps) { - if (!entry.artifact().equals(dep.name) || !entry.isVersionVulnerable(dep.version)) { - continue; - } - for (ScaSymbol symbol : entry.symbols()) { - if (!symbol.className().equals(className)) { - continue; - } - if (symbol.isClassLevel()) { - reportHit(entry, dep.version, className, "", 1); - } else { - methodCallbacks - .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) - .add( - new MethodCallbackSpec( - entry.vulnId(), - entry.artifact(), - dep.version, - className.replace('/', '.'), - symbol.method())); - } + // Resolve version: first check the class's own JAR, then fall back to a full classpath + // scan. The fallback handles cases where the vulnerable artifact is an aggregator/starter + // POM whose watched classes actually live in a transitive dependency JAR (e.g., + // spring-boot-starter-web watches @Controller, but @Controller is in spring-context.jar). + String version = resolveVersionForArtifact(entry.artifact(), classJarDeps); + if (version == null) { + if (entryHasMethodLevelSymbol) { + hasUnresolvedMethodLevelSymbols = true; } + continue; } - // If deps were empty (version not resolved yet) and this entry has method-level symbols, - // flag for retry. The class will be retransformed on the next periodic action heartbeat. - if (deps.isEmpty() && entryHasMethodLevelSymbol) { - hasUnresolvedMethodLevelSymbols = true; + if (!entry.isVersionVulnerable(version)) { + continue; + } + + for (ScaSymbol symbol : entry.symbols()) { + if (!symbol.className().equals(className)) { + continue; + } + if (symbol.isClassLevel()) { + reportHit(entry, version, className, "", 1); + } else { + methodCallbacks + .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) + .add( + new MethodCallbackSpec( + entry.vulnId(), + entry.artifact(), + version, + className.replace('/', '.'), + symbol.method())); + } } } @@ -321,18 +333,18 @@ public void performPendingRetransforms() { /** Path A: class came from a 3rd-party JAR — match artifact + check version. */ private void processPathA(String internalClassName, URL jarUrl, List entries) { - List deps = resolveDependencies(jarUrl); + List classJarDeps = resolveDependencies(jarUrl); for (ScaEntry entry : entries) { - for (Dependency dep : deps) { - if (entry.artifact().equals(dep.name) && entry.isVersionVulnerable(dep.version)) { - // Only class-level symbols are reported at class load time. - // Method-level symbols are handled by processClass() via ASM injection. - for (ScaSymbol symbol : entry.symbols()) { - if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { - reportHit(entry, dep.version, internalClassName, "", 1); - break; // one hit per entry is sufficient - } - } + String version = resolveVersionForArtifact(entry.artifact(), classJarDeps); + if (version == null || !entry.isVersionVulnerable(version)) { + continue; + } + // Only class-level symbols are reported at class load time. + // Method-level symbols are handled by processClass() via ASM injection. + for (ScaSymbol symbol : entry.symbols()) { + if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { + reportHit(entry, version, internalClassName, "", 1); + break; // one hit per entry is sufficient } } } @@ -353,6 +365,38 @@ private void processPathB(String internalClassName, List entries) { } } + /** + * Resolves the version of {@code artifactName} using a two-step strategy: + * + *
      + *
    1. Check the dependencies resolved from the class's own JAR ({@code classJarDeps}). This + * covers the common case where the class and its artifact live in the same JAR. + *
    2. If not found, fall back to a full classpath scan via {@link + * #findArtifactVersionInClasspath}. This handles aggregator/starter POM artifacts (e.g., + * {@code spring-boot-starter-web}) whose watched classes live in transitive dependency JARs + * rather than in the starter JAR itself. Results of successful scans are cached. + *
    + * + * @return the resolved version string, or {@code null} if the artifact cannot be found + */ + private String resolveVersionForArtifact(String artifactName, List classJarDeps) { + for (Dependency dep : classJarDeps) { + if (artifactName.equals(dep.name)) { + return dep.version; + } + } + // Classpath fallback: check cache first, then scan. + String cached = classpathArtifactCache.get(artifactName); + if (cached != null) { + return cached; + } + String version = findArtifactVersionInClasspath(artifactName); + if (version != null) { + classpathArtifactCache.put(artifactName, version); // only cache hits; misses are retried + } + return version; + } + // --------------------------------------------------------------------------- // Method-level bytecode injection (ASM) // --------------------------------------------------------------------------- From dc8ffd345e774fe2bd5d5b14fd4823faf2a90b70 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 12:54:03 +0200 Subject: [PATCH 10/35] Refactor: extract CLASS_LEVEL_SYMBOL constant and reportClassLevelHitIfPresent helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CLASS_LEVEL_SYMBOL = "" constant to avoid magic string repetition (appeared 3 times in the same class; a typo would silently produce wrong symbol names) - Extract reportClassLevelHitIfPresent(entry, version, internalClassName) helper to unify identical class-level symbol matching loops in processPathA, processPathB, and processClass — all three now delegate to the single helper --- .../sca/ScaReachabilityTransformer.java | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index e5e53c820b6..97b36dd7b17 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -58,6 +58,9 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); + /** JVM internal name for the class initializer, used as the symbol name for class-level hits. */ + private static final String CLASS_LEVEL_SYMBOL = ""; + private final ScaCveDatabase database; private final Instrumentation instrumentation; @@ -144,7 +147,7 @@ public byte[] transform( * *
      *
    • Class-level ({@code symbol.method() == null}): reports a hit immediately via {@link - * ScaReachabilityCollector} with symbol {@code ""}. + * ScaReachabilityCollector} with symbol {@code CLASS_LEVEL_SYMBOL}. *
    • Method-level ({@code symbol.method() != null}): injects a static callback into the method * bytecode via ASM. The callback is invoked the first time the method is called and reports * via {@link ScaReachabilityCallback}. Returns modified bytecode; {@code null} if only @@ -180,23 +183,21 @@ private byte[] processClass( continue; } + // Report class-level hit immediately; collect method-level symbols for ASM injection. + reportClassLevelHitIfPresent(entry, version, className); for (ScaSymbol symbol : entry.symbols()) { - if (!symbol.className().equals(className)) { + if (!symbol.className().equals(className) || symbol.isClassLevel()) { continue; } - if (symbol.isClassLevel()) { - reportHit(entry, version, className, "", 1); - } else { - methodCallbacks - .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) - .add( - new MethodCallbackSpec( - entry.vulnId(), - entry.artifact(), - version, - className.replace('/', '.'), - symbol.method())); - } + methodCallbacks + .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) + .add( + new MethodCallbackSpec( + entry.vulnId(), + entry.artifact(), + version, + className.replace('/', '.'), + symbol.method())); } } @@ -341,12 +342,7 @@ private void processPathA(String internalClassName, URL jarUrl, List e } // Only class-level symbols are reported at class load time. // Method-level symbols are handled by processClass() via ASM injection. - for (ScaSymbol symbol : entry.symbols()) { - if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { - reportHit(entry, version, internalClassName, "", 1); - break; // one hit per entry is sufficient - } - } + reportClassLevelHitIfPresent(entry, version, internalClassName); } } @@ -355,12 +351,21 @@ private void processPathB(String internalClassName, List entries) { for (ScaEntry entry : entries) { String version = findArtifactVersionInClasspath(entry.artifact()); if (version != null && entry.isVersionVulnerable(version)) { - for (ScaSymbol symbol : entry.symbols()) { - if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { - reportHit(entry, version, internalClassName, "", 1); - break; - } - } + reportClassLevelHitIfPresent(entry, version, internalClassName); + } + } + } + + /** + * Reports a class-level reachability hit for the first class-level symbol in {@code entry} that + * matches {@code internalClassName}. No-op if no matching class-level symbol exists. + */ + private void reportClassLevelHitIfPresent( + ScaEntry entry, String version, String internalClassName) { + for (ScaSymbol symbol : entry.symbols()) { + if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { + reportHit(entry, version, internalClassName, CLASS_LEVEL_SYMBOL, 1); + return; // one hit per entry is sufficient } } } From 849f3764f02c22c911879c6539e4cc7296e02ad7 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 13:03:46 +0200 Subject: [PATCH 11/35] Move CLASS_LEVEL_SYMBOL to ScaReachabilityHit; fix misleading comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move CLASS_LEVEL_SYMBOL = "" to ScaReachabilityHit (internal-api) as a public constant so both the transformer (appsec) and the telemetry payload builder share the canonical definition without cross-module string duplication. The convenience constructor also uses the constant now. ScaReachabilityTransformer delegates to ScaReachabilityHit.CLASS_LEVEL_SYMBOL. Fix misleading comment in processClass: "We enqueue via classBeingRedefined is null here" → explains that classBeingRedefined is null on first class load, preventing direct Class queuing, so scheduleRetransformByName is used instead. --- .../appsec/sca/ScaReachabilityTransformer.java | 14 ++++++++------ .../trace/api/telemetry/ScaReachabilityHit.java | 13 +++++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 97b36dd7b17..0626db4de27 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -58,8 +58,9 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); - /** JVM internal name for the class initializer, used as the symbol name for class-level hits. */ - private static final String CLASS_LEVEL_SYMBOL = ""; + // CLASS_LEVEL_SYMBOL is defined in ScaReachabilityHit (internal-api) so both the transformer + // and the telemetry payload builder share the same constant without cross-module duplication. + private static final String CLASS_LEVEL_SYMBOL = ScaReachabilityHit.CLASS_LEVEL_SYMBOL; private final ScaCveDatabase database; private final Instrumentation instrumentation; @@ -202,10 +203,11 @@ private byte[] processClass( } if (hasUnresolvedMethodLevelSymbols) { - // Schedule retransformation: when the periodic action fires, it will call - // performPendingRetransforms() which will retry version resolution and inject bytecode. - // We enqueue via classBeingRedefined is null here — we'll locate the Class object later - // in performPendingRetransforms() via instrumentation.getAllLoadedClasses(). + // Schedule retransformation for a later attempt. In transform(), classBeingRedefined is + // null (first class load), so we don't have a Class handle to put directly into + // pendingRetransform. Instead we queue the internal class name; performPendingRetransforms() + // will resolve it back to a Class via instrumentation.getAllLoadedClasses() and + // retransform. scheduleRetransformByName(className); } diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java index b21f95dc380..fed180d5890 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java @@ -7,6 +7,12 @@ */ public final class ScaReachabilityHit { + /** + * JVM internal name for the class initializer. Used as the {@code symbolName} for class-level + * hits where no specific method was targeted (detection fires at class load time). + */ + public static final String CLASS_LEVEL_SYMBOL = ""; + private final String vulnId; private final String artifact; private final String version; @@ -14,9 +20,12 @@ public final class ScaReachabilityHit { private final String symbolName; // "" for class-level; method name for method-level private final int line; // 1 as placeholder for class-level; actual first line for method-level - /** Convenience constructor for class-level hits (symbolName = {@code ""}, line = 1). */ + /** + * Convenience constructor for class-level hits ({@code symbolName = CLASS_LEVEL_SYMBOL}, line = + * 1). + */ public ScaReachabilityHit(String vulnId, String artifact, String version, String className) { - this(vulnId, artifact, version, className, "", 1); + this(vulnId, artifact, version, className, CLASS_LEVEL_SYMBOL, 1); } public ScaReachabilityHit( From 3ea0e05cce294e7db03804f5fda878e92f442015 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 13:07:42 +0200 Subject: [PATCH 12/35] Move java.nio comment to usage site; add tests for transitive JAR fallback - ScaCveDatabase: move "java.nio.* forbidden in premain" comment from the imports block to inline at the InputStreamReader construction site (comments in imports are unusual and smola flags verbose placement) - ScaReachabilityTransformer.resolveVersionForArtifact: make package-private for testing; add 4 tests covering the two-step fallback: (1) version from classJarDeps directly (2) classpath fallback when classJarDeps is empty (transitive JAR case) (3) classpathArtifactCache hit on second call (4) null for absent artifact --- .../datadog/appsec/sca/ScaCveDatabase.java | 3 +- .../sca/ScaReachabilityTransformer.java | 3 +- .../sca/ScaReachabilityTransformerTest.java | 69 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java index 819ba430858..35f360bd639 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java @@ -7,8 +7,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; -// Note: java.nio.* is forbidden during premain (bootstrap_design_guidelines.md). -// Use the string charset name "UTF-8" instead of StandardCharsets.UTF_8. import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -49,6 +47,7 @@ public static ScaCveDatabase load() { RESOURCE_PATH); return new ScaCveDatabase(Collections.emptyMap()); } + // "UTF-8" string literal — java.nio.* is forbidden during premain try (InputStreamReader reader = new InputStreamReader(stream, "UTF-8")) { return parse(reader); } catch (Exception e) { diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 0626db4de27..9796e281593 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -386,7 +386,8 @@ private void reportClassLevelHitIfPresent( * * @return the resolved version string, or {@code null} if the artifact cannot be found */ - private String resolveVersionForArtifact(String artifactName, List classJarDeps) { + // package-private for testing + String resolveVersionForArtifact(String artifactName, List classJarDeps) { for (Dependency dep : classJarDeps) { if (artifactName.equals(dep.name)) { return dep.version; diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java index 2eead9933e8..3e55be7d006 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -165,6 +166,74 @@ private static java.lang.instrument.Instrumentation fakeInstrumentationReturning }); } + // --- resolveVersionForArtifact (transitive JAR fallback) --- + + @Test + void resolveVersionForArtifact_returnsVersionFromClassJarWhenPresent() throws Exception { + // When classJarDeps contains the artifact, the version is returned directly without + // hitting the classpath scan. + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, null); + datadog.telemetry.dependency.Dependency dep = + new datadog.telemetry.dependency.Dependency( + "com.fasterxml.jackson.core:jackson-databind", "2.8.5", null, null); + + String version = + t.resolveVersionForArtifact( + "com.fasterxml.jackson.core:jackson-databind", + java.util.Collections.singletonList(dep)); + + assertEquals("2.8.5", version, "Should return version from classJarDeps directly"); + } + + @Test + void resolveVersionForArtifact_fallsBackToClasspathWhenClassJarLacksArtifact() throws Exception { + // When classJarDeps is empty (class loaded from a transitive JAR, e.g. @Controller from + // spring-context.jar while the CVE is on spring-boot-starter-web), resolveVersionForArtifact + // must fall back to scanning the application classpath. jackson-databind IS a + // testImplementation + // dependency, so if it's present in the classpath the fallback should find it. + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, null); + + String version = + t.resolveVersionForArtifact( + "com.fasterxml.jackson.core:jackson-databind", java.util.Collections.emptyList()); + + // jackson-databind is a testImplementation dep — it should be found on java.class.path. + assertNotNull(version, "Classpath fallback must find jackson-databind on the test classpath"); + assertFalse(version.isEmpty(), "Found version must be non-empty"); + } + + @Test + void resolveVersionForArtifact_cachesClasspathLookupResult() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, null); + + // First call — populates classpathArtifactCache + String v1 = + t.resolveVersionForArtifact( + "com.fasterxml.jackson.core:jackson-databind", java.util.Collections.emptyList()); + // Second call — should return the cached result + String v2 = + t.resolveVersionForArtifact( + "com.fasterxml.jackson.core:jackson-databind", java.util.Collections.emptyList()); + + assertEquals(v1, v2, "Classpath fallback result must be cached across calls"); + } + + @Test + void resolveVersionForArtifact_returnsNullForAbsentArtifact() throws Exception { + ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); + ScaReachabilityTransformer t = new ScaReachabilityTransformer(db, null); + + String version = + t.resolveVersionForArtifact( + "com.example:nonexistent-artifact-xyz", java.util.Collections.emptyList()); + + assertNull(version, "Artifact not on classpath must return null"); + } + // --- performPendingRetransforms --- @Test From 525a81c0707a6d42c8b91260d8cb00c6ba058962 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 13:15:46 +0200 Subject: [PATCH 13/35] Remove dead visitCode() override and redundant CLASS_LEVEL_SYMBOL alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove empty visitCode() in MethodEntryInjector: the method only called super.visitCode() and its comment was misleading — the actual no-debug-info fallback injection is handled by ensureInjected() in the visitInsn/visitVarInsn/ visitMethodInsn/visitFieldInsn overrides, not here - Remove private CLASS_LEVEL_SYMBOL alias in ScaReachabilityTransformer: the constant is used in exactly one place (reportClassLevelHitIfPresent) and ScaReachabilityHit.CLASS_LEVEL_SYMBOL is self-documenting at that site; the alias added a private field with no benefit after the constant was moved to ScaReachabilityHit in a previous commit --- .../appsec/sca/ScaReachabilityTransformer.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 9796e281593..1428674b1da 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -58,10 +58,6 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); - // CLASS_LEVEL_SYMBOL is defined in ScaReachabilityHit (internal-api) so both the transformer - // and the telemetry payload builder share the same constant without cross-module duplication. - private static final String CLASS_LEVEL_SYMBOL = ScaReachabilityHit.CLASS_LEVEL_SYMBOL; - private final ScaCveDatabase database; private final Instrumentation instrumentation; @@ -148,7 +144,7 @@ public byte[] transform( * *
        *
      • Class-level ({@code symbol.method() == null}): reports a hit immediately via {@link - * ScaReachabilityCollector} with symbol {@code CLASS_LEVEL_SYMBOL}. + * ScaReachabilityCollector} with symbol {@link ScaReachabilityHit#CLASS_LEVEL_SYMBOL}. *
      • Method-level ({@code symbol.method() != null}): injects a static callback into the method * bytecode via ASM. The callback is invoked the first time the method is called and reports * via {@link ScaReachabilityCallback}. Returns modified bytecode; {@code null} if only @@ -366,7 +362,7 @@ private void reportClassLevelHitIfPresent( ScaEntry entry, String version, String internalClassName) { for (ScaSymbol symbol : entry.symbols()) { if (symbol.className().equals(internalClassName) && symbol.isClassLevel()) { - reportHit(entry, version, internalClassName, CLASS_LEVEL_SYMBOL, 1); + reportHit(entry, version, internalClassName, ScaReachabilityHit.CLASS_LEVEL_SYMBOL, 1); return; // one hit per entry is sufficient } } @@ -463,13 +459,6 @@ public void visitLineNumber(int line, Label start) { super.visitLineNumber(line, start); } - @Override - public void visitCode() { - super.visitCode(); - // Fallback for methods without debug info (no visitLineNumber calls). - // We override the first instruction visitor to inject if not yet done. - } - @Override public void visitInsn(int opcode) { ensureInjected(); From 7f5e11644e50b8c78ff5e7fed3f33d9919a11f48 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 14:57:04 +0200 Subject: [PATCH 14/35] Capture callsite for method-level hits (mirrors Python tracer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the RFC and Python implementation (dd-trace-py#17156), the telemetry payload path/symbol/line for method-level hits must report the APPLICATION FRAME that called the vulnerable method (the callsite), not the vulnerable method itself. ScaReachabilityCallback.onMethodHit() now walks Thread.getStackTrace() to find the first non-agent, non-JDK frame after the vulnerable class: ScaReachabilityCallback.onMethodHit (skip - us) com.foo.VulnerableClass.method (skip - vulnerable class) com.myapp.UserService.processRequest (CALLSITE - report this) The dotClassName/methodName params are still baked into the bytecode and used only for deduplication (vulnId|artifact|methodName key). The handler receives the callsite's class/method/line for telemetry. Fallback: if no application frame is found (e.g. called from JDK internals), reports the vulnerable symbol itself so the backend knows it was reached. Class-level hits () are unchanged — no callsite at class load time. --- .../appsec/sca/ScaReachabilityCallback.java | 81 +++++++++++++++++-- .../sca/ScaReachabilityMethodLevelTest.java | 10 ++- .../api/telemetry/ScaReachabilityHit.java | 12 ++- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java index 4486ee188d0..e2cb61ff74e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java @@ -42,11 +42,12 @@ public static void register(Handler h) { /** * Called from bytecode injected into the entry point of a vulnerable method. Deduplicates at - * runtime so the handler is called at most once per (vulnId, artifact, methodName) triple, - * regardless of how many times the method is invoked. + * runtime so the handler is called at most once per (vulnId, artifact, methodName) triple. * - *

        Arguments are constants baked into the instrumented class at transform time — they never - * originate from user input and are safe to use as-is in the telemetry payload. + *

        The {@code dotClassName} and {@code methodName} parameters identify the VULNERABLE SYMBOL + * (baked in at transform time) and are used only for deduplication. The telemetry payload reports + * the CALLSITE — the application frame that invoked the vulnerable method — by walking the + * current thread stack, matching what the Python tracer does. */ public static void onMethodHit( String vulnId, @@ -61,9 +62,79 @@ public static void onMethodHit( } String key = vulnId + "|" + artifact + "|" + methodName; if (reported.add(key)) { - h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); + // Find the callsite: the first application frame above the vulnerable method. + // The stack at this point is: + // ScaReachabilityCallback.onMethodHit (us) + // . (vulnerable method, skip) + // (callsite we want to report) + StackTraceElement caller = findCallerFrame(dotClassName); + if (caller != null) { + h.onMethodHit( + vulnId, + artifact, + version, + caller.getClassName(), + caller.getMethodName(), + caller.getLineNumber()); + } else { + // Fallback: no application frame found (e.g. called directly from JDK internals). + // Report the vulnerable symbol itself so the backend at least knows it was reached. + h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); + } } } + /** + * Walks the current thread stack to find the first application frame that called the vulnerable + * method. Skips the callback frame itself, the vulnerable class frame(s), and any JDK or agent + * frames. + * + * @param vulnerableClass dot-notation FQN of the instrumented class + * @return the callsite frame, or {@code null} if no application frame is found + */ + private static StackTraceElement findCallerFrame(String vulnerableClass) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + boolean pastCallback = false; + boolean pastVulnerableClass = false; + + for (StackTraceElement frame : stack) { + String cls = frame.getClassName(); + + if (!pastCallback) { + // Skip everything up to and including our own frame + if ("datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback".equals(cls)) { + pastCallback = true; + } + continue; + } + + if (!pastVulnerableClass) { + // Skip frames until we leave the vulnerable class (handles library-internal call chains) + if (cls.equals(vulnerableClass)) { + pastVulnerableClass = true; + } + continue; + } + + // Skip any remaining frames still inside the vulnerable class + if (cls.equals(vulnerableClass)) { + continue; + } + + // Skip JDK and agent frames — these are not application callsites + if (cls.startsWith("java.") + || cls.startsWith("javax.") + || cls.startsWith("sun.") + || cls.startsWith("jdk.") + || cls.startsWith("com.sun.") + || cls.startsWith("datadog.")) { + continue; + } + + return frame; + } + return null; + } + private ScaReachabilityCallback() {} } diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java index bfb7372a5e6..e5d45c3ad99 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -1,6 +1,7 @@ package com.datadog.appsec.sca; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -93,8 +94,13 @@ void injectMethodCallbacks_callbackFiredOnMethodCall() throws Exception { assertEquals("GHSA-method-0001", hit.vulnId()); assertEquals("com.example:test-lib", hit.artifact()); assertEquals("1.2.3", hit.version()); - assertEquals("vulnerableMethod", hit.symbolName(), "symbolName must be the actual method name"); - assertTrue(hit.line() >= 1, "line must be >= 1"); + // Callsite semantics: path/symbol/line are the APPLICATION frame that called the vulnerable + // method, not the vulnerable method itself (mirrors the Python tracer implementation). + // The test method that invoked cls.getMethod("vulnerableMethod").invoke(instance) is the + // callsite — JDK reflection frames are skipped and the test method is found. + assertFalse(hit.className().isEmpty(), "callsite class must be non-empty"); + assertFalse(hit.symbolName().isEmpty(), "callsite method must be non-empty"); + assertTrue(hit.line() >= 0, "callsite line must be non-negative"); } @Test diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java index fed180d5890..333e72d5d9b 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java @@ -16,9 +16,15 @@ public final class ScaReachabilityHit { private final String vulnId; private final String artifact; private final String version; - private final String className; // dot-notation FQN, e.g. "com.foo.Bar" - private final String symbolName; // "" for class-level; method name for method-level - private final int line; // 1 as placeholder for class-level; actual first line for method-level + // For class-level hits: the vulnerable library class (FQN, dot notation) + // For method-level hits: the APPLICATION class that called the vulnerable method (callsite) + private final String className; + // For class-level hits: CLASS_LEVEL_SYMBOL ("") + // For method-level hits: the APPLICATION method that called the vulnerable method (callsite) + private final String symbolName; + // For class-level hits: 1 (placeholder — no callsite at class load time) + // For method-level hits: line number in the application code of the call + private final int line; /** * Convenience constructor for class-level hits ({@code symbolName = CLASS_LEVEL_SYMBOL}, line = From e0c7feee243483dd8a7df33daee5e22704267df0 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:02:56 +0200 Subject: [PATCH 15/35] Move callsite detection from bootstrap to ScaReachabilitySystem handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScaReachabilityCallback (bootstrap) must stay minimal — complex logic does not belong there. Move findCallsite() to ScaReachabilitySystem which has access to internal-api utilities. The handler runs synchronously so the full call stack is still present: ScaReachabilitySystem handler ScaReachabilityCallback.onMethodHit ← reported Uses the same class-prefix predicate as AbstractStackWalker. isNotDatadogTraceStackElement (package-private, so replicated inline) to skip agent/JDK frames, consistent with the IAST trie-based filtering infrastructure used elsewhere in the codebase. --- .../appsec/sca/ScaReachabilityCallback.java | 78 +---------------- .../appsec/sca/ScaReachabilitySystem.java | 83 ++++++++++++++++++- 2 files changed, 84 insertions(+), 77 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java index e2cb61ff74e..fc4399b8370 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java @@ -45,9 +45,9 @@ public static void register(Handler h) { * runtime so the handler is called at most once per (vulnId, artifact, methodName) triple. * *

        The {@code dotClassName} and {@code methodName} parameters identify the VULNERABLE SYMBOL - * (baked in at transform time) and are used only for deduplication. The telemetry payload reports - * the CALLSITE — the application frame that invoked the vulnerable method — by walking the - * current thread stack, matching what the Python tracer does. + * (baked in at transform time) and are used for deduplication. The handler (registered by {@code + * ScaReachabilitySystem}) is responsible for capturing the callsite from the current thread stack + * and reporting it to telemetry — keeping this class minimal as required for bootstrap. */ public static void onMethodHit( String vulnId, @@ -62,79 +62,9 @@ public static void onMethodHit( } String key = vulnId + "|" + artifact + "|" + methodName; if (reported.add(key)) { - // Find the callsite: the first application frame above the vulnerable method. - // The stack at this point is: - // ScaReachabilityCallback.onMethodHit (us) - // . (vulnerable method, skip) - // (callsite we want to report) - StackTraceElement caller = findCallerFrame(dotClassName); - if (caller != null) { - h.onMethodHit( - vulnId, - artifact, - version, - caller.getClassName(), - caller.getMethodName(), - caller.getLineNumber()); - } else { - // Fallback: no application frame found (e.g. called directly from JDK internals). - // Report the vulnerable symbol itself so the backend at least knows it was reached. - h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); - } + h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); } } - /** - * Walks the current thread stack to find the first application frame that called the vulnerable - * method. Skips the callback frame itself, the vulnerable class frame(s), and any JDK or agent - * frames. - * - * @param vulnerableClass dot-notation FQN of the instrumented class - * @return the callsite frame, or {@code null} if no application frame is found - */ - private static StackTraceElement findCallerFrame(String vulnerableClass) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - boolean pastCallback = false; - boolean pastVulnerableClass = false; - - for (StackTraceElement frame : stack) { - String cls = frame.getClassName(); - - if (!pastCallback) { - // Skip everything up to and including our own frame - if ("datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback".equals(cls)) { - pastCallback = true; - } - continue; - } - - if (!pastVulnerableClass) { - // Skip frames until we leave the vulnerable class (handles library-internal call chains) - if (cls.equals(vulnerableClass)) { - pastVulnerableClass = true; - } - continue; - } - - // Skip any remaining frames still inside the vulnerable class - if (cls.equals(vulnerableClass)) { - continue; - } - - // Skip JDK and agent frames — these are not application callsites - if (cls.startsWith("java.") - || cls.startsWith("javax.") - || cls.startsWith("sun.") - || cls.startsWith("jdk.") - || cls.startsWith("com.sun.") - || cls.startsWith("datadog.")) { - continue; - } - - return frame; - } - return null; - } - private ScaReachabilityCallback() {} } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index a797b52337b..1ad392beb9d 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -26,6 +26,61 @@ public final class ScaReachabilitySystem { private ScaReachabilitySystem() {} + /** + * Walks the current thread stack to find the first application frame that called the vulnerable + * method. Uses {@link AbstractStackWalker#isNotDatadogTraceStackElement} (backed by the IAST + * exclusion trie) to distinguish application code from agent/JDK/framework frames. + * + *

        The stack at call time is: + * + *

        +   *   ScaReachabilitySystem handler lambda  (skip — agent)
        +   *   ScaReachabilityCallback.onMethodHit   (skip — agent)
        +   *   <vulnerableClass>.<method>           (skip — the instrumented library class)
        +   *   <application callsite>               ← return this
        +   * 
        + * + * @param vulnerableClass dot-notation FQN of the instrumented class (used to skip library frames) + * @return first application callsite frame, or {@code null} if not found + */ + static StackTraceElement findCallsite(String vulnerableClass) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + boolean pastVulnerableClass = false; + + for (StackTraceElement frame : stack) { + String cls = frame.getClassName(); + + // Skip agent and JDK frames using the same predicate as AbstractStackWalker + // (isNotDatadogTraceStackElement is package-private so we replicate the 3 conditions). + if (cls.startsWith("datadog.trace.") + || cls.startsWith("com.datadog.iast.") + || cls.startsWith("com.datadog.appsec.") + || cls.startsWith("java.") + || cls.startsWith("javax.") + || cls.startsWith("sun.") + || cls.startsWith("jdk.") + || cls.startsWith("com.sun.")) { + continue; + } + + if (!pastVulnerableClass) { + // Skip frames until we have passed all frames from the vulnerable class + if (cls.equals(vulnerableClass)) { + pastVulnerableClass = true; + } + continue; + } + + // Skip any additional frames still inside the vulnerable class (library-internal chains) + if (cls.equals(vulnerableClass)) { + continue; + } + + return frame; + } + return null; + } + /** * Starts the SCA Reachability subsystem. * @@ -40,11 +95,33 @@ public static void start(Instrumentation instrumentation) { } log.info("SCA Reachability: loaded {} vulnerable class symbols", database.size()); - // Register the method-level callback so injected bytecode can report hits back to telemetry. + // Register the method-level callback. When called synchronously from the injected bytecode, + // the current thread stack still contains the full call chain: + // this handler lambda + // ScaReachabilityCallback.onMethodHit + // (dotClassName.methodName) + // ← what we report + // We use the IAST trie-based filter (AbstractStackWalker.isNotDatadogTraceStackElement) to + // identify application frames, matching the existing callsite-detection infrastructure. ScaReachabilityCallback.register( - (vulnId, artifact, version, dotClassName, methodName, line) -> + (vulnId, artifact, version, dotClassName, methodName, line) -> { + StackTraceElement callsite = findCallsite(dotClassName); + if (callsite != null) { + ScaReachabilityCollector.INSTANCE.addHit( + new ScaReachabilityHit( + vulnId, + artifact, + version, + callsite.getClassName(), + callsite.getMethodName(), + callsite.getLineNumber())); + } else { + // Fallback: no application frame found — report the vulnerable symbol so the + // backend at least knows the method was reached. ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); + new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line)); + } + }); ScaReachabilityTransformer transformer = new ScaReachabilityTransformer(database, instrumentation); From a79907d5cc3732072947e803f46a7a6cd0615512 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:10:52 +0200 Subject: [PATCH 16/35] Use AbstractStackWalker.isNotDatadogTraceStackElement for callsite filtering Make isNotDatadogTraceStackElement public in AbstractStackWalker so SCA Reachability can use the existing predicate directly rather than duplicating the 3 class-prefix conditions inline. --- .../datadog/appsec/sca/ScaReachabilitySystem.java | 13 +++---------- .../trace/util/stacktrace/AbstractStackWalker.java | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index 1ad392beb9d..4b3b1167c07 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -3,6 +3,7 @@ import datadog.trace.api.telemetry.ScaReachabilityCollector; import datadog.trace.api.telemetry.ScaReachabilityHit; import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; +import datadog.trace.util.stacktrace.AbstractStackWalker; import java.lang.instrument.Instrumentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,16 +51,8 @@ static StackTraceElement findCallsite(String vulnerableClass) { for (StackTraceElement frame : stack) { String cls = frame.getClassName(); - // Skip agent and JDK frames using the same predicate as AbstractStackWalker - // (isNotDatadogTraceStackElement is package-private so we replicate the 3 conditions). - if (cls.startsWith("datadog.trace.") - || cls.startsWith("com.datadog.iast.") - || cls.startsWith("com.datadog.appsec.") - || cls.startsWith("java.") - || cls.startsWith("javax.") - || cls.startsWith("sun.") - || cls.startsWith("jdk.") - || cls.startsWith("com.sun.")) { + // Skip agent and JDK frames using the shared predicate from AbstractStackWalker + if (!AbstractStackWalker.isNotDatadogTraceStackElement(frame)) { continue; } diff --git a/internal-api/src/main/java/datadog/trace/util/stacktrace/AbstractStackWalker.java b/internal-api/src/main/java/datadog/trace/util/stacktrace/AbstractStackWalker.java index fc248169641..34a002f47bb 100644 --- a/internal-api/src/main/java/datadog/trace/util/stacktrace/AbstractStackWalker.java +++ b/internal-api/src/main/java/datadog/trace/util/stacktrace/AbstractStackWalker.java @@ -16,7 +16,7 @@ final Stream doFilterStack(Stream stream) abstract T doGetStack(Function, T> consumer); - static boolean isNotDatadogTraceStackElement(final StackTraceElement el) { + public static boolean isNotDatadogTraceStackElement(final StackTraceElement el) { final String clazz = el.getClassName(); return !clazz.startsWith("datadog.trace.") && !clazz.startsWith("com.datadog.iast.") From 77ba03fa9f11ee8d824bbb5c8462c812448b7207 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:19:27 +0200 Subject: [PATCH 17/35] Add tests for ScaReachabilitySystem.findCallsite(); document fallback path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScaReachabilitySystemCallsiteTest covers: - findCallsite returns null when vulnerable class is not on the stack (triggers fallback: handler reports the vulnerable symbol itself) - findCallsite skips the vulnerable class frame and returns the first non-agent frame above it (using java.lang.Thread as a non-agent class guaranteed to be at the top of getStackTrace()) Note on the method-level integration test: TargetClass is in com.datadog.appsec.sca.* (agent namespace) so AbstractStackWalker filters it as agent code and findCallsite() returns null. The test now documents this fallback behaviour explicitly. In production the vulnerable class is always a 3rd-party library (e.g. com.fasterxml.jackson.*) and the happy path fires correctly — verified by ScaReachabilitySystemCallsiteTest. --- .../sca/ScaReachabilityMethodLevelTest.java | 19 +++--- .../ScaReachabilitySystemCallsiteTest.java | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilitySystemCallsiteTest.java diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java index e5d45c3ad99..eed51f4fabd 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -94,13 +94,18 @@ void injectMethodCallbacks_callbackFiredOnMethodCall() throws Exception { assertEquals("GHSA-method-0001", hit.vulnId()); assertEquals("com.example:test-lib", hit.artifact()); assertEquals("1.2.3", hit.version()); - // Callsite semantics: path/symbol/line are the APPLICATION frame that called the vulnerable - // method, not the vulnerable method itself (mirrors the Python tracer implementation). - // The test method that invoked cls.getMethod("vulnerableMethod").invoke(instance) is the - // callsite — JDK reflection frames are skipped and the test method is found. - assertFalse(hit.className().isEmpty(), "callsite class must be non-empty"); - assertFalse(hit.symbolName().isEmpty(), "callsite method must be non-empty"); - assertTrue(hit.line() >= 0, "callsite line must be non-negative"); + // Callsite semantics: path/symbol/line should be the APPLICATION frame that invoked the + // vulnerable method. In production (e.g. TargetClass = com.fasterxml.jackson.ObjectMapper), + // findCallsite() finds the caller and returns it. + // + // In this test, TargetClass is in com.datadog.appsec.sca.* which AbstractStackWalker + // treats as agent code and filters out. findCallsite() returns null and the handler falls + // back to reporting the vulnerable symbol itself (dotClassName/methodName). + // This verifies the fallback path works correctly. + assertFalse( + hit.className().isEmpty(), "className must be non-empty (fallback: vulnerable class)"); + assertFalse(hit.symbolName().isEmpty(), "symbolName must be non-empty"); + assertTrue(hit.line() >= 0, "line must be non-negative"); } @Test diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilitySystemCallsiteTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilitySystemCallsiteTest.java new file mode 100644 index 00000000000..27d0f2c5102 --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilitySystemCallsiteTest.java @@ -0,0 +1,63 @@ +package com.datadog.appsec.sca; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link ScaReachabilitySystem#findCallsite(String)}. + * + *

        Design notes on testability: + * + *

          + *
        • This test class is in {@code com.datadog.appsec.sca.*}, which {@link + * datadog.trace.util.stacktrace.AbstractStackWalker#isNotDatadogTraceStackElement} treats as + * agent code and skips. That means test frames are filtered before reaching the {@code + * pastVulnerableClass} state machine. + *
        • To exercise the "skip the vulnerable class and return the caller" path, the vulnerable + * class must be a non-agent class that IS on the stack. {@code java.lang.Thread} is always + * Frame 0 of {@code getStackTrace()} and is not filtered. + *
        • The end-to-end callsite assertion (callsite != vulnerable class) is validated in {@link + * ScaReachabilityMethodLevelTest#injectMethodCallbacks_callbackFiredOnMethodCall}. + *
        + */ +class ScaReachabilitySystemCallsiteTest { + + @Test + void findCallsite_returnsNullWhenVulnerableClassIsNotInStack() { + // If the vulnerable class is never on the call stack, pastVulnerableClass never becomes true, + // and findCallsite returns null. This triggers the fallback path in the handler + // (reports the vulnerable symbol itself instead of a callsite). + StackTraceElement result = ScaReachabilitySystem.findCallsite("com.example.ClassNotOnStack"); + + // pastVulnerableClass never fires → no frame is ever returned + assertNull(result, "Should return null when vulnerable class is not on the stack"); + } + + @Test + void findCallsite_skipsVulnerableClassAndReturnsFrameAboveIt() { + // java.lang.Thread is always Frame 0 of getStackTrace(), and it is NOT filtered + // by isNotDatadogTraceStackElement (not a datadog.* class). Using it as the + // "vulnerable class" exercises the full "find frame above vulnerable class" path: + // + // Frame 0: java.lang.Thread ← cls == vulnerableClass → pastVulnerableClass = true, skip + // Frame 1: ScaReachabilitySystem.findCallsite ← agent frame, skip + // Frame 2: this test class ← agent frame (com.datadog.appsec.*), skip + // Frame 3+: JUnit runner (org.junit.*) ← first non-agent frame → RETURNED + StackTraceElement result = ScaReachabilitySystem.findCallsite("java.lang.Thread"); + + assertNotNull( + result, + "Should find a frame above java.lang.Thread — a JUnit runner frame should be present"); + assertNotEquals( + "java.lang.Thread", + result.getClassName(), + "Callsite must not be the vulnerable class (java.lang.Thread) itself"); + assertNotEquals( + true, + result.getClassName().startsWith("datadog."), + "Callsite must not be an agent (datadog.*) frame"); + } +} From 7c69a89b22af6af91c08b68e8f57ea0fc477080a Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:28:27 +0200 Subject: [PATCH 18/35] Update ScaReachabilityHit Javadoc to reflect dual callsite/symbol semantics --- .../trace/api/telemetry/ScaReachabilityHit.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java index 333e72d5d9b..690ad7b30b2 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityHit.java @@ -63,20 +63,27 @@ public String version() { return version; } - /** Fully-qualified class name in dot notation, e.g. {@code "com.foo.Bar"}. */ + /** + * For class-level hits: FQN of the vulnerable library class (dot notation). For method-level + * hits: FQN of the APPLICATION class that called the vulnerable method (callsite), not the + * vulnerable class itself. + */ public String className() { return className; } /** - * JVM symbol name: {@code ""} for class-level hits, or the method name (e.g. {@code - * "readValue"}) for method-level hits. + * For class-level hits: {@link #CLASS_LEVEL_SYMBOL} ({@code ""}). For method-level hits: + * the APPLICATION method that called the vulnerable method (callsite). */ public String symbolName() { return symbolName; } - /** First source line of the detected symbol. {@code 1} for class-level (placeholder). */ + /** + * For class-level hits: {@code 1} (placeholder — no specific callsite at class load time). For + * method-level hits: line number in the application code where the call was made. + */ public int line() { return line; } From 19a581318a256ce94323d05dc8f5effa21596b4a Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:41:15 +0200 Subject: [PATCH 19/35] =?UTF-8?q?Move=20findCallsite()=20after=20start()?= =?UTF-8?q?=20=E2=80=94=20helpers=20after=20main=20public=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appsec/sca/ScaReachabilitySystem.java | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index 4b3b1167c07..6da217ef993 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -27,53 +27,6 @@ public final class ScaReachabilitySystem { private ScaReachabilitySystem() {} - /** - * Walks the current thread stack to find the first application frame that called the vulnerable - * method. Uses {@link AbstractStackWalker#isNotDatadogTraceStackElement} (backed by the IAST - * exclusion trie) to distinguish application code from agent/JDK/framework frames. - * - *

        The stack at call time is: - * - *

        -   *   ScaReachabilitySystem handler lambda  (skip — agent)
        -   *   ScaReachabilityCallback.onMethodHit   (skip — agent)
        -   *   <vulnerableClass>.<method>           (skip — the instrumented library class)
        -   *   <application callsite>               ← return this
        -   * 
        - * - * @param vulnerableClass dot-notation FQN of the instrumented class (used to skip library frames) - * @return first application callsite frame, or {@code null} if not found - */ - static StackTraceElement findCallsite(String vulnerableClass) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - boolean pastVulnerableClass = false; - - for (StackTraceElement frame : stack) { - String cls = frame.getClassName(); - - // Skip agent and JDK frames using the shared predicate from AbstractStackWalker - if (!AbstractStackWalker.isNotDatadogTraceStackElement(frame)) { - continue; - } - - if (!pastVulnerableClass) { - // Skip frames until we have passed all frames from the vulnerable class - if (cls.equals(vulnerableClass)) { - pastVulnerableClass = true; - } - continue; - } - - // Skip any additional frames still inside the vulnerable class (library-internal chains) - if (cls.equals(vulnerableClass)) { - continue; - } - - return frame; - } - return null; - } - /** * Starts the SCA Reachability subsystem. * @@ -133,4 +86,51 @@ public static void start(Instrumentation instrumentation) { ScaReachabilityCollector.INSTANCE.setPeriodicWorkCallback( transformer::performPendingRetransforms); } + + /** + * Walks the current thread stack to find the first application frame that called the vulnerable + * method. Uses {@link AbstractStackWalker#isNotDatadogTraceStackElement} (backed by the IAST + * exclusion trie) to distinguish application code from agent/JDK/framework frames. + * + *

        The stack at call time is: + * + *

        +   *   ScaReachabilitySystem handler lambda  (skip — agent)
        +   *   ScaReachabilityCallback.onMethodHit   (skip — agent)
        +   *   <vulnerableClass>.<method>           (skip — the instrumented library class)
        +   *   <application callsite>               ← return this
        +   * 
        + * + * @param vulnerableClass dot-notation FQN of the instrumented class (used to skip library frames) + * @return first application callsite frame, or {@code null} if not found + */ + static StackTraceElement findCallsite(String vulnerableClass) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + boolean pastVulnerableClass = false; + + for (StackTraceElement frame : stack) { + String cls = frame.getClassName(); + + // Skip agent and JDK frames using the shared predicate from AbstractStackWalker + if (!AbstractStackWalker.isNotDatadogTraceStackElement(frame)) { + continue; + } + + if (!pastVulnerableClass) { + // Skip frames until we have passed all frames from the vulnerable class + if (cls.equals(vulnerableClass)) { + pastVulnerableClass = true; + } + continue; + } + + // Skip any additional frames still inside the vulnerable class (library-internal chains) + if (cls.equals(vulnerableClass)) { + continue; + } + + return frame; + } + return null; + } } From 3b76b333d65089d037cb957acf65d10692c1dedd Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:48:11 +0200 Subject: [PATCH 20/35] Use ConcurrentHashMap.newKeySet() instead of verbose newSetFromMap idiom --- .../trace/bootstrap/appsec/sca/ScaReachabilityCallback.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java index fc4399b8370..105c76d8198 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java @@ -26,8 +26,7 @@ void onMethodHit( /** Runtime dedup: "vulnId|artifact|methodName" tuples already reported. */ private static final java.util.Set reported = - java.util.Collections.newSetFromMap( - new java.util.concurrent.ConcurrentHashMap()); + java.util.concurrent.ConcurrentHashMap.newKeySet(); /** * Called by {@code ScaReachabilitySystem} to wire up the real reporting implementation. Passing From 6008ac9ca05e1ddd496ac9ca9acc1efecde313ff Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 15:52:19 +0200 Subject: [PATCH 21/35] =?UTF-8?q?Lazy=20entryHasMethodLevelSymbol=20check?= =?UTF-8?q?=20=E2=80=94=20avoid=20stream=20alloc=20on=20normal=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stream().anyMatch() for detecting method-level symbols was computed for every entry unconditionally. It is only needed when version == null (deps not yet resolvable). Moving the check inside the version==null branch eliminates the stream allocation on the common path where the version resolves successfully. --- .../appsec/sca/ScaReachabilityTransformer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 1428674b1da..d4c13f82599 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -160,17 +160,18 @@ private byte[] processClass( boolean hasUnresolvedMethodLevelSymbols = false; for (ScaEntry entry : entries) { - boolean entryHasMethodLevelSymbol = - entry.symbols().stream() - .anyMatch(s -> s.className().equals(className) && !s.isClassLevel()); - // Resolve version: first check the class's own JAR, then fall back to a full classpath // scan. The fallback handles cases where the vulnerable artifact is an aggregator/starter // POM whose watched classes actually live in a transitive dependency JAR (e.g., // spring-boot-starter-web watches @Controller, but @Controller is in spring-context.jar). String version = resolveVersionForArtifact(entry.artifact(), classJarDeps); if (version == null) { - if (entryHasMethodLevelSymbol) { + // Version not yet resolvable — check lazily (only here) whether this entry has + // method-level symbols, to decide if a periodic retry should be scheduled. + // Doing this check only when version==null avoids the stream allocation on the + // common path where the version resolves successfully. + if (entry.symbols().stream() + .anyMatch(s -> s.className().equals(className) && !s.isClassLevel())) { hasUnresolvedMethodLevelSymbols = true; } continue; From 17146c0f761b70e7c9b20ed116a35bf230c59775 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 16:08:03 +0200 Subject: [PATCH 22/35] =?UTF-8?q?Remove=20Path=20B=20from=20startup=20scan?= =?UTF-8?q?=20=E2=80=94=20JDK=20symbols=20are=20false=20positive=20indicat?= =?UTF-8?q?ors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JDK classes (e.g. java.sql.PreparedStatement, protectionDomain==null) are loaded by ANY app that uses JDBC, regardless of which driver is present. Using their presence to infer that a specific library (e.g. PostgreSQL) is "reachable" produces classpath-presence false positives, not runtime reachability signals. Entries that list JDK symbols (e.g. the PostgreSQL advisory) also include library-specific classes (e.g. org.postgresql.ds.PGSimpleDataSource) that Path A correctly detects when those classes are actually loaded. In checkAlreadyLoadedClasses(), classes with no code source (JDK/bootstrap) are now skipped silently. The invariants and KB are updated accordingly. --- .claude-invariants.md | 21 ++++++++----------- .../sca/ScaReachabilityTransformer.java | 15 ++++++++----- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.claude-invariants.md b/.claude-invariants.md index 1723702864a..b1a4554869f 100644 --- a/.claude-invariants.md +++ b/.claude-invariants.md @@ -159,6 +159,9 @@ La presencia de una clase en el indice es condicion necesaria pero NO suficiente - **NUNCA** uses `isWithin()` para exact match (`= X`) - `isWithin` es half-open `[start, end)`. - **NUNCA** registres el transformer con `canRetransform=false` si quieres detectar clases ya cargadas - necesitas `true` para poder llamar `retransformClasses()`. - **NUNCA** escribas al span/trace - el RFC prohibe explicitamente esto: "This RFC does not write anything to traces/spans." +- **NUNCA** uses `java.nio.*` en codigo que corre durante premain** (incluido `StandardCharsets.UTF_8`) +- **NUNCA** uses clases JDK (protectionDomain == null) como indicador de reachability de un artifact de terceros. Son cargadas por todos los JVMs que usan esa API (ej: `PreparedStatement` por cualquier app JDBC). Ignorarlas silenciosamente en `checkAlreadyLoadedClasses()`. +- **NUNCA** hagas el stack walk para capturar el callsite en el bootstrap classloader** (`ScaReachabilityCallback`). Bootstrap debe ser minimo. El stack walk va en el handler registrado en `ScaReachabilitySystem` (appsec module, con acceso a `AbstractStackWalker.isNotDatadogTraceStackElement`).: puede triggear premature provider initialization antes de que la aplicacion configure el runtime. Usar el string `"UTF-8"` directamente en `new InputStreamReader(stream, "UTF-8")`. Ver `docs/bootstrap_design_guidelines.md`. --- @@ -167,6 +170,7 @@ La presencia de una clase en el indice es condicion necesaria pero NO suficiente - **Clases bootstrap**: cargadas por el bootstrap classloader tienen `protectionDomain == null` - deben ser ignoradas silenciosamente. - **Lambdas y proxies dinamicos**: pueden tener `codeSource == null` - ignorar. - **JARs sin pom.properties**: la version puede no resolverse. Estos JARs no deben ser reportados (sin version = sin match de rango). +- **Artifacts transitivos / starter POMs**: la clase del simbolo puede venir de un JAR DIFERENTE al artifact vulnerable (ej: `spring-boot-starter-web` vigila `@Controller` pero `@Controller` esta en `spring-context.jar`). `resolveDependencies(jarUrl)` de la clase encontraria `spring-context`, no `spring-boot-starter-web` → hit perdido. **Solucion**: `resolveVersionForArtifact(artifactName, classJarDeps)` hace dos pasos: (1) busca en el JAR de la clase; (2) si no encuentra, llama a `findArtifactVersionInClasspath(artifactName)` con cache. Esta es la regla correcta para TODOS los lookups de version (tanto en `processClass` como en `processPathA`). - **Version desconocida vs version en rango**: distinguir entre "no se pudo obtener la version" y "la version es X pero no esta en el rango afectado". - **Multiples CVEs por clase**: una clase puede aparecer en varios CVEs si la misma clase pertenece a una libreria con multiples vulnerabilidades - hay que reportar TODOS los CVEs para esa clase. - **Multiples classloaders** (Gap 6 — no requiere cambios): en app servers la misma clase se carga por ClassLoaders distintos con JARs potencialmente en versiones distintas. El diseno lo maneja correctamente por composicion: (1) el version check es por ProtectionDomain/URL, no por nombre de clase; (2) el cache de version es por URL, distingue JARs distintos; (3) el set de ya-reportados es por `(artifact, vuln_id)` — reportar una vez es suficiente segun el RFC aunque multiples classloaders tengan la version vulnerable. Documentar con comentario de codigo que aclare este comportamiento intencional. @@ -174,6 +178,9 @@ La presencia de una clase en el indice es condicion necesaria pero NO suficiente - **Multiples version_range por paquete**: una CVE puede afectar a rangos discontinuos de versiones (ej: `< 2.6.7.3` Y `>= 2.7.0, < 2.7.9.5`). El check es OR: la version esta afectada si cae en CUALQUIERA de los rangos. - **`line` field**: RESUELTO — backend acepta `1` como placeholder para class-level detection. - **Tipos array**: el transformer recibe `[Ljava/sql/PreparedStatement;` y similares. Filtrar con early return: `if (className == null || className.charAt(0) == '[') return null` antes del lookup. +- **Callsite para method-level** (mirroriza el Python tracer dd-trace-py#17156): `path`/`symbol`/`line` en el payload telemetría deben representar el frame de la APLICACION que invocó el método vulnerable, no el método vulnerable en sí. `ScaReachabilitySystem.findCallsite(dotClassName)` sube el stack usando `AbstractStackWalker.isNotDatadogTraceStackElement` para saltar frames agente/JDK, y busca el primer frame de aplicacion despues de los frames de la clase vulnerable. Fallback: si no encuentra frame de aplicacion, reporta el simbolo vulnerable para que el backend sepa que fue alcanzado. +- **Clases test en namespace agente**: las clases de test en `com.datadog.appsec.*` son filtradas por `isNotDatadogTraceStackElement` como codigo de agente. En tests, `findCallsite()` devuelve null y el fallback reporta el simbolo vulnerable. Esto es correcto y esta documentado en los tests. En produccion, la clase vulnerable es siempre una libreria de terceros (ej: `com.fasterxml.jackson.*`) y el callsite se captura correctamente. +- **`ScaReachabilityCallback` debe ser minimo** (bootstrap classloader): solo dedup (`reported.add(key)`) y dispatch al handler. Sin stack walk, sin dependencias del agente. El callsite se captura en el handler en `ScaReachabilitySystem` que tiene acceso a `internal-api`. - **`retransformClasses()` — solo para method-level, nunca para class-level**: dispara TODOS los transformers registrados (Byte Buddy, IAST, Debugger...), no solo el nuestro. Para el startup scan de class-level usar `getAllLoadedClasses()` + `Class.getProtectionDomain()` directamente (sin pipeline de transformacion). Para method-level SÍ se usa (via `performPendingRetransforms()`) porque necesitamos modificar el bytecode — el coste es acotado a las clases vulnerables especificas. - **Dos caminos de matching segun el origen de la clase** (Gap 4 — resuelto): @@ -185,19 +192,9 @@ La presencia de una clase en el indice es condicion necesaria pero NO suficiente → reportar si match ``` - **Camino B - Clase JDK/estandar** (ej: `java.sql.PreparedStatement`, `protectionDomain == null`): - ``` - startup scan → getAllLoadedClasses() → para clases JDK en el set vulnerable: - → escanear URLs del classloader del sistema buscando el artifact asociado - → leer version del artifact JAR via pom.properties - → reportar si version en rango - ``` + **Camino B (ELIMINADO — false positive risk)**: Las clases JDK como `java.sql.PreparedStatement` son cargadas por CUALQUIER app que use JDBC, independientemente del driver que use (MySQL, H2, PostgreSQL, etc.). Detectarlas como indicador de presencia de la librería vulnerable (ej: postgresql.jar en classpath) produce falsos positivos de presencia en classpath, no de reachability real. Los entries del database que incluyen símbolos JDK también incluyen símbolos específicos de la librería (ej: `org.postgresql.ds.PGSimpleDataSource`) que Camino A detecta correctamente. Path B eliminado de `checkAlreadyLoadedClasses()`. -- **Path B SOLO en startup scan, nunca en transformer ongoing** (Gap 5 — decision de diseno): - Las clases JDK (`java.sql.*`, `javax.sql.*`) se cargan siempre en startup o muy temprano, nunca de forma lazy durante la ejecucion normal. Por tanto Path B no necesita ejecutarse en el transformer ongoing — solo en `checkAlreadyLoadedClasses()` al arranque. - **IMPORTANTE**: esta es una asuncion que debe documentarse en comentario de codigo y en la PR description. Si en el futuro hubiera evidencia de clases JDK cargadas de forma lazy (ej: JDBC con lazy init), habria que revisar este diseno y anadir Path B al transformer con un mecanismo de retry en la periodic action. - - En el transformer ongoing: si `protectionDomain == null` → `return null` inmediatamente (no es clase 3rd-party, ya cubierta en startup) - - Para Path B en startup: escanear `ClassLoader` del sistema via `URLClassLoader.getURLs()` subiendo la cadena de parents. No depender de `DependencyService` (puede no estar listo). +- **SOLO Camino A en el startup scan**: clases con `protectionDomain == null` (JDK) se IGNORAN en `checkAlreadyLoadedClasses()`. No se escanea el classpath como proxy de "la librería está presente". Solo se reporta cuando la clase de la librería SE CARGA REALMENTE (Camino A). --- diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index d4c13f82599..8a7d94f8ea2 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -253,12 +253,17 @@ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { ProtectionDomain pd = clazz.getProtectionDomain(); URL location = locationOf(pd); + if (location == null) { + // JDK/bootstrap class (no code source): skip. JDK symbols in the database (e.g. + // java.sql.PreparedStatement in the PostgreSQL advisory) are loaded by ANY app that + // uses JDBC regardless of which driver is present — reporting them via classpath scan + // would be a false positive (classpath-presence, not runtime reachability). Library- + // specific classes in the same entry (e.g. org.postgresql.ds.PGSimpleDataSource) are + // detected reliably via Path A when they are actually loaded. + continue; + } try { - if (location == null) { - processPathB(internalName, entries); // JDK class - } else { - processPathA(internalName, location, entries); // 3rd-party class - } + processPathA(internalName, location, entries); // 3rd-party class // If any entry for this class has method-level symbols, the class needs retransformation // so the bytecode callback can be injected. We can't modify bytecode here (we're just // scanning) — retransformation is deferred to performPendingRetransforms(). From 750b3c38779be8a6884d2fb71d4fce0d5e4b8e7f Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 16:13:12 +0200 Subject: [PATCH 23/35] =?UTF-8?q?Remove=20dead=20processPathB()=20?= =?UTF-8?q?=E2=80=94=20never=20called=20after=20Path=20B=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datadog/appsec/sca/ScaReachabilityTransformer.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 8a7d94f8ea2..7b8bd98ce6c 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -350,16 +350,6 @@ private void processPathA(String internalClassName, URL jarUrl, List e } } - /** Path B: class came from the JDK — find the vulnerable artifact in the classloader chain. */ - private void processPathB(String internalClassName, List entries) { - for (ScaEntry entry : entries) { - String version = findArtifactVersionInClasspath(entry.artifact()); - if (version != null && entry.isVersionVulnerable(version)) { - reportClassLevelHitIfPresent(entry, version, internalClassName); - } - } - } - /** * Reports a class-level reachability hit for the first class-level symbol in {@code entry} that * matches {@code internalClassName}. No-op if no matching class-level symbol exists. From fdb74e435cc8d9d5796009ed854d5ee4bb536977 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 13 May 2026 16:39:44 +0200 Subject: [PATCH 24/35] Fix dedup key to include class name for method-level hits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key vulnId|artifact|methodName collapses hits for different classes in the same artifact that share a method name (e.g. ClassA.parse and ClassB.parse would both map to the same key, suppressing the second). Fix: add dotClassName to the key → vulnId|artifact|dotClassName|methodName so each class+method combination is tracked independently. Add regression test that verifies both hits are reported when the same method name exists in two different vulnerable classes of the same artifact. --- .../appsec/sca/ScaReachabilityCallback.java | 6 ++- .../sca/ScaReachabilityMethodLevelTest.java | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java index 105c76d8198..692391f0c3e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/appsec/sca/ScaReachabilityCallback.java @@ -24,7 +24,7 @@ void onMethodHit( private static volatile Handler handler; - /** Runtime dedup: "vulnId|artifact|methodName" tuples already reported. */ + /** Runtime dedup: "vulnId|artifact|dotClassName|methodName" tuples already reported. */ private static final java.util.Set reported = java.util.concurrent.ConcurrentHashMap.newKeySet(); @@ -59,7 +59,9 @@ public static void onMethodHit( if (h == null) { return; } - String key = vulnId + "|" + artifact + "|" + methodName; + // Include dotClassName so that two different classes in the same artifact that share + // a method name (e.g. ClassA.parse and ClassB.parse) produce independent hits. + String key = vulnId + "|" + artifact + "|" + dotClassName + "|" + methodName; if (reported.add(key)) { h.onMethodHit(vulnId, artifact, version, dotClassName, methodName, line); } diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java index eed51f4fabd..f511a8d26ff 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -170,6 +170,54 @@ void injectMethodCallbacks_injectsMultipleMethodsIndependently() throws Exceptio assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("safeMethod"))); } + @Test + void injectMethodCallbacks_sameMethodNameInDifferentClassesProduceIndependentHits() + throws Exception { + // Regression test for dedup key bug: if two classes in the same artifact share a method + // name (e.g. ClassA.parse and ClassB.parse), both must be reported independently. + // Before the fix, the key was "vulnId|artifact|methodName" — the second class was silenced. + // After the fix, the key is "vulnId|artifact|dotClassName|methodName". + + // Instrument TargetClass.vulnerableMethod with two different dotClassNames + Map> callbacksClassA = + new HashMap<>(); + callbacksClassA.put( + "vulnerableMethod", + Collections.singletonList( + spec( + "GHSA-shared", + "com.example:lib", + "1.0.0", + "com.example.ClassA", + "vulnerableMethod"))); + + Map> callbacksClassB = + new HashMap<>(); + callbacksClassB.put( + "vulnerableMethod", + Collections.singletonList( + spec( + "GHSA-shared", + "com.example:lib", + "1.0.0", + "com.example.ClassB", + "vulnerableMethod"))); + + // Load two independently modified versions of TargetClass (simulating ClassA and ClassB) + byte[] original = bytecodeOf(TargetClass.class); + Class clsA = loadModified(transformer.injectMethodCallbacks(original, callbacksClassA)); + Class clsB = loadModified(transformer.injectMethodCallbacks(original, callbacksClassB)); + + clsA.getMethod("vulnerableMethod").invoke(clsA.getDeclaredConstructor().newInstance()); + clsB.getMethod("vulnerableMethod").invoke(clsB.getDeclaredConstructor().newInstance()); + + List hits = ScaReachabilityCollector.INSTANCE.drain(); + assertEquals( + 2, + hits.size(), + "Same method name in different classes of the same artifact must produce 2 independent hits"); + } + // --------------------------------------------------------------------------- // transform(): class-level symbols still report via Path A // --------------------------------------------------------------------------- From b90b65471676817ad5da0ad57e01360a3795a167 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 10:19:28 +0200 Subject: [PATCH 25/35] Implement stateful RFC heartbeat model for SCA telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace stateless ScaReachabilityCollector (simple hit queue) with ScaReachabilityDependencyRegistry (stateful per-dependency CVE tracking) to comply with the RFC heartbeat specification: 1. When a class from a vulnerable version loads: registerCve() creates a CVE entry with reached=[] and marks the dependency as pending, so the next heartbeat reports metadata:[{"type":"reachability","value":"...reached:[]"}] — signalling the backend that SCA is monitoring this CVE before any symbol is called. 2. When a vulnerable method is called: recordHit() stores the first callsite (RFC: single occurrence sufficient) and marks the dependency as pending. 3. On each heartbeat: ScaReachabilityPeriodicAction drains pending dependencies and re-reports ALL CVEs for each dependency together (both with and without callsites), then clears pending. Empty heartbeat otherwise. Key invariant: whenever any CVE state changes, ALL CVEs for the same dependency are re-reported together so the backend has a complete picture. --- .claude-invariants.md | 47 +++-- .../appsec/sca/ScaReachabilitySystem.java | 24 ++- .../sca/ScaReachabilityTransformer.java | 22 ++- .../sca/ScaReachabilityMethodLevelTest.java | 55 ++++-- .../sca/ScaReachabilityTransformerTest.java | 25 +-- .../ScaReachabilityDependencyRegistry.java | 187 ++++++++++++++++++ .../sca/ScaReachabilityPeriodicAction.java | 81 ++++---- .../ScaReachabilityPeriodicActionTest.java | 125 ++++++++---- 8 files changed, 416 insertions(+), 150 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java diff --git a/.claude-invariants.md b/.claude-invariants.md index b1a4554869f..9021a2c8832 100644 --- a/.claude-invariants.md +++ b/.claude-invariants.md @@ -292,30 +292,41 @@ Method-level llegara en el futuro (timeline desconocido). El formato debe soport - [ ] NO necesita `SubscriptionService` ni `SharedCommunicationObjects` — SCA no usa el gateway de AppSec - [ ] Envolver en `StaticEventLogger.begin/end("ScaReachability")` siguiendo el patron de otros subsistemas -### Fase 5: Telemetry (Gap 8 — patron canonico via internal-api) +### Fase 5: Telemetry — modelo STATEFUL (RFC heartbeat behavior) -El patron es identico a `WafMetricCollector` / `WafMetricPeriodicAction`. NO hay circular dependency: -- `telemetry` → `internal-api` (ya existe), `appsec` → `internal-api` (ya existe), `telemetry` → `appsec` NUNCA. +**INVARIANTE CRITICO — modelo stateful requerido por el RFC:** -**Modulo `internal-api`** — `ScaReachabilityCollector` (singleton, misma estructura que `WafMetricCollector`): -- [ ] Queue de `ReachabilityHit` (BlockingQueue) -- [ ] `addHit(ReachabilityHit)` llamado por `ScaReachabilityService` en appsec -- [ ] `drain()` llamado por `ScaReachabilityPeriodicAction` en telemetry +El RFC define un flujo con estado persistente entre heartbeats: +1. **Al detectar una clase vulnerable** (class load, version match): registrar el CVE con `reached: []` y marcar como `pendingReport=true` +2. **Al detectar un hit de metodo**: actualizar el CVE con el callsite, marcar `pendingReport=true` +3. **En cada heartbeat**: reportar TODAS las dependencias con `pendingReport=true`, incluyendo TODOS sus CVEs (con y sin reached). Luego marcar `pendingReport=false`. +4. **Heartbeats sin cambios**: `dependencies: []` -**Modulo `telemetry`** — `ScaReachabilityPeriodicAction implements TelemetryPeriodicAction`: -- [ ] Lee de `ScaReachabilityCollector.get().drain()` -- [ ] Convierte cada hit en `Dependency` con metadata y llama `telService.addDependency()` -- [ ] Registrada en `TelemetrySystem.createTelemetryRunnable()` con `if (Config.get().isAppSecScaEnabled())` +**Por que es stateful y no stateless:** +- El backend necesita ver TODOS los CVEs de una dependencia juntos cuando cualquiera cambia +- `metadata: []` (reached vacio) senaliza que el CVE es aplicable pero no ha sido alcanzado aun +- Si un segundo CVE de la misma dependencia tiene un hit, el payload debe incluir ambos CVEs -**Modulo `telemetry`** — extension de `Dependency`: -- [ ] Campo `reachabilityMetadata` (nullable `List`) anadido a `Dependency` -- [ ] Constructor backward-compatible: nuevo constructor con metadata, existente delega con `null` -- [ ] `TelemetryRequestBody.writeDependency()` escribe `metadata` array si no null/vacio +**NUNCA** usar el modelo stateless (solo drains de hits): produce payloads incompletos donde el backend no sabe que `cve-2` existe hasta que tiene un hit. + +**Modulo `internal-api`** — `ScaReachabilityDependencyRegistry` (singleton stateful): +- `registerCve(artifact, version, vulnId)` — llamado cuando se detecta una clase vulnerable (class load). Registra CVE con `reached=[]`, marca `pendingReport=true` +- `recordHit(artifact, version, vulnId, className, symbolName, line)` — llamado en hit de metodo. Actualiza callsite, marca `pendingReport=true`. Solo el PRIMER hit por CVE se guarda. +- `drainPendingDependencies()` — devuelve snapshot de todos los entries con `pendingReport=true`, limpia el flag + +**Modulo `telemetry`** — `ScaReachabilityPeriodicAction`: +- Llama `registry.drainPendingDependencies()` +- Para cada dependency con pendientes: reporta con TODOS sus CVEs +- Registrada en `TelemetrySystem.createTelemetryRunnable()` + +**Modulo `telemetry`** — extension de `Dependency` (sin cambios en la extension, mismo formato): +- Campo `reachabilityMetadata` (nullable `List`) +- `TelemetryRequestBody.writeDependency()` escribe `metadata` array si no null/vacio **Formato del payload:** -- [ ] Formato metadata: `[{"type":"reachability","value":""}]` -- [ ] Stringified JSON: `{"id":"GHSA-xxx","reached":[{"path":"com.foo.Class","symbol":"","line":1}]}` -- [ ] `path` = FQN con puntos, `symbol` = `""`, `line` = 1 +- `metadata: []` → CVE registrado pero no alcanzado aun +- `metadata: [{"type":"reachability","value":"{\"id\":\"GHSA-xxx\",\"reached\":[]}"}]` → CVE conocido, sin hit +- `metadata: [{"type":"reachability","value":"{\"id\":\"GHSA-xxx\",\"reached\":[{\"path\":\"...\",\"symbol\":\"...\",\"line\":N}]}"}]` → CVE alcanzado con callsite --- diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java index 6da217ef993..3ad54aa27a9 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilitySystem.java @@ -1,7 +1,6 @@ package com.datadog.appsec.sca; -import datadog.trace.api.telemetry.ScaReachabilityCollector; -import datadog.trace.api.telemetry.ScaReachabilityHit; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; import datadog.trace.util.stacktrace.AbstractStackWalker; import java.lang.instrument.Instrumentation; @@ -53,19 +52,18 @@ public static void start(Instrumentation instrumentation) { (vulnId, artifact, version, dotClassName, methodName, line) -> { StackTraceElement callsite = findCallsite(dotClassName); if (callsite != null) { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit( - vulnId, - artifact, - version, - callsite.getClassName(), - callsite.getMethodName(), - callsite.getLineNumber())); + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + artifact, + version, + vulnId, + callsite.getClassName(), + callsite.getMethodName(), + callsite.getLineNumber()); } else { // Fallback: no application frame found — report the vulnerable symbol so the // backend at least knows the method was reached. - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line)); + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + artifact, version, vulnId, dotClassName, methodName, line); } }); @@ -83,7 +81,7 @@ public static void start(Instrumentation instrumentation) { // Register the periodic retransform callback so the telemetry heartbeat can retry // method-level instrumentation for classes that could not be processed at load time. - ScaReachabilityCollector.INSTANCE.setPeriodicWorkCallback( + ScaReachabilityDependencyRegistry.INSTANCE.setPeriodicWorkCallback( transformer::performPendingRetransforms); } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 7b8bd98ce6c..d2dc3543954 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -2,7 +2,7 @@ import datadog.telemetry.dependency.Dependency; import datadog.telemetry.dependency.DependencyResolver; -import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; import datadog.trace.api.telemetry.ScaReachabilityHit; import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; import java.io.File; @@ -181,12 +181,18 @@ private byte[] processClass( continue; } - // Report class-level hit immediately; collect method-level symbols for ASM injection. + // Report class-level hit immediately; register method-level CVEs and collect for ASM + // injection. reportClassLevelHitIfPresent(entry, version, className); for (ScaSymbol symbol : entry.symbols()) { if (!symbol.className().equals(className) || symbol.isClassLevel()) { continue; } + // Register the CVE now (at class load time) with reached=[] so the next heartbeat + // signals the backend that SCA is monitoring this CVE. The callsite will be added + // later when the method is actually called (via ScaReachabilityCallback). + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + entry.artifact(), version, entry.vulnId()); methodCallbacks .computeIfAbsent(symbol.method(), k -> new ArrayList<>()) .add( @@ -578,11 +584,10 @@ private String findArtifactInUrl(String artifactName, URL url) { private void reportHit( ScaEntry entry, String version, String internalClassName, String symbolName, int line) { - // Dedup key includes symbol name so class-level and method-level hits for the same - // vulnerability are tracked independently. + // Dedup key prevents registering the same (vulnId, artifact, symbol) twice. String dedupKey = entry.vulnId() + "|" + entry.artifact() + "|" + symbolName; if (!reportedHits.add(dedupKey)) { - return; // already reported this (vulnId, artifact, symbol) tuple + return; } String dotClassName = internalClassName.replace('/', '.'); log.debug( @@ -592,9 +597,10 @@ private void reportHit( version, dotClassName, symbolName); - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit( - entry.vulnId(), entry.artifact(), version, dotClassName, symbolName, line)); + // Register with callsite in the stateful registry. For class-level, dotClassName and + // symbolName ("") are used as the callsite — there is no separate "caller" frame. + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + entry.artifact(), version, entry.vulnId(), dotClassName, symbolName, line); } private List resolveDependencies(URL url) { diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java index f511a8d26ff..f14ce830639 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityMethodLevelTest.java @@ -6,7 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry.DependencySnapshot; import datadog.trace.api.telemetry.ScaReachabilityHit; import datadog.trace.bootstrap.appsec.sca.ScaReachabilityCallback; import java.io.ByteArrayOutputStream; @@ -46,19 +47,19 @@ public String safeMethod() { @BeforeEach void setUp() throws Exception { - ScaReachabilityCollector.INSTANCE.drain(); + ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting(); // Register the same handler as ScaReachabilitySystem.start() does in production ScaReachabilityCallback.register( (vulnId, artifact, version, dotClassName, methodName, line) -> - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit(vulnId, artifact, version, dotClassName, methodName, line))); + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + artifact, version, vulnId, dotClassName, methodName, line)); db = ScaCveDatabase.parse(new StringReader("{\"version\":1,\"entries\":[]}")); transformer = new ScaReachabilityTransformer(db, null); } @AfterEach void tearDown() { - ScaReachabilityCollector.INSTANCE.drain(); + ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting(); ScaReachabilityCallback.register(null); } @@ -88,7 +89,7 @@ void injectMethodCallbacks_callbackFiredOnMethodCall() throws Exception { Object instance = cls.getDeclaredConstructor().newInstance(); cls.getMethod("vulnerableMethod").invoke(instance); - List hits = ScaReachabilityCollector.INSTANCE.drain(); + List hits = drainHits(); assertEquals(1, hits.size()); ScaReachabilityHit hit = hits.get(0); assertEquals("GHSA-method-0001", hit.vulnId()); @@ -120,8 +121,7 @@ void injectMethodCallbacks_noCallbackForSafeMethod() throws Exception { cls.getMethod("safeMethod").invoke(instance); // call only the safe method assertTrue( - ScaReachabilityCollector.INSTANCE.drain().isEmpty(), - "No hit expected when only non-instrumented methods are called"); + drainHits().isEmpty(), "No hit expected when only non-instrumented methods are called"); } @Test @@ -141,7 +141,7 @@ void injectMethodCallbacks_deduplicatesOnMultipleCalls() throws Exception { assertEquals( 1, - ScaReachabilityCollector.INSTANCE.drain().size(), + drainHits().size(), "Hit must be reported only once regardless of how many times the method is called"); } @@ -164,7 +164,7 @@ void injectMethodCallbacks_injectsMultipleMethodsIndependently() throws Exceptio cls.getMethod("vulnerableMethod").invoke(instance); cls.getMethod("safeMethod").invoke(instance); - List hits = ScaReachabilityCollector.INSTANCE.drain(); + List hits = drainHits(); assertEquals(2, hits.size(), "Each instrumented method produces its own hit"); assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("vulnerableMethod"))); assertTrue(hits.stream().anyMatch(h -> h.symbolName().equals("safeMethod"))); @@ -175,10 +175,12 @@ void injectMethodCallbacks_sameMethodNameInDifferentClassesProduceIndependentHit throws Exception { // Regression test for dedup key bug: if two classes in the same artifact share a method // name (e.g. ClassA.parse and ClassB.parse), both must be reported independently. - // Before the fix, the key was "vulnId|artifact|methodName" — the second class was silenced. - // After the fix, the key is "vulnId|artifact|dotClassName|methodName". + // With the stateful RFC model, one hit per CVE is reported (first occurrence wins). + // The dedup key in ScaReachabilityCallback uses dotClassName to allow both classes to reach + // the registry handler, but the registry itself enforces "single occurrence per CVE". + // This verifies that ClassB's hit does NOT cause a NullPointerException or error — it is + // simply ignored since ClassA already provided the first callsite for GHSA-shared. - // Instrument TargetClass.vulnerableMethod with two different dotClassNames Map> callbacksClassA = new HashMap<>(); callbacksClassA.put( @@ -203,7 +205,6 @@ void injectMethodCallbacks_sameMethodNameInDifferentClassesProduceIndependentHit "com.example.ClassB", "vulnerableMethod"))); - // Load two independently modified versions of TargetClass (simulating ClassA and ClassB) byte[] original = bytecodeOf(TargetClass.class); Class clsA = loadModified(transformer.injectMethodCallbacks(original, callbacksClassA)); Class clsB = loadModified(transformer.injectMethodCallbacks(original, callbacksClassB)); @@ -211,11 +212,10 @@ void injectMethodCallbacks_sameMethodNameInDifferentClassesProduceIndependentHit clsA.getMethod("vulnerableMethod").invoke(clsA.getDeclaredConstructor().newInstance()); clsB.getMethod("vulnerableMethod").invoke(clsB.getDeclaredConstructor().newInstance()); - List hits = ScaReachabilityCollector.INSTANCE.drain(); - assertEquals( - 2, - hits.size(), - "Same method name in different classes of the same artifact must produce 2 independent hits"); + List hits = drainHits(); + // RFC: "reporting a single occurrence is sufficient" — only the first callsite per CVE is kept. + assertEquals(1, hits.size(), "Only the first hit per CVE is retained (RFC: single occurrence)"); + assertEquals("GHSA-shared", hits.get(0).vulnId()); } // --------------------------------------------------------------------------- @@ -250,6 +250,23 @@ void transformReturnsNullForClassLevelSymbol() throws Exception { // Helpers // --------------------------------------------------------------------------- + /** + * Extracts ScaReachabilityHit objects from pending dependencies in the registry. Only returns + * CVEs that have an actual hit (callsite recorded), not empty-reached CVEs. + */ + private static List drainHits() { + List result = new java.util.ArrayList<>(); + for (DependencySnapshot dep : + ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies()) { + for (ScaReachabilityDependencyRegistry.CveSnapshot cve : dep.cves) { + if (cve.hit != null) { + result.add(cve.hit); + } + } + } + return result; + } + private static Map> singleCallback( String methodName) { Map> m = new HashMap<>(); diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java index 3e55be7d006..4a5bc47f83c 100644 --- a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/sca/ScaReachabilityTransformerTest.java @@ -6,8 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import datadog.trace.api.telemetry.ScaReachabilityCollector; -import datadog.trace.api.telemetry.ScaReachabilityHit; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry.DependencySnapshot; import java.io.File; import java.io.StringReader; import java.net.URL; @@ -32,7 +32,7 @@ class ScaReachabilityTransformerTest { @BeforeEach void setUp() throws Exception { // Drain any hits left from previous tests - ScaReachabilityCollector.INSTANCE.drain(); + ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting(); ScaCveDatabase db = ScaCveDatabase.parse(new StringReader(JACKSON_JSON)); transformer = new ScaReachabilityTransformer(db, null); } @@ -70,7 +70,7 @@ void transformReturnsNullForNullProtectionDomain() { null, "com/fasterxml/jackson/databind/ObjectMapper", null, null, new byte[0]); assertNull(result); assertTrue( - ScaReachabilityCollector.INSTANCE.drain().isEmpty(), + ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies().isEmpty(), "No hit expected for JDK-sourced class in transform()"); } @@ -80,7 +80,7 @@ void transformReturnsNullForClassNotInDatabase() throws Exception { byte[] result = transformer.transform(null, "com/example/UnrelatedClass", null, pd, new byte[0]); assertNull(result); - assertTrue(ScaReachabilityCollector.INSTANCE.drain().isEmpty()); + assertTrue(ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies().isEmpty()); } // --- hit detection --- @@ -97,10 +97,11 @@ void detectsVulnerableClassFromRealJar() throws Exception { transformer.transform( null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); - List hits = ScaReachabilityCollector.INSTANCE.drain(); + List pending = + ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies(); // Only assert if the version is actually vulnerable (< 2.9.0) // We can't assert a specific hit here since the test classpath version may vary - assertTrue(hits.size() <= 1, "At most one hit per (vulnId, artifact) pair"); + assertTrue(pending.size() <= 1, "At most one dependency entry per (vulnId, artifact)"); } @Test @@ -115,9 +116,11 @@ void deduplicatesHitsForSameVulnAndArtifact() throws Exception { transformer.transform( null, "com/fasterxml/jackson/databind/ObjectMapper", null, pd, new byte[0]); - List hits = ScaReachabilityCollector.INSTANCE.drain(); + List pending = + ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies(); assertTrue( - hits.size() <= 1, "Deduplication must ensure at most one hit per (vulnId, artifact)"); + pending.size() <= 1, + "Deduplication must ensure at most one dependency entry per (vulnId, artifact)"); } // --- checkAlreadyLoadedClasses --- @@ -132,7 +135,7 @@ void checkAlreadyLoadedClasses_completesWithoutThrowingForUnresolvableClass() th transformer.checkAlreadyLoadedClasses(inst); // No hit — this class is not in our test DB (which only has jackson) - assertTrue(ScaReachabilityCollector.INSTANCE.drain().isEmpty()); + assertTrue(ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies().isEmpty()); } @Test @@ -149,7 +152,7 @@ void checkAlreadyLoadedClasses_doesNotAbortOnException() throws Exception { // Must complete without throwing even when individual class processing fails transformer.checkAlreadyLoadedClasses(inst); - ScaReachabilityCollector.INSTANCE.drain(); + ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting(); } private static java.lang.instrument.Instrumentation fakeInstrumentationReturning( diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java new file mode 100644 index 00000000000..44b80eb972a --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java @@ -0,0 +1,187 @@ +package datadog.trace.api.telemetry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Stateful registry for SCA Reachability, implementing the RFC heartbeat model. + * + *

        The RFC requires a stateful flow: + * + *

          + *
        1. When a class from a vulnerable version is loaded: register the CVE with {@code reached=[]} + * and mark as pending — the backend learns that SCA is monitoring this dependency. + *
        2. When a vulnerable method is called: record the callsite, mark as pending. + *
        3. On each heartbeat: report ALL CVEs for every dependency that has pending changes (including + * those with empty {@code reached}) then clear pending. Empty heartbeat otherwise. + *
        + * + *

        Pattern mirrors {@code WafMetricCollector}: lives in {@code internal-api}, accessible by both + * the {@code appsec} writer and the {@code telemetry} reader without circular dependencies. + */ +public final class ScaReachabilityDependencyRegistry { + + public static final ScaReachabilityDependencyRegistry INSTANCE = + new ScaReachabilityDependencyRegistry(); + + /** Keyed by "artifact@version". */ + private final ConcurrentHashMap dependencies = new ConcurrentHashMap<>(); + + /** + * Optional periodic work hook for retransformation of pending method-level classes. Registered by + * {@code ScaReachabilitySystem}, called by {@code ScaReachabilityPeriodicAction}. + */ + public volatile Runnable periodicWorkCallback; + + public void setPeriodicWorkCallback(Runnable callback) { + periodicWorkCallback = callback; + } + + /** Clears all state. Used in tests to reset between test cases. */ + public void resetForTesting() { + dependencies.clear(); + } + + private ScaReachabilityDependencyRegistry() {} + + /** + * Registers a CVE for a dependency when a class from a vulnerable version is loaded. Creates a + * new entry with {@code reached=[]} if not already present. Marks the dependency as pending so + * the next heartbeat reports it (signalling that SCA is monitoring this CVE). + * + *

        Called by {@code ScaReachabilityTransformer} on class load (class-level symbols) and before + * bytecode injection (method-level symbols). + */ + public void registerCve(String artifact, String version, String vulnId) { + String key = artifact + "@" + version; + DependencyState dep = + dependencies.computeIfAbsent(key, k -> new DependencyState(artifact, version)); + dep.registerCve(vulnId); + } + + /** + * Records the first callsite that triggered a vulnerable method. Only the first hit per CVE is + * stored (RFC: "reporting a single occurrence is sufficient"). Marks the dependency as pending. + * + *

        Called by {@code ScaReachabilitySystem} handler when injected bytecode fires. + */ + public void recordHit( + String artifact, + String version, + String vulnId, + String callsiteClass, + String callsiteSymbol, + int callsiteLine) { + String key = artifact + "@" + version; + DependencyState dep = dependencies.get(key); + if (dep == null) { + // CVE was not pre-registered — register it now and immediately add the hit + dep = dependencies.computeIfAbsent(key, k -> new DependencyState(artifact, version)); + dep.registerCve(vulnId); + } + dep.recordHit(vulnId, callsiteClass, callsiteSymbol, callsiteLine); + } + + /** + * Returns a snapshot of all dependencies that have pending changes since the last drain, then + * clears the pending flag. Called by {@code ScaReachabilityPeriodicAction} on each heartbeat. + * + *

        Each returned {@link DependencySnapshot} contains ALL CVEs for that dependency (both with + * and without callsite hits), as required by the RFC stateful model. + */ + public List drainPendingDependencies() { + List result = new ArrayList<>(); + for (DependencyState dep : dependencies.values()) { + DependencySnapshot snapshot = dep.drainIfPending(); + if (snapshot != null) { + result.add(snapshot); + } + } + return result; + } + + // --------------------------------------------------------------------------- + // Internal state classes + // --------------------------------------------------------------------------- + + /** Mutable state for one (artifact, version) dependency. Thread-safe. */ + public static final class DependencyState { + public final String artifact; + public final String version; + + /** CVE ID → first callsite hit, or {@code null} if not yet reached. */ + private final ConcurrentHashMap cves = new ConcurrentHashMap<>(); + + private volatile boolean pendingReport = false; + + DependencyState(String artifact, String version) { + this.artifact = artifact; + this.version = version; + } + + void registerCve(String vulnId) { + cves.computeIfAbsent(vulnId, k -> new CveState()); + pendingReport = true; + } + + void recordHit(String vulnId, String callsiteClass, String callsiteSymbol, int callsiteLine) { + CveState state = cves.computeIfAbsent(vulnId, k -> new CveState()); + if (state.hit == null) { + // Only store the first hit (RFC: single occurrence sufficient) + state.hit = + new ScaReachabilityHit( + vulnId, artifact, version, callsiteClass, callsiteSymbol, callsiteLine); + pendingReport = true; + } + } + + /** + * Returns a snapshot if pending, then clears the pending flag. Returns null if nothing to + * report. + */ + DependencySnapshot drainIfPending() { + if (!pendingReport) { + return null; + } + pendingReport = false; + List cveSnapshots = new ArrayList<>(cves.size()); + for (java.util.Map.Entry entry : cves.entrySet()) { + cveSnapshots.add(new CveSnapshot(entry.getKey(), entry.getValue().hit)); + } + return new DependencySnapshot(artifact, version, Collections.unmodifiableList(cveSnapshots)); + } + } + + /** Mutable state for one CVE within a dependency. */ + static final class CveState { + volatile ScaReachabilityHit hit; // null = registered but not yet reached + } + + /** Immutable snapshot of a dependency's CVE state at drain time. */ + public static final class DependencySnapshot { + public final String artifact; + public final String version; + + /** All CVEs for this dependency: hit==null means known but not reached yet. */ + public final List cves; + + DependencySnapshot(String artifact, String version, List cves) { + this.artifact = artifact; + this.version = version; + this.cves = cves; + } + } + + /** Snapshot of one CVE: hit is null if not yet reached. */ + public static final class CveSnapshot { + public final String vulnId; + public final ScaReachabilityHit hit; // null = reached:[] + + public CveSnapshot(String vulnId, ScaReachabilityHit hit) { + this.vulnId = vulnId; + this.hit = hit; + } + } +} diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java index dbbc95d9576..fd8f7d0ae74 100644 --- a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -3,21 +3,27 @@ import datadog.telemetry.TelemetryRunnable; import datadog.telemetry.TelemetryService; import datadog.telemetry.dependency.Dependency; -import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry.CveSnapshot; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry.DependencySnapshot; import datadog.trace.api.telemetry.ScaReachabilityHit; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; /** - * Drains the {@link ScaReachabilityCollector} on each telemetry heartbeat and reports reachability - * hits as {@code app-dependencies-loaded} entries with {@code metadata} of type {@code - * "reachability"}. + * Reports SCA Reachability state on each telemetry heartbeat, implementing the RFC stateful model: * - *

        Hits are grouped by {@code (artifact, version)} so that multiple CVEs affecting the same - * library version produce a single dependency entry with multiple metadata values, matching the RFC - * payload format. + *

          + *
        1. When a CVE is first detected (class load): reports {@code metadata: + * [{"type":"reachability","value":"{\"id\":\"...\",\"reached\":[]}"}]} — signals the backend + * that SCA is monitoring this CVE even before any symbol is called. + *
        2. When a vulnerable symbol is called: re-reports the dependency with ALL its CVEs, now + * including the callsite in {@code reached} for the CVE that was hit. + *
        3. When nothing changes: reports {@code dependencies:[]} (empty heartbeat). + *
        + * + *

        The key invariant: whenever any CVE's state changes, ALL CVEs for the same dependency are + * re-reported together so the backend always has a complete picture. * *

        Registered in {@link datadog.telemetry.TelemetrySystem} when {@code DD_APPSEC_SCA_ENABLED} is * true. @@ -29,52 +35,45 @@ public final class ScaReachabilityPeriodicAction public void doIteration(TelemetryService telService) { // Trigger pending retransformations (method-level symbols on already-loaded classes, or // classes where JAR version resolution failed at load time and needs a retry). - Runnable work = ScaReachabilityCollector.INSTANCE.getPeriodicWorkCallback(); + Runnable work = ScaReachabilityDependencyRegistry.INSTANCE.periodicWorkCallback; if (work != null) { work.run(); } - List hits = ScaReachabilityCollector.INSTANCE.drain(); - if (hits.isEmpty()) { + List pending = + ScaReachabilityDependencyRegistry.INSTANCE.drainPendingDependencies(); + if (pending.isEmpty()) { return; } - // Group hits by (artifact, version) — multiple CVEs for the same dep go in one entry. - Map> byArtifactVersion = new LinkedHashMap<>(); - for (ScaReachabilityHit hit : hits) { - String key = hit.artifact() + "@" + hit.version(); - byArtifactVersion.computeIfAbsent(key, k -> new ArrayList<>()).add(hit); - } - - for (Map.Entry> entry : byArtifactVersion.entrySet()) { - List group = entry.getValue(); - ScaReachabilityHit first = group.get(0); - - // Build one stringified JSON metadata value per CVE in this group. - List metadataValues = new ArrayList<>(group.size()); - for (ScaReachabilityHit hit : group) { - metadataValues.add(buildMetadataValue(hit)); + for (DependencySnapshot dep : pending) { + // Build one metadata entry per CVE — both those with and without callsite hits. + // This ensures the backend always sees ALL CVEs for the dependency, not just new ones. + List metadataValues = new ArrayList<>(dep.cves.size()); + for (CveSnapshot cve : dep.cves) { + metadataValues.add(buildMetadataValue(cve)); } - - Dependency dep = - new Dependency(first.artifact(), first.version(), null, null, metadataValues); - telService.addDependency(dep); + telService.addDependency( + new Dependency(dep.artifact, dep.version, null, null, metadataValues)); } } /** - * Builds the stringified JSON value for one reachability hit, per RFC: - * - *

        {@code {"id":"GHSA-xxx","reached":[{"path":"com.foo.Bar","symbol":"","line":1}]}}
        -   * 
        + * Builds the stringified JSON value for one CVE snapshot, per RFC: * - *

        For class-level hits, {@code symbol} is {@code ""} and {@code line} is {@code 1} - * (placeholder). For method-level hits, {@code symbol} is the actual method name and {@code line} - * is the first line of the method definition. + *

          + *
        • Not yet reached: {@code {"id":"GHSA-xxx","reached":[]}} + *
        • Reached: {@code + * {"id":"GHSA-xxx","reached":[{"path":"com.foo.Bar","symbol":"...","line":N}]}} + *
        */ - static String buildMetadataValue(ScaReachabilityHit hit) { - // Manual JSON construction — values are safe (GHSA IDs, FQN class names, and method names - // contain no quotes or characters that require JSON escaping). + static String buildMetadataValue(CveSnapshot cve) { + ScaReachabilityHit hit = cve.hit; + if (hit == null) { + // CVE known but no callsite yet — signals "monitoring, not reached" + return "{\"id\":\"" + cve.vulnId + "\",\"reached\":[]}"; + } + // CVE has been reached — include the callsite return "{\"id\":\"" + hit.vulnId() + "\",\"reached\":[{\"path\":\"" diff --git a/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java index 53534e1c505..c2c9c02b5fb 100644 --- a/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java +++ b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java @@ -1,8 +1,7 @@ package datadog.telemetry.sca; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Mockito.mock; @@ -12,8 +11,9 @@ import datadog.telemetry.TelemetryService; import datadog.telemetry.dependency.Dependency; -import datadog.trace.api.telemetry.ScaReachabilityCollector; +import datadog.trace.api.telemetry.ScaReachabilityDependencyRegistry; import datadog.trace.api.telemetry.ScaReachabilityHit; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -25,77 +25,106 @@ class ScaReachabilityPeriodicActionTest { @BeforeEach void setUp() { - ScaReachabilityCollector.INSTANCE.drain(); // clear any leftovers + ScaReachabilityDependencyRegistry.INSTANCE.resetForTesting(); telService = mock(TelemetryService.class); action = new ScaReachabilityPeriodicAction(); } @Test - void doesNothingWhenNoHits() { + void doesNothingWhenNoPendingDependencies() { action.doIteration(telService); verify(telService, never()).addDependency(org.mockito.Mockito.any()); } @Test - void reportsSingleHit() { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit( - "GHSA-test-1234-5678", "com.example:lib", "1.0.0", "com.example.Foo")); + void reportsRegisteredCveWithEmptyReached() { + // CVE registered but no hit yet → metadata: [{cve-1, reached:[]}] + ScaReachabilityDependencyRegistry.INSTANCE.registerCve("com.example:lib", "1.0.0", "GHSA-xxx"); action.doIteration(telService); ArgumentCaptor captor = forClass(Dependency.class); verify(telService, times(1)).addDependency(captor.capture()); - Dependency dep = captor.getValue(); assertEquals("com.example:lib", dep.name); - assertEquals("1.0.0", dep.version); - assertNull(dep.hash); - assertNotNull(dep.reachabilityMetadata); assertEquals(1, dep.reachabilityMetadata.size()); + assertTrue( + dep.reachabilityMetadata.get(0).contains("\"reached\":[]"), + "CVE with no hit must have reached:[]"); + assertTrue(dep.reachabilityMetadata.get(0).contains("\"id\":\"GHSA-xxx\"")); } @Test - void metadataValueContainsClinit() { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-xxx", "com.example:lib", "1.0.0", "com.example.Foo")); + void reportsRegisteredCveWithCallsiteAfterHit() { + // CVE registered, then hit → metadata: [{cve-1, reached:[callsite]}] + ScaReachabilityDependencyRegistry.INSTANCE.registerCve("com.example:lib", "1.0.0", "GHSA-xxx"); + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + "com.example:lib", "1.0.0", "GHSA-xxx", "com.myapp.Service", "process", 42); action.doIteration(telService); ArgumentCaptor captor = forClass(Dependency.class); - verify(telService).addDependency(captor.capture()); - String value = captor.getValue().reachabilityMetadata.get(0); - - assertTrue(value.contains("\"id\":\"GHSA-xxx\"")); - assertTrue(value.contains("\"path\":\"com.example.Foo\"")); - assertTrue(value.contains("\"symbol\":\"\"")); - assertTrue(value.contains("\"line\":1")); + verify(telService, times(1)).addDependency(captor.capture()); + String metaValue = captor.getValue().reachabilityMetadata.get(0); + assertTrue(metaValue.contains("\"path\":\"com.myapp.Service\"")); + assertTrue(metaValue.contains("\"symbol\":\"process\"")); + assertTrue(metaValue.contains("\"line\":42")); + assertFalse(metaValue.contains("\"reached\":[]"), "Hit must not produce empty reached"); } @Test - void groupsTwoCvesForSameArtifactVersionIntoOneEntry() { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-cve-1", "com.example:lib", "1.0.0", "com.example.Foo")); - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-cve-2", "com.example:lib", "1.0.0", "com.example.Bar")); + void groupsTwoCvesForSameArtifactIntoOneEntry() { + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-1"); + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-2"); action.doIteration(telService); ArgumentCaptor captor = forClass(Dependency.class); verify(telService, times(1)).addDependency(captor.capture()); - Dependency dep = captor.getValue(); assertEquals(2, dep.reachabilityMetadata.size()); assertTrue(dep.reachabilityMetadata.stream().anyMatch(v -> v.contains("GHSA-cve-1"))); assertTrue(dep.reachabilityMetadata.stream().anyMatch(v -> v.contains("GHSA-cve-2"))); } + @Test + void reportsAllCvesWhenOneIsHit() { + // RFC requirement: when cve-1 is hit, re-report BOTH cve-1 (with callsite) and cve-2 (empty) + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-1"); + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-2"); + // First heartbeat: both sent with empty reached + action.doIteration(telService); + + // Now hit cve-1 + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + "com.example:lib", "1.0.0", "GHSA-cve-1", "com.myapp.Svc", "call", 10); + + // Second heartbeat: BOTH CVEs re-reported — cve-1 with callsite, cve-2 still empty + action.doIteration(telService); + + ArgumentCaptor captor = forClass(Dependency.class); + verify(telService, times(2)).addDependency(captor.capture()); + List reported = captor.getAllValues(); + Dependency secondReport = reported.get(1); + assertEquals(2, secondReport.reachabilityMetadata.size()); + // cve-1 now has a callsite + assertTrue( + secondReport.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-1") && v.contains("\"path\""))); + // cve-2 still has empty reached + assertTrue( + secondReport.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-2") && v.contains("\"reached\":[]"))); + } + @Test void separateEntriesForDifferentArtifacts() { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-a", "com.example:lib-a", "1.0.0", "com.example.A")); - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-b", "com.example:lib-b", "2.0.0", "com.example.B")); + ScaReachabilityDependencyRegistry.INSTANCE.registerCve("com.example:lib-a", "1.0.0", "GHSA-a"); + ScaReachabilityDependencyRegistry.INSTANCE.registerCve("com.example:lib-b", "2.0.0", "GHSA-b"); action.doIteration(telService); @@ -103,29 +132,45 @@ void separateEntriesForDifferentArtifacts() { } @Test - void drainsClearsPreviousHits() { - ScaReachabilityCollector.INSTANCE.addHit( - new ScaReachabilityHit("GHSA-x", "com.example:lib", "1.0.0", "com.example.X")); + void drainsClearsPendingState() { + ScaReachabilityDependencyRegistry.INSTANCE.registerCve("com.example:lib", "1.0.0", "GHSA-x"); action.doIteration(telService); verify(telService, times(1)).addDependency(org.mockito.Mockito.any()); - // Second iteration with no new hits — nothing to report + // Second iteration with no new state — nothing to report TelemetryService telService2 = mock(TelemetryService.class); action.doIteration(telService2); verify(telService2, never()).addDependency(org.mockito.Mockito.any()); } @Test - void buildMetadataValue_format() { + void buildMetadataValue_emptyReachedWhenNoHit() { + ScaReachabilityDependencyRegistry.CveSnapshot cve = + new ScaReachabilityDependencyRegistry.CveSnapshot("GHSA-645p-88qh-w398", null); + + String value = ScaReachabilityPeriodicAction.buildMetadataValue(cve); + + assertEquals( + "{\"id\":\"GHSA-645p-88qh-w398\",\"reached\":[]}", + value, + "CVE with no hit must produce reached:[]"); + } + + @Test + void buildMetadataValue_includesCallsiteWhenHit() { ScaReachabilityHit hit = new ScaReachabilityHit( "GHSA-645p-88qh-w398", "com.fasterxml.jackson.core:jackson-databind", "2.8.5", - "com.fasterxml.jackson.databind.ObjectMapper"); + "com.fasterxml.jackson.databind.ObjectMapper", + "", + 1); + ScaReachabilityDependencyRegistry.CveSnapshot cve = + new ScaReachabilityDependencyRegistry.CveSnapshot("GHSA-645p-88qh-w398", hit); - String value = ScaReachabilityPeriodicAction.buildMetadataValue(hit); + String value = ScaReachabilityPeriodicAction.buildMetadataValue(cve); assertEquals( "{\"id\":\"GHSA-645p-88qh-w398\"," From fdcb4212e9efcaaadd2cd931fc5804beefbf1151 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 10:25:19 +0200 Subject: [PATCH 26/35] Add smoke test for SCA Reachability telemetry (APPSEC-62260) Verifies the RFC stateful heartbeat model end-to-end: - jackson-databind:2.6.0 (vulnerable, range < 2.6.7.3) appears in app-dependencies-loaded with metadata reachability entries - GHSA identifier present in the metadata value - reached[] contains a callsite after ObjectMapper is loaded at startup Uses the existing springboot smoke test app (already has jackson-databind:2.6.0) with DD_APPSEC_SCA_ENABLED=true added to JVM args. --- .../appsec/ScaReachabilitySmokeTest.groovy | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy new file mode 100644 index 00000000000..f0f5333a6c4 --- /dev/null +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy @@ -0,0 +1,93 @@ +package datadog.smoketest.appsec + +import groovy.json.JsonSlurper +import spock.lang.Shared + +/** + * Smoke test for SCA Reachability (DD_APPSEC_SCA_ENABLED=true). + * + * Verifies that the tracer reports vulnerable library classes via the + * app-dependencies-loaded telemetry heartbeat using the RFC stateful model: + * + * 1. At startup, vulnerable dependencies are reported with metadata: [{cve, reached:[]}] + * (signals the backend that SCA is monitoring those CVEs). + * 2. Once a class from the vulnerable library is loaded, reached is populated + * with the first callsite. + * + * The springboot smoke test app uses jackson-databind:2.6.0, which falls in the + * vulnerable range "< 2.6.7.3" for GHSA-645p-88qh-w398. Spring Boot auto-configures + * Jackson at startup so com.fasterxml.jackson.databind.ObjectMapper is always loaded. + */ +class ScaReachabilitySmokeTest extends AbstractAppSecServerSmokeTest { + + @Shared + String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot.shadowJar.path") + + @Override + ProcessBuilder createProcessBuilder() { + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(defaultAppSecProperties) + // Enable SCA Reachability + command.add("-Ddd.appsec.sca.enabled=true") + command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"]) + + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + return processBuilder + } + + void 'SCA reachability reports vulnerable jackson-databind via telemetry'() { + when: 'application starts and telemetry heartbeats arrive' + waitForTelemetryFlat { it.get('request_type') == 'app-dependencies-loaded' } + + then: 'jackson-databind 2.6.0 appears with SCA reachability metadata' + // Collect all dependencies from all app-dependencies-loaded messages + def allDependencies = [] + telemetryFlatMessages.findAll { it.get('request_type') == 'app-dependencies-loaded' }.each { + def payload = it.get('payload') as Map + def deps = payload?.get('dependencies') as List + if (deps) allDependencies.addAll(deps) + } + + // Find the jackson-databind entry + def jacksonDep = allDependencies.find { dep -> + (dep as Map).get('name') == 'com.fasterxml.jackson.core:jackson-databind' + } as Map + + assert jacksonDep != null : "jackson-databind must appear in app-dependencies-loaded" + assert jacksonDep.get('version') == '2.6.0' : "must be the vulnerable version 2.6.0" + + // The metadata array must be present (SCA is enabled) + def metadata = jacksonDep.get('metadata') as List + assert metadata != null : "metadata field must be present when DD_APPSEC_SCA_ENABLED=true" + + // Find the reachability metadata entry + def reachabilityEntry = metadata.find { entry -> + (entry as Map).get('type') == 'reachability' + } as Map + + assert reachabilityEntry != null : "at least one reachability metadata entry expected" + + // Parse the stringified JSON value + def valueJson = reachabilityEntry.get('value') as String + assert valueJson != null && !valueJson.isEmpty() : "value must not be empty" + + def reachabilityPayload = new JsonSlurper().parseText(valueJson) as Map + assert reachabilityPayload.get('id') != null : "CVE id must be present" + assert reachabilityPayload.get('id').toString().startsWith('GHSA-') : + "id must be a GHSA identifier, got: ${reachabilityPayload.get('id')}" + assert reachabilityPayload.get('reached') instanceof List : "reached must be a list" + + // ObjectMapper is always loaded by Spring Boot — reached must be non-empty + def reached = reachabilityPayload.get('reached') as List + assert !reached.isEmpty() : + "ObjectMapper is loaded at Spring Boot startup — reached must contain at least one callsite" + + def callsite = reached[0] as Map + assert callsite.get('path') != null : "callsite path must be present" + assert callsite.get('symbol') != null : "callsite symbol must be present" + assert (callsite.get('line') as int) >= 0 : "callsite line must be non-negative" + } +} From 579bbd0506d3aa21f948426eb85e5d022306ce29 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 10:41:02 +0200 Subject: [PATCH 27/35] Add method-level symbols for jackson-databind deserialization CVEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the 7 jackson-databind CVE entries, adds method-level symbols for the deserialization entry points that actually trigger gadget chains when polymorphic typing is enabled with untrusted input: ObjectMapper.readValue — primary deserialization entry point ObjectMapper.readValues — multiple-value deserialization ObjectReader.readValue — reader-based deserialization variant ObjectReader.readValues — reader-based multiple values Class-level symbols (method=null) are kept alongside the new method-level ones: class load detection signals the library is present; method detection signals the vulnerable code path was actually invoked. 26 method-level symbols added across 7 entries (ObjectMapper + ObjectReader × readValue + readValues × 7 GHSA entries). --- dd-java-agent/appsec/src/main/resources/sca_cves.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/appsec/src/main/resources/sca_cves.json b/dd-java-agent/appsec/src/main/resources/sca_cves.json index 0cccdbc111a..0a990076acb 100644 --- a/dd-java-agent/appsec/src/main/resources/sca_cves.json +++ b/dd-java-agent/appsec/src/main/resources/sca_cves.json @@ -1 +1 @@ -{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]}]} \ No newline at end of file +{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]}]} \ No newline at end of file From 9c89c1b042cc2a59edc42e4c66b6960893334dc5 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 10:49:48 +0200 Subject: [PATCH 28/35] Add method-level symbols for xstream, log4j, snakeyaml, jackson-mapper-asl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds method-level detection for 24 entries across 4 libraries where the deserialization/injection entry point is 100% certain: XStream (17 entries): fromXML — THE entry point for all XStream CVEs; triggers gadget chains when deserializing untrusted XML log4j-core (4 entries): info, error, warn, debug, trace, fatal, log — Log4Shell (GHSA-jfh8-c2jp-5v3q) triggers JNDI lookup when log messages contain ${jndi:...} patterns; any Logger method is an entry point snakeyaml (1 entry): load, loadAll — unsafe YAML deserialization; instantiates arbitrary Java classes from untrusted YAML input jackson-mapper-asl (1 entry): readValue, readValues — same deserialization pattern as jackson-databind, applies to the legacy 1.x mapper 56 method-level symbols added. Class-level symbols (method=null) are kept alongside the new method-level ones for dual detection coverage. --- dd-java-agent/appsec/src/main/resources/sca_cves.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/appsec/src/main/resources/sca_cves.json b/dd-java-agent/appsec/src/main/resources/sca_cves.json index 0a990076acb..33745ddd971 100644 --- a/dd-java-agent/appsec/src/main/resources/sca_cves.json +++ b/dd-java-agent/appsec/src/main/resources/sca_cves.json @@ -1 +1 @@ -{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null}]}]} \ No newline at end of file +{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValue"},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null},{"class":"org/yaml/snakeyaml/Yaml","method":"load"},{"class":"org/yaml/snakeyaml/Yaml","method":"loadAll"}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]}]} \ No newline at end of file From 00185f12c9ddc97f716f57cfe649b5f7a4e3e913 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 14:41:08 +0200 Subject: [PATCH 29/35] Fix SCA smoke test, RFC compliance and add heartbeat flow tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScaReachabilitySmokeTest: fix find() to look for the entry with reachability metadata — the same dep appears twice (once from DependencyService without metadata, once from SCA with CVE data) - TelemetryRequestBody.writeDependency(): write metadata:[] even when list is empty — null means SCA disabled, empty list means SCA active but no CVEs detected yet (RFC: all deps get metadata:[] at startup) - sca_cves.json: remove class-level symbol from snakeyaml — Spring Boot loads Yaml at startup causing registerCve+recordHit to fire in the same request, preventing the reached:[] heartbeat from being observed - ScaReachabilityPeriodicActionTest: add rfcFullHeartbeatFlow test covering Heartbeats #2-#6 from the RFC spec - TelemetryRequestBodyDependencyMetadataTest: update to reflect that metadata:[] is written (not suppressed) when list is empty --- .../appsec/src/main/resources/sca_cves.json | 2 +- .../appsec/ScaReachabilitySmokeTest.groovy | 21 ++--- .../telemetry/TelemetryRequestBody.java | 5 +- ...etryRequestBodyDependencyMetadataTest.java | 6 +- .../ScaReachabilityPeriodicActionTest.java | 80 +++++++++++++++++++ 5 files changed, 101 insertions(+), 13 deletions(-) diff --git a/dd-java-agent/appsec/src/main/resources/sca_cves.json b/dd-java-agent/appsec/src/main/resources/sca_cves.json index 33745ddd971..05414d3d8ec 100644 --- a/dd-java-agent/appsec/src/main/resources/sca_cves.json +++ b/dd-java-agent/appsec/src/main/resources/sca_cves.json @@ -1 +1 @@ -{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValue"},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":null},{"class":"org/yaml/snakeyaml/Yaml","method":"load"},{"class":"org/yaml/snakeyaml/Yaml","method":"loadAll"}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]}]} \ No newline at end of file +{"version":1,"entries":[{"vuln_id":"GHSA-24rp-q3w6-vc56","artifact":"org.postgresql:postgresql","version_ranges":[">= 42.2.0, < 42.2.28",">= 42.3.0, < 42.3.9",">= 42.4.0, < 42.4.4",">= 42.5.0, < 42.5.5",">= 42.6.0, < 42.6.1",">= 42.7.0, < 42.7.2"],"symbols":[{"class":"java/sql/PreparedStatement","method":null},{"class":"java/sql/Statement","method":null},{"class":"java/sql/Connection","method":null},{"class":"java/sql/DriverManager","method":null},{"class":"javax/sql/DataSource","method":null},{"class":"org/postgresql/ds/PGSimpleDataSource","method":null},{"class":"org/postgresql/ds/PGPoolingDataSource","method":null},{"class":"org/postgresql/ds/PGConnectionPoolDataSource","method":null}]},{"vuln_id":"GHSA-2p3x-qw9c-25hh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.16"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-2q8x-2p7f-574v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-web","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework.boot:spring-boot-starter-webflux","version_ranges":["< 2.5.12",">= 2.6.0, < 2.6.6"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-beans","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webflux","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-36p3-wjmg-h94x","artifact":"org.springframework:spring-webmvc","version_ranges":[">= 5.3.0, < 5.3.18","< 5.2.20.RELEASE"],"symbols":[{"class":"org/springframework/stereotype/Controller","method":null},{"class":"org/springframework/web/bind/annotation/RestController","method":null},{"class":"org/springframework/web/bind/annotation/RequestMapping","method":null},{"class":"org/springframework/web/bind/annotation/PostMapping","method":null},{"class":"org/springframework/web/bind/annotation/PutMapping","method":null},{"class":"org/springframework/web/bind/annotation/PatchMapping","method":null},{"class":"org/springframework/web/bind/annotation/ModelAttribute","method":null},{"class":"org/springframework/validation/DataBinder","method":null},{"class":"org/springframework/web/bind/WebDataBinder","method":null},{"class":"org/springframework/web/bind/annotation/InitBinder","method":null},{"class":"org/springframework/web/bind/annotation/ControllerAdvice","method":null}]},{"vuln_id":"GHSA-3ccq-5vw3-2p6x","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-3gm7-v7vw-866c","artifact":"org.apache.solr:solr-core","version_ranges":["< 8.2.0"],"symbols":[{"class":"org/apache/solr/handler/dataimport/DataImporter","method":null},{"class":"org/apache/solr/handler/dataimport/DataImportHandler","method":null}]},{"vuln_id":"GHSA-4cch-wxpw-8p28","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.15"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-4gq5-ch57-c2mg","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4jrv-ppp4-jm57","artifact":"com.google.code.gson:gson","version_ranges":["< 2.8.9"],"symbols":[{"class":"com/google/gson/Gson","method":null},{"class":"com/google/gson/GsonBuilder","method":null}]},{"vuln_id":"GHSA-4w82-r329-3q67","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.4",">= 2.7.0, < 2.7.9.7",">= 2.8.0, < 2.8.11.5",">= 2.9.0, < 2.9.10.3"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-4wrc-f8pq-fpqp","artifact":"org.springframework:spring-web","version_ranges":["< 6.0.0"],"symbols":[{"class":"org/springframework/remoting/rmi/CodebaseAwareObjectInputStream","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/JndiRmiServiceExporter","method":null},{"class":"org/springframework/remoting/rmi/RemoteInvocationSerializingExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiBasedExporter","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptor","method":null},{"class":"org/springframework/remoting/rmi/RmiClientInterceptorUtils","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationHandler","method":null},{"class":"org/springframework/remoting/rmi/RmiInvocationWrapper","method":null},{"class":"org/springframework/remoting/rmi/RmiProxyFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiRegistryFactoryBean","method":null},{"class":"org/springframework/remoting/rmi/RmiServiceExporter","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerClientInterceptor","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerProxyFactoryBean","method":null},{"class":"org/springframework/jms/remoting/JmsInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianClientInterceptor","method":null},{"class":"org/springframework/remoting/caucho/HessianExporter","method":null},{"class":"org/springframework/remoting/caucho/HessianProxyFactoryBean","method":null},{"class":"org/springframework/remoting/caucho/HessianServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/AbstractHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientConfiguration","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerClientInterceptor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerRequestExecutor","method":null},{"class":"org/springframework/remoting/httpinvoker/SimpleHttpInvokerServiceExporter","method":null}]},{"vuln_id":"GHSA-645p-88qh-w398","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.6.7.3",">= 2.7.0, < 2.7.9.5",">= 2.8.0, < 2.8.11.3",">= 2.9.0, < 2.9.7"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-64xx-cq4q-mf44","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-6w62-hx7r-mw68","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-7rjr-3q55-vv33","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.16.0","< 2.12.2"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-8jrj-525p-826v","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-9mxf-g3x6-wv74","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.7",">= 2.8.0, < 2.8.11.3",">= 2.7.0, < 2.7.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"},{"class":"com/fasterxml/jackson/databind/annotation/JsonTypeInfo","method":null}]},{"vuln_id":"GHSA-c27h-mcmw-48hv","artifact":"org.codehaus.jackson:jackson-mapper-asl","version_ranges":["<= 1.9.13"],"symbols":[{"class":"org/codehaus/jackson/map/ObjectMapper","method":null},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValue"},{"class":"org/codehaus/jackson/map/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-c9hw-wf7x-jp9j","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.31",">= 8.5.0, < 8.5.51",">= 7.0.0, < 7.0.100"],"symbols":[{"class":"org/apache/coyote/ajp/AbstractAjpProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpProcessor","method":null},{"class":"org/apache/coyote/ajp/AjpNioProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpAprProtocol","method":null},{"class":"org/apache/coyote/ajp/AjpNio2Protocol","method":null},{"class":"org/apache/catalina/connector/Connector","method":null}]},{"vuln_id":"GHSA-cm59-pr5q-cw85","artifact":"org.springframework.boot:spring-boot","version_ranges":["<= 2.2.10.RELEASE"],"symbols":[{"class":"org/springframework/boot/SpringApplication","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-client","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-crg9-44h2-xw35","artifact":"org.apache.activemq:activemq-openwire-legacy","version_ranges":["< 5.15.16",">= 5.16.0, < 5.16.7",">= 5.17.0, < 5.17.6",">= 5.18.0, < 5.18.3"],"symbols":[{"class":"org/apache/activemq/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQSslConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/ActiveMQXASslConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/PooledConnectionFactory","method":null},{"class":"org/apache/activemq/jms/pool/XaPooledConnectionFactory","method":null},{"class":"org/apache/activemq/broker/BrokerService","method":null},{"class":"org/apache/activemq/broker/BrokerFactory","method":null},{"class":"org/apache/activemq/xbean/BrokerFactoryBean","method":null},{"class":"org/apache/activemq/xbean/XBeanBrokerService","method":null},{"class":"org/apache/activemq/spring/ActiveMQConnectionFactory","method":null},{"class":"org/apache/activemq/spring/ActiveMQXAConnectionFactory","method":null},{"class":"org/apache/activemq/pool/PooledConnectionFactoryBean","method":null},{"class":"org/apache/activemq/jndi/ActiveMQInitialContextFactory","method":null},{"class":"org/apache/activemq/jndi/ActiveMQSslInitialContextFactory","method":null},{"class":"org/apache/activemq/transport/tcp/TcpTransportFactory","method":null},{"class":"org/apache/activemq/transport/tcp/SslTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOTransportFactory","method":null},{"class":"org/apache/activemq/transport/nio/NIOSSLTransportFactory","method":null}]},{"vuln_id":"GHSA-cxfm-5m4g-x7xp","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-f3j5-rmmp-3fc5","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.8.11.5",">= 2.9.0, < 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-g5h3-w546-pj7f","artifact":"org.springframework.boot:spring-boot-actuator-autoconfigure","version_ranges":[">= 3.0.0, < 3.0.6",">= 2.7.0, < 2.7.11",">= 2.6.0, < 2.6.15",">= 2.5.0, < 2.5.15"],"symbols":[{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping","method":null},{"class":"org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping","method":null}]},{"vuln_id":"GHSA-g5w6-mrj7-75h2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-h7v4-7xg3-hxcc","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-hph2-m3g5-xxv4","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-j9h8-phrw-h4fh","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.apache.logging.log4j:log4j-core","version_ranges":[">= 2.13.0, < 2.15.0",">= 2.4, < 2.12.2",">= 2.0-beta9, < 2.3.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"com.guicedee.services:log4j-core","version_ranges":["<= 1.2.1.2-jre17"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"org.xbib.elasticsearch:log4j","version_ranges":["= 6.3.2.1"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-jfh8-c2jp-5v3q","artifact":"uk.co.nichesolutions.logging.log4j:log4j-core","version_ranges":["= 2.6.3-CUSTOM"],"symbols":[{"class":"org/apache/logging/log4j/Logger","method":null},{"class":"org/apache/logging/log4j/Logger","method":"info"},{"class":"org/apache/logging/log4j/Logger","method":"error"},{"class":"org/apache/logging/log4j/Logger","method":"warn"},{"class":"org/apache/logging/log4j/Logger","method":"debug"},{"class":"org/apache/logging/log4j/Logger","method":"trace"},{"class":"org/apache/logging/log4j/Logger","method":"fatal"},{"class":"org/apache/logging/log4j/Logger","method":"log"},{"class":"org/apache/logging/log4j/LogManager","method":null}]},{"vuln_id":"GHSA-mjmj-j48q-9wg2","artifact":"org.yaml:snakeyaml","version_ranges":["<= 1.33"],"symbols":[{"class":"org/yaml/snakeyaml/Yaml","method":"load"},{"class":"org/yaml/snakeyaml/Yaml","method":"loadAll"}]},{"vuln_id":"GHSA-mw36-7c6c-q4q2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["<= 1.4.13"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-p8pq-r894-fm8f","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-qmqc-x3r4-6v39","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":["< 2.9.10"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"}]},{"vuln_id":"GHSA-qr7j-h6gg-jmgc","artifact":"com.fasterxml.jackson.core:jackson-databind","version_ranges":[">= 2.0.0, <= 2.7.9.3",">= 2.8.0, <= 2.8.11.1",">= 2.9.0, <= 2.9.5"],"symbols":[{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":null},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectMapper","method":"readValues"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":null},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValue"},{"class":"com/fasterxml/jackson/databind/ObjectReader","method":"readValues"}]},{"vuln_id":"GHSA-qrx8-8545-4wg2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-r4x2-3cq5-hqvp","artifact":"org.apache.tomcat.embed:tomcat-embed-core","version_ranges":[">= 9.0.0.M1, < 9.0.9",">= 8.5.0, < 8.5.32",">= 8.0.0-RC1, < 8.0.53",">= 7.0.41, < 7.0.88"],"symbols":[{"class":"org/apache/catalina/filters/CorsFilter","method":null}]},{"vuln_id":"GHSA-rmr5-cpv2-vgjf","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.19"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-classic","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-vmfg-rjjm-rjrj","artifact":"ch.qos.logback:logback-access","version_ranges":["< 1.2.0"],"symbols":[{"class":"ch/qos/logback/classic/net/SimpleSocketServer","method":null},{"class":"ch/qos/logback/classic/net/SocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/ServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/server/SSLServerSocketReceiver","method":null},{"class":"ch/qos/logback/classic/net/SimpleSSLSocketServer","method":null},{"class":"ch/qos/logback/access/net/SimpleSocketServer","method":null}]},{"vuln_id":"GHSA-ww97-9w65-2crx","artifact":"org.apache.solr:solr-core","version_ranges":[">= 5.0.0, <= 5.5.5",">= 6.0.0, <= 6.6.6",">= 7.0.0, <= 7.7.2",">= 8.0.0, <= 8.3.1"],"symbols":[{"class":"org/apache/velocity/app/Velocity","method":null},{"class":"org/apache/velocity/VelocityContext","method":null},{"class":"org/apache/velocity/Template","method":null}]},{"vuln_id":"GHSA-xw4p-crpj-vjx2","artifact":"com.thoughtworks.xstream:xstream","version_ranges":["< 1.4.18"],"symbols":[{"class":"com/thoughtworks/xstream/XStream","method":null},{"class":"com/thoughtworks/xstream/XStream","method":"fromXML"}]}]} \ No newline at end of file diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy index f0f5333a6c4..ee805f5bfd8 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy @@ -51,19 +51,22 @@ class ScaReachabilitySmokeTest extends AbstractAppSecServerSmokeTest { if (deps) allDependencies.addAll(deps) } - // Find the jackson-databind entry + // Find the jackson-databind entry that has SCA reachability metadata. + // The same dependency may appear multiple times: once from the regular dependency + // detector (no metadata) and once from the SCA periodic action (with metadata). + // We must search for the entry that actually carries reachability metadata. def jacksonDep = allDependencies.find { dep -> - (dep as Map).get('name') == 'com.fasterxml.jackson.core:jackson-databind' + def d = dep as Map + d.get('name') == 'com.fasterxml.jackson.core:jackson-databind' && + (d.get('metadata') as List)?.any { (it as Map).get('type') == 'reachability' } } as Map - assert jacksonDep != null : "jackson-databind must appear in app-dependencies-loaded" + assert jacksonDep != null : + "jackson-databind must appear with SCA reachability metadata in app-dependencies-loaded" assert jacksonDep.get('version') == '2.6.0' : "must be the vulnerable version 2.6.0" - // The metadata array must be present (SCA is enabled) - def metadata = jacksonDep.get('metadata') as List - assert metadata != null : "metadata field must be present when DD_APPSEC_SCA_ENABLED=true" - // Find the reachability metadata entry + def metadata = jacksonDep.get('metadata') as List def reachabilityEntry = metadata.find { entry -> (entry as Map).get('type') == 'reachability' } as Map @@ -77,13 +80,13 @@ class ScaReachabilitySmokeTest extends AbstractAppSecServerSmokeTest { def reachabilityPayload = new JsonSlurper().parseText(valueJson) as Map assert reachabilityPayload.get('id') != null : "CVE id must be present" assert reachabilityPayload.get('id').toString().startsWith('GHSA-') : - "id must be a GHSA identifier, got: ${reachabilityPayload.get('id')}" + "id must be a GHSA identifier, got: ${reachabilityPayload.get('id')}" assert reachabilityPayload.get('reached') instanceof List : "reached must be a list" // ObjectMapper is always loaded by Spring Boot — reached must be non-empty def reached = reachabilityPayload.get('reached') as List assert !reached.isEmpty() : - "ObjectMapper is loaded at Spring Boot startup — reached must contain at least one callsite" + "ObjectMapper is loaded at Spring Boot startup — reached must contain at least one callsite" def callsite = reached[0] as Map assert callsite.get('path') != null : "callsite path must be present" diff --git a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java index 65cfd7d166e..40bd2e46471 100644 --- a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java +++ b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java @@ -271,7 +271,10 @@ public void writeDependency(Dependency d) throws IOException { bodyWriter.name("hash").value(d.hash); // optional bodyWriter.name("name").value(d.name); bodyWriter.name("version").value(d.version); // optional - if (d.reachabilityMetadata != null && !d.reachabilityMetadata.isEmpty()) { + if (d.reachabilityMetadata != null) { + // Write metadata array even when empty: empty list signals "SCA is active for this dep" + // (RFC: all deps get metadata:[] at startup when DD_APPSEC_SCA_ENABLED=true). + // Null means SCA is disabled; empty list means SCA is enabled but no CVEs detected yet. bodyWriter.name("metadata").beginArray(); for (String value : d.reachabilityMetadata) { bodyWriter.beginObject(); diff --git a/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java b/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java index a617221c475..9751aaf68d6 100644 --- a/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java +++ b/telemetry/src/test/java/datadog/telemetry/TelemetryRequestBodyDependencyMetadataTest.java @@ -68,13 +68,15 @@ void writeDependency_omitsMetadataFieldWhenNull() throws IOException { } @Test - void writeDependency_omitsMetadataFieldWhenEmptyList() throws IOException { + void writeDependency_includesEmptyMetadataArrayWhenListIsEmpty() throws IOException { + // RFC: metadata:[] (non-null, empty) means "SCA is active for this dep but no CVEs detected". + // Must be written so the backend knows SCA is monitoring the dependency. Dependency dep = new Dependency("com.example:lib", "1.0.0", null, null, Collections.emptyList()); String json = serializeDependency(dep); - assertFalse(json.contains("\"metadata\""), "metadata field must be absent when list is empty"); + assertTrue(json.contains("\"metadata\":[]"), "metadata:[] must be present when list is empty"); } private static String serializeDependency(Dependency dep) throws IOException { diff --git a/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java index c2c9c02b5fb..85ab5b78fbf 100644 --- a/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java +++ b/telemetry/src/test/java/datadog/telemetry/sca/ScaReachabilityPeriodicActionTest.java @@ -144,6 +144,86 @@ void drainsClearsPendingState() { verify(telService2, never()).addDependency(org.mockito.Mockito.any()); } + /** + * Validates the full RFC heartbeat flow (Heartbeats #2–#6 from the spec): + * + *
          + *
        1. Heartbeat after CVE registration: both CVEs reported with reached:[] + *
        2. Heartbeat with no changes: nothing reported + *
        3. Heartbeat after first CVE hit: both CVEs reported (one with callsite, one empty) + *
        4. Heartbeat with no changes: nothing reported + *
        5. Heartbeat after second CVE hit: both CVEs reported with their respective callsites + *
        + */ + @Test + void rfcFullHeartbeatFlow_twoCveSameDepBothHitSequentially() { + // Phase 1 — CVE registration (Heartbeat #2) + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-1"); + ScaReachabilityDependencyRegistry.INSTANCE.registerCve( + "com.example:lib", "1.0.0", "GHSA-cve-2"); + + action.doIteration(telService); + + ArgumentCaptor captor1 = ArgumentCaptor.forClass(Dependency.class); + verify(telService, times(1)).addDependency(captor1.capture()); + Dependency hb2 = captor1.getValue(); + assertEquals(2, hb2.reachabilityMetadata.size()); + assertTrue( + hb2.reachabilityMetadata.stream().allMatch(v -> v.contains("\"reached\":[]")), + "Heartbeat #2: both CVEs must have reached:[]"); + + // Phase 2 — No changes (Heartbeat #3) + TelemetryService telService3 = mock(TelemetryService.class); + action.doIteration(telService3); + verify(telService3, never()).addDependency(org.mockito.Mockito.any()); + + // Phase 3 — First CVE hit (Heartbeat #4) + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + "com.example:lib", "1.0.0", "GHSA-cve-1", "com.myapp.Controller", "handleRequest", 10); + + TelemetryService telService4 = mock(TelemetryService.class); + action.doIteration(telService4); + + ArgumentCaptor captor4 = ArgumentCaptor.forClass(Dependency.class); + verify(telService4, times(1)).addDependency(captor4.capture()); + Dependency hb4 = captor4.getValue(); + assertEquals(2, hb4.reachabilityMetadata.size()); + assertTrue( + hb4.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-1") && v.contains("\"path\"")), + "Heartbeat #4: cve-1 must have callsite"); + assertTrue( + hb4.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-2") && v.contains("\"reached\":[]")), + "Heartbeat #4: cve-2 must still have reached:[]"); + + // Phase 4 — No changes (Heartbeat #5) + TelemetryService telService5 = mock(TelemetryService.class); + action.doIteration(telService5); + verify(telService5, never()).addDependency(org.mockito.Mockito.any()); + + // Phase 5 — Second CVE hit (Heartbeat #6) + ScaReachabilityDependencyRegistry.INSTANCE.recordHit( + "com.example:lib", "1.0.0", "GHSA-cve-2", "com.myapp.Service", "processData", 44); + + TelemetryService telService6 = mock(TelemetryService.class); + action.doIteration(telService6); + + ArgumentCaptor captor6 = ArgumentCaptor.forClass(Dependency.class); + verify(telService6, times(1)).addDependency(captor6.capture()); + Dependency hb6 = captor6.getValue(); + assertEquals(2, hb6.reachabilityMetadata.size()); + assertTrue( + hb6.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-1") && v.contains("\"path\"")), + "Heartbeat #6: cve-1 must retain callsite"); + assertTrue( + hb6.reachabilityMetadata.stream() + .anyMatch(v -> v.contains("GHSA-cve-2") && v.contains("\"path\"")), + "Heartbeat #6: cve-2 must now have callsite"); + } + @Test void buildMetadataValue_emptyReachedWhenNoHit() { ScaReachabilityDependencyRegistry.CveSnapshot cve = From a12d3f9487bf161cc439bb885f00948d20584c1e Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 15:27:06 +0200 Subject: [PATCH 30/35] fix(smoke): add braces to if statement to satisfy CodeNarc IfStatementBraces rule --- .../datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy index ee805f5bfd8..67890896826 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ScaReachabilitySmokeTest.groovy @@ -48,7 +48,9 @@ class ScaReachabilitySmokeTest extends AbstractAppSecServerSmokeTest { telemetryFlatMessages.findAll { it.get('request_type') == 'app-dependencies-loaded' }.each { def payload = it.get('payload') as Map def deps = payload?.get('dependencies') as List - if (deps) allDependencies.addAll(deps) + if (deps) { + allDependencies.addAll(deps) + } } // Find the jackson-databind entry that has SCA reachability metadata. From 32fb0d1318b7bcd393be54082cfdd5cbe12a4788 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 15:40:53 +0200 Subject: [PATCH 31/35] fix(spotbugs): make periodicWorkCallback private, expose via getter --- .../api/telemetry/ScaReachabilityDependencyRegistry.java | 6 +++++- .../telemetry/sca/ScaReachabilityPeriodicAction.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java index 44b80eb972a..a460df3a318 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/ScaReachabilityDependencyRegistry.java @@ -33,12 +33,16 @@ public final class ScaReachabilityDependencyRegistry { * Optional periodic work hook for retransformation of pending method-level classes. Registered by * {@code ScaReachabilitySystem}, called by {@code ScaReachabilityPeriodicAction}. */ - public volatile Runnable periodicWorkCallback; + private volatile Runnable periodicWorkCallback; public void setPeriodicWorkCallback(Runnable callback) { periodicWorkCallback = callback; } + public Runnable getPeriodicWorkCallback() { + return periodicWorkCallback; + } + /** Clears all state. Used in tests to reset between test cases. */ public void resetForTesting() { dependencies.clear(); diff --git a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java index fd8f7d0ae74..9e2137e9de0 100644 --- a/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java +++ b/telemetry/src/main/java/datadog/telemetry/sca/ScaReachabilityPeriodicAction.java @@ -35,7 +35,7 @@ public final class ScaReachabilityPeriodicAction public void doIteration(TelemetryService telService) { // Trigger pending retransformations (method-level symbols on already-loaded classes, or // classes where JAR version resolution failed at load time and needs a retry). - Runnable work = ScaReachabilityDependencyRegistry.INSTANCE.periodicWorkCallback; + Runnable work = ScaReachabilityDependencyRegistry.INSTANCE.getPeriodicWorkCallback(); if (work != null) { work.run(); } From 54332faf12f1574b866905ba48b6bb78e13427d6 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 15:56:58 +0200 Subject: [PATCH 32/35] refactor: replace Map casts with typed Moshi DTOs in ScaCveDatabase --- .../datadog/appsec/sca/ScaCveDatabase.java | 68 +++++++++++++++---- .../java/com/datadog/appsec/sca/ScaEntry.java | 42 ------------ 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java index 35f360bd639..caa5958d54c 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaCveDatabase.java @@ -1,17 +1,17 @@ package com.datadog.appsec.sca; +import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; -import com.squareup.moshi.Types; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,29 +61,21 @@ public static ScaCveDatabase load() { static ScaCveDatabase parse(java.io.Reader reader) throws IOException { Moshi moshi = new Moshi.Builder().build(); - Type rootType = Types.newParameterizedType(Map.class, String.class, Object.class); - JsonAdapter> adapter = moshi.adapter(rootType); + JsonAdapter adapter = moshi.adapter(DatabaseJson.class); String content = readAll(reader); - Map root = adapter.fromJson(content); - if (root == null) { - throw new IOException("sca_cves.json is empty"); - } - - List rawEntries = (List) root.get("entries"); - if (rawEntries == null) { + DatabaseJson root = adapter.fromJson(content); + if (root == null || root.entries == null) { return new ScaCveDatabase(Collections.emptyMap()); } Map> index = new HashMap<>(); int entryCount = 0; - for (Object rawEntry : rawEntries) { - Map entryMap = (Map) rawEntry; - ScaEntry entry = ScaEntry.fromMap(entryMap); + for (EntryJson e : root.entries) { + ScaEntry entry = toScaEntry(e); if (entry == null) continue; entryCount++; - for (ScaSymbol symbol : entry.symbols()) { index.computeIfAbsent(symbol.className(), k -> new ArrayList<>()).add(entry); } @@ -94,6 +86,21 @@ static ScaCveDatabase parse(java.io.Reader reader) throws IOException { return new ScaCveDatabase(Collections.unmodifiableMap(index)); } + @Nullable + private static ScaEntry toScaEntry(EntryJson e) { + if (e.vulnId == null || e.artifact == null || e.versionRanges == null || e.symbols == null) { + log.debug("SCA Reachability: skipping malformed entry: {}", e); + return null; + } + List symbols = new ArrayList<>(e.symbols.size()); + for (SymbolJson s : e.symbols) { + if (s.className == null) continue; + symbols.add(new ScaSymbol(s.className, s.method)); + } + if (symbols.isEmpty()) return null; + return new ScaEntry(e.vulnId, e.artifact, e.versionRanges, symbols); + } + /** Returns the entries associated with the given JVM internal class name, or null if none. */ public List entriesForClass(String internalClassName) { return index.get(internalClassName); @@ -116,4 +123,35 @@ private static String readAll(java.io.Reader reader) throws IOException { } return sb.toString(); } + + // --------------------------------------------------------------------------- + // JSON DTOs — only used during parsing, never exposed outside this class + // --------------------------------------------------------------------------- + + static final class DatabaseJson { + int version; + @Nullable List entries; + } + + static final class EntryJson { + @Json(name = "vuln_id") + @Nullable + String vulnId; + + @Nullable String artifact; + + @Json(name = "version_ranges") + @Nullable + List versionRanges; + + @Nullable List symbols; + } + + static final class SymbolJson { + @Json(name = "class") + @Nullable + String className; + + @Nullable String method; + } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java index 9d9e33a17f8..f1fc2382640 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaEntry.java @@ -1,18 +1,11 @@ package com.datadog.appsec.sca; -import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** One entry from sca_cves.json: a vulnerability affecting a specific Maven artifact. */ public final class ScaEntry { - private static final Logger log = LoggerFactory.getLogger(ScaEntry.class); - private final String vulnId; private final String artifact; private final List versionRanges; @@ -51,39 +44,4 @@ public List symbols() { public boolean isVersionVulnerable(String version) { return VersionRangeParser.matchesAny(version, versionRanges); } - - @Nullable - static ScaEntry fromMap(Map map) { - try { - String vulnId = (String) map.get("vuln_id"); - String artifact = (String) map.get("artifact"); - List rawRanges = (List) map.get("version_ranges"); - List rawSymbols = (List) map.get("symbols"); - - if (vulnId == null || artifact == null || rawRanges == null || rawSymbols == null) { - log.debug("SCA Reachability: skipping malformed entry: {}", map); - return null; - } - - List versionRanges = new ArrayList<>(rawRanges.size()); - for (Object r : rawRanges) { - versionRanges.add((String) r); - } - - List symbols = new ArrayList<>(rawSymbols.size()); - for (Object rawSymbol : rawSymbols) { - Map symbolMap = (Map) rawSymbol; - String className = (String) symbolMap.get("class"); - String method = (String) symbolMap.get("method"); - if (className == null) continue; - symbols.add(new ScaSymbol(className, method)); - } - - if (symbols.isEmpty()) return null; - return new ScaEntry(vulnId, artifact, versionRanges, symbols); - } catch (Exception e) { - log.debug("SCA Reachability: skipping malformed entry", e); - return null; - } - } } From d4698211de0090d866a00e2292ed73616b2e2a9e Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 16:13:26 +0200 Subject: [PATCH 33/35] cleanup: remove stale Path A/B terminology after Path B was removed --- .../sca/ScaReachabilityTransformer.java | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index d2dc3543954..52eaf7c1d42 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -48,10 +48,6 @@ *
      • Each (vulnId, artifact, symbolName) tuple is reported at most once — RFC requires a single * occurrence. Class-level dedup lives in {@link #reportedHits}; method-level dedup lives in * {@code ScaReachabilityCallback.reported} (bootstrap-side, persists across retransforms). - *
      • Path B (JDK classes such as {@code java.sql.PreparedStatement}) is handled only in {@link - * #checkAlreadyLoadedClasses}, not in {@link #transform}, because JDK classes are always - * loaded at startup. If a JDK class relevant to a CVE were loaded lazily after startup, the - * detection would be missed. This is a known, documented trade-off. *
      */ public final class ScaReachabilityTransformer implements ClassFileTransformer { @@ -111,8 +107,8 @@ public byte[] transform( return null; } - // JDK/bootstrap classes (protectionDomain == null) are handled at startup in - // checkAlreadyLoadedClasses() via Path B. Skip here to avoid per-bootstrap-class overhead. + // JDK/bootstrap classes (protectionDomain == null) are skipped — they are loaded regardless + // of which library is present and are not reliable reachability indicators. if (protectionDomain == null) { return null; } @@ -234,17 +230,9 @@ private void scheduleRetransformByName(String internalClassName) { /** * Checks classes already loaded before this transformer was registered. * - *

      Path A: 3rd-party classes — version resolved from the class's own JAR via {@link - * ProtectionDomain}. - * - *

      Path B: JDK/standard-library classes (e.g. {@code java.sql.PreparedStatement}) — {@code - * ProtectionDomain} is null, so we scan the system classloader's URL chain for the associated - * Maven artifact. - * - *

      Assumption: JDK-sourced symbols in vulnerability data are loaded at startup, not - * lazily during normal application operation. If an application defers JDK class loading past - * agent startup (e.g. lazy JDBC initialisation), Path B hits for those classes will be missed. - * See APPSEC-62260 for design rationale. + *

      Only processes 3rd-party classes (non-null {@link ProtectionDomain} with a code source). JDK + * classes are skipped: they are always loaded regardless of which library is in the classpath and + * would produce false positives if used as reachability proxies. See APPSEC-62260. */ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { for (Class clazz : instrumentation.getAllLoadedClasses()) { @@ -260,16 +248,11 @@ public void checkAlreadyLoadedClasses(Instrumentation instrumentation) { ProtectionDomain pd = clazz.getProtectionDomain(); URL location = locationOf(pd); if (location == null) { - // JDK/bootstrap class (no code source): skip. JDK symbols in the database (e.g. - // java.sql.PreparedStatement in the PostgreSQL advisory) are loaded by ANY app that - // uses JDBC regardless of which driver is present — reporting them via classpath scan - // would be a false positive (classpath-presence, not runtime reachability). Library- - // specific classes in the same entry (e.g. org.postgresql.ds.PGSimpleDataSource) are - // detected reliably via Path A when they are actually loaded. + // JDK/bootstrap class (no code source): skip — false positive, see class Javadoc. continue; } try { - processPathA(internalName, location, entries); // 3rd-party class + processClass(internalName, location, entries); // If any entry for this class has method-level symbols, the class needs retransformation // so the bytecode callback can be injected. We can't modify bytecode here (we're just // scanning) — retransformation is deferred to performPendingRetransforms(). @@ -342,8 +325,7 @@ public void performPendingRetransforms() { // Internal matching logic // --------------------------------------------------------------------------- - /** Path A: class came from a 3rd-party JAR — match artifact + check version. */ - private void processPathA(String internalClassName, URL jarUrl, List entries) { + private void processClass(String internalClassName, URL jarUrl, List entries) { List classJarDeps = resolveDependencies(jarUrl); for (ScaEntry entry : entries) { String version = resolveVersionForArtifact(entry.artifact(), classJarDeps); From ab5850b87b6623494c0ba37880565d272bc94c0d Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 16:22:33 +0200 Subject: [PATCH 34/35] chore: remove .claude-invariants.md from tracking, add to .gitignore --- .claude-invariants.md | 353 ------------------------------------------ .gitignore | 2 + 2 files changed, 2 insertions(+), 353 deletions(-) delete mode 100644 .claude-invariants.md diff --git a/.claude-invariants.md b/.claude-invariants.md deleted file mode 100644 index 9021a2c8832..00000000000 --- a/.claude-invariants.md +++ /dev/null @@ -1,353 +0,0 @@ -# Pre-code Invariants: SCA Reachability - -**Generado:** 2026-05-12 -**Tarea:** Implementar SCA Reachability en dd-trace-java: Gradle task que descarga GHSA JSONs y genera `sca_cves.json`; `ClassFileTransformer` en `dd-java-agent/appsec/` que detecta class load de clases vulnerables; reporte via `app-dependencies-loaded` telemetry con campo `metadata` (`{type:"reachability", value: stringified JSON}`). Feature flag: `DD_APPSEC_SCA_ENABLED`. -**Ejemplo de referencia:** `telemetry/src/main/java/datadog/telemetry/dependency/LocationsCollectingTransformer.java` (patron ClassFileTransformer observation-only), `dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java` (patron startup con Instrumentation) -**Frameworks analizados:** telemetry pipeline, AppSec startup, ClassFileTransformer JVM API, Gradle build system, ComparableVersion - ---- - -## 1. Patron canonico para este tipo de cambio - -### Patron ClassFileTransformer observation-only (referencia: LocationsCollectingTransformer) - -```java -// Nunca modificar el bytecode - siempre devolver null -// Nunca lanzar excepciones - catch all internamente -// Null-check obligatorio en protectionDomain Y codeSource -public byte[] transform( - ClassLoader loader, - String className, // FORMATO INTERNO: "com/foo/Bar" con slashes - Class classBeingRedefined, - ProtectionDomain protectionDomain, - byte[] classfileBuffer) { - - if (protectionDomain == null) return null; // bootstrap classes - CodeSource cs = protectionDomain.getCodeSource(); - if (cs == null) return null; // runtime-generated classes - - // className llega en formato interno - convertir para lookup: className.replace('/', '.') - if (!vulnerableClasses.containsKey(className)) return null; // early return rapido - - URL location = cs.getLocation(); - if (location == null) return null; - - // ... queue hit para procesamiento asincrono - return null; // SIEMPRE null - no modificamos bytecode -} -``` - -### Patron de startup (referencia: IastSystem / Agent.java) - -```java -// En Agent.java execute(): -maybeStartAppSec(scoClass, sco); -maybeStartScaReachability(instrumentation, scoClass, sco); // NUEVO - despues de AppSec -maybeStartCiVisibility(instrumentation, scoClass, sco); - -// En maybeStartScaReachability(): -private static void maybeStartScaReachability( - Instrumentation instrumentation, Class scoClass, Object sco) { - if (!Config.get().isAppSecScaEnabled()) return; - // ... cargar sca_cves.json, construir indices, registrar transformer - instrumentation.addTransformer(transformer, true); // true = canRetransform - // Para clases ya cargadas: - checkAlreadyLoadedClasses(instrumentation, transformer); -} -``` - ---- - -## 2. Invariantes por categoria - -### Telemetria: Dependency class y serializacion - -| Invariante | Por que | Fuente | -|---|---|---| -| `Dependency` es `final` - no se puede subclasear | Diseno intencional de la clase | `Dependency.java` | -| `Dependency` tiene exactamente 4 campos publicos: `name`, `version`, `source`, `hash` | Ver declaracion de clase | `Dependency.java:44-47` | -| El campo `source` NO se serializa en el JSON de telemetria | Solo `hash`, `name`, `version` van al payload | `TelemetryRequestBody.java:269-274` | -| La serializacion usa **Moshi JsonWriter** (NO Jackson) | Ver imports de `TelemetryRequestBody` | `TelemetryRequestBody.java:264` | -| Para anadir `metadata`: hay que anadir campo a `Dependency` Y actualizar `writeDependency()` | `Dependency` es inmutable - nuevo constructor con metadata opcional | Investigacion | -| El campo `metadata.value` en el payload DEBE ser un JSON string (no objeto) | RFC lo especifica explicitamente: "MUST be serialized into a JSON string" | RFC | -| Heartbeat interval: 60s por defecto | Los hits de SCA se reportan con hasta 60s de delay | `TelemetryRunnable` / Config | -| Limite de 2000 dependencias en extended heartbeat | `ExtendedHeartbeatData` cap | `ExtendedHeartbeatData.java` | -| `DependencyService.installOn(instrumentation)` se llama DENTRO de `startTelemetry()` | `startTelemetry()` se ejecuta DESPUES de `maybeStartAppSec()` | `Agent.java:684,692` / `TelemetrySystem.java:45` | - -### Condicion fundamental de instrumentacion/reporte - -**Una clase que aparece en `sca_cves.json` NO se instrumenta ni reporta por defecto. Solo se actua sobre ella cuando se cumplen las TRES condiciones simultaneamente:** - -1. **El nombre de la clase esta en el indice** — `database.entriesForClass(className)` devuelve entries no vacios. -2. **La dependencia esta cargada** — `DependencyResolver.resolve(jarUrl)` (o el scan del classpath en Path B) devuelve al menos un `Dependency` cuyo `name` coincide con `entry.artifact()`. -3. **La version es vulnerable** — `entry.isVersionVulnerable(dep.version)` devuelve `true`, es decir, la version del JAR cargado cae dentro de los rangos de `sca_cves.json`. - -La presencia de una clase en el indice es condicion necesaria pero NO suficiente. Si la aplicacion no tiene cargada la libreria vulnerable (o tiene una version parcheada), el transformer no hace nada para esa clase aunque su nombre aparezca en el indice. - -**Implicacion para el indice de `ScaCveDatabase`:** el indice mapea class name → entries para lookup rapido O(1), pero entries puede contener vulnerabilidades de multiples artifacts distintos. El filtrado por artifact+version ocurre siempre en `processClass()` / `processPathA()` / `processPathB()`, NUNCA en el indice mismo. - -**Implicacion para el retransform periodico:** las clases en `pendingRetransform` / `pendingRetransformNames` se retransforman pero si en el momento del retransform la version sigue sin resolverse o no coincide, `transform()` devuelve null sin reportar nada. No es un bug — es el comportamiento correcto. - -### ClassFileTransformer: formato y threading - -| Invariante | Por que | Fuente | -|---|---|---| -| `className` en `transform()` llega en formato INTERNO con slashes: `"com/foo/Bar"` | Formato JVM interno, no dot-separated | `ClassFileTransformer` JVM spec | -| El map de lookup DEBE usar formato con slashes como clave | Para evitar conversion en el hot path | `IastSecurityControlTransformer.java:36` | -| Devolver `null` = no modificar bytecode (el JVM conserva el original) | Documentado en `LocationsCollectingTransformer.java:35` | `LocationsCollectingTransformer.java:35` | -| `protectionDomain` puede ser null (clases del bootstrap classloader) | Bootstrap classes no tienen ProtectionDomain | `LocationsCollectingTransformer.java:31` | -| `protectionDomain.getCodeSource()` puede devolver null (clases generadas en runtime) | Proxies dinamicos, lambdas, etc. | `LocationsCollectingTransformer.java:40` | -| `transform()` se llama desde multiples threads concurrentemente | JVM class loading es multi-threaded | JVM spec | -| Para detectar clases ya cargadas: `instrumentation.getAllLoadedClasses()` filtradas por el set | Necesario con `canRetransform=true` | `Instrumentation` javadoc | -| `addTransformer(transformer, true)` requerido para poder retransformar clases ya cargadas | El segundo parametro es `canRetransform` | `Instrumentation.addTransformer()` | -| Al retransformar clases ya cargadas, llamar `instrumentation.retransformClasses(classes[])` | Dispara el transformer sobre clases ya loaded | `ConfigurationUpdater.java` (Debugger) | - -### AppSec startup: punto de inyeccion - -| Invariante | Por que | Fuente | -|---|---|---| -| `AppSecSystem.start()` NO recibe `Instrumentation` | Solo recibe `SubscriptionService` y `SharedCommunicationObjects` | `AppSecSystem.java:49` | -| El patron correcto es una funcion `maybeStartScaReachability(Instrumentation, ...)` separada en `Agent.java` | Mismo patron que IAST (`maybeStartIast(instrumentation)`) | `Agent.java:670,1082` | -| Inyectar DESPUES de `maybeStartAppSec()` en `execute()` (linea 684) | AppSec debe estar iniciado antes; `instrumentation` disponible en `execute()` via campo de la clase externa | `Agent.java:684-693` | -| `Config.get().isAppSecScaEnabled()` es la gate correcta | Ya existe y hace null-check de `Boolean` | `Config.java:5761` | -| La llamada en `Agent.java` usa reflexion para `AppSecSystem.start()` | Ver `startAppSec()` - usa `getMethod().invoke()` | `Agent.java:1055-1064` | - -### Version matching - -| Invariante | Por que | Fuente | -|---|---|---| -| `ComparableVersion` ya existe en `internal-api` - backport de Apache Maven 3.9.9 | No hace falta dependencia externa | `internal-api/src/main/java/datadog/trace/util/ComparableVersion.java` | -| `isWithin(start, end)` comprueba `[start, end)` - inclusivo start, exclusivo end | Ver implementacion: `compareTo(start) >= 0 && compareTo(end) < 0` | `ComparableVersion.java:130-132` | -| Los rangos GHSA usan formato: `"< 2.6.7.3"`, `">= 2.7.0, < 2.7.9.5"`, `"= 9.5.0"` | Formato propio del database | GHSA enrichments JSONs | -| Hay que implementar un `VersionRangeParser` para estos strings - no existe en el codebase | No hay utilidad de parsing de rangos | Investigacion | -| Versiones de 4 partes (`2.6.7.3`) son soportadas por `ComparableVersion` | Backport de Maven que las soporta nativamente | `ComparableVersion.java` | -| Para rango `"< X"`: equivale a `isWithin(ZERO, X)` o `compareTo(X) < 0` | isWithin no cubre este caso directamente | Logica | -| Para rango `"= X"`: equivale a `compareTo(X) == 0` | isWithin es half-open, no sirve para exact match | Logica | - -### Gradle: bundling de recursos generados - -| Invariante | Por que | Fuente | -|---|---|---| -| `appsec/build.gradle` YA tiene hook `processResources` con minificacion JSON | Ver `doLast { fileTree... JsonOutput.toJson... }` | `dd-java-agent/appsec/build.gradle:41-56` | -| El patron para recursos generados: `sourceSets.main.output.dir(generatedDir, builtBy: task)` | Ver ejemplo en `mule-4.5/build.gradle:253-272` | `mule-4.5/build.gradle` | -| NO hay plugin de descarga externo configurado - usar Java `URL` en tarea Gradle o anadir `de.undercouch.gradle-download-plugin` | No se usa en el resto del proyecto | `build.gradle.kts` raiz | -| `buildSrc` tiene Jackson disponible para procesamiento JSON en tareas Gradle | Ver `buildSrc/build.gradle.kts` dependencies | `buildSrc/build.gradle.kts` | -| Recursos en `src/main/resources/` o directorio generado son accesibles via `getResourceAsStream()` | Comportamiento estandar de JAR | Java spec | - -### Formato GHSA: parsing del database - -| Invariante | Por que | Fuente | -|---|---|---| -| Cada fichero GHSA es un **array JSON** (no objeto): `[{...}]` | Ver cualquier fichero del database | GHSA database | -| Solo procesar entradas con `"language": "jvm"` | Hay otros lenguajes (python, etc.) en el mismo repo | GHSA database | -| Nombre completo de clase = `value + "." + name` (e.g., `"com.fasterxml.jackson.databind"` + `"ObjectMapper"` = `"com.fasterxml.jackson.databind.ObjectMapper"`) | Separacion en el formato GHSA | GHSA `GHSA-645p-88qh-w398.json` | -| Los ficheros NO contienen CVE ID directamente - solo el GHSA ID es el nombre del fichero | Para el `id` en el payload, usar GHSA ID o enriquecer con CVE API | GHSA database | -| Version ranges estan en `package[].version_range[]` (array de strings) | Cada paquete afectado puede tener multiples rangos | GHSA database | -| Simbolos en `ecosystem_specific.imports[].symbols[]` - puede haber multiples arrays `imports` | Iterar todos los imports, no solo el primero | GHSA database | - ---- - -## 3. Reglas "nunca hagas X" - -- **NUNCA** modifiques el bytecode para class-level symbols - siempre devuelve `null`. Para method-level SÍ devuelve bytecode modificado (inyeccion ASM). -- **NUNCA** lances excepciones desde `transform()` - cualquier error debe ser capturado internamente; lanzar aqui romperia la carga de la clase. -- **NUNCA** hagas I/O bloqueante en el hot path de `transform()` (no abrir JARs en cada llamada) - usar cache `ConcurrentHashMap>`. Solo cachear resultados NO vacios — los vacios permiten reintento en el retransform periodico. -- **NUNCA** pongas dedup en `injectCallbacks()` del ASM MethodVisitor**: `retransformClasses()` parte siempre de los bytes ORIGINALES del classfile. Si el dedup bloquea la reinyeccion, la retransformacion devuelve los bytes originales sin callback — el callback queda eliminado. La unica dedup autorizada para method-level es `ScaReachabilityCallback.reported` (bootstrap), que persiste entre retransformaciones. Para class-level la dedup vive en `reportedHits` del transformer (se aplica en `reportHit()`, nunca en la ruta ASM). -- **NUNCA** reportes el mismo (vulnId, artifact, symbolName) mas de una vez. Dedup por nivel: class-level → `reportedHits` en transformer; method-level → `ScaReachabilityCallback.reported` en bootstrap. -- **NUNCA** uses el `source` field de `Dependency` para metadata - no se serializa en el JSON de telemetria. -- **NUNCA** uses Moshi fuera del modulo `telemetry/` para serializar - dentro del modulo appsec usar `JsonOutput.toJson()` (Groovy/Gradle) o implementar manualmente para el stringified JSON del payload. -- **NUNCA** uses `isWithin()` para exact match (`= X`) - `isWithin` es half-open `[start, end)`. -- **NUNCA** registres el transformer con `canRetransform=false` si quieres detectar clases ya cargadas - necesitas `true` para poder llamar `retransformClasses()`. -- **NUNCA** escribas al span/trace - el RFC prohibe explicitamente esto: "This RFC does not write anything to traces/spans." -- **NUNCA** uses `java.nio.*` en codigo que corre durante premain** (incluido `StandardCharsets.UTF_8`) -- **NUNCA** uses clases JDK (protectionDomain == null) como indicador de reachability de un artifact de terceros. Son cargadas por todos los JVMs que usan esa API (ej: `PreparedStatement` por cualquier app JDBC). Ignorarlas silenciosamente en `checkAlreadyLoadedClasses()`. -- **NUNCA** hagas el stack walk para capturar el callsite en el bootstrap classloader** (`ScaReachabilityCallback`). Bootstrap debe ser minimo. El stack walk va en el handler registrado en `ScaReachabilitySystem` (appsec module, con acceso a `AbstractStackWalker.isNotDatadogTraceStackElement`).: puede triggear premature provider initialization antes de que la aplicacion configure el runtime. Usar el string `"UTF-8"` directamente en `new InputStreamReader(stream, "UTF-8")`. Ver `docs/bootstrap_design_guidelines.md`. - ---- - -## 4. Edge cases que los tests revelan - -- **Clases bootstrap**: cargadas por el bootstrap classloader tienen `protectionDomain == null` - deben ser ignoradas silenciosamente. -- **Lambdas y proxies dinamicos**: pueden tener `codeSource == null` - ignorar. -- **JARs sin pom.properties**: la version puede no resolverse. Estos JARs no deben ser reportados (sin version = sin match de rango). -- **Artifacts transitivos / starter POMs**: la clase del simbolo puede venir de un JAR DIFERENTE al artifact vulnerable (ej: `spring-boot-starter-web` vigila `@Controller` pero `@Controller` esta en `spring-context.jar`). `resolveDependencies(jarUrl)` de la clase encontraria `spring-context`, no `spring-boot-starter-web` → hit perdido. **Solucion**: `resolveVersionForArtifact(artifactName, classJarDeps)` hace dos pasos: (1) busca en el JAR de la clase; (2) si no encuentra, llama a `findArtifactVersionInClasspath(artifactName)` con cache. Esta es la regla correcta para TODOS los lookups de version (tanto en `processClass` como en `processPathA`). -- **Version desconocida vs version en rango**: distinguir entre "no se pudo obtener la version" y "la version es X pero no esta en el rango afectado". -- **Multiples CVEs por clase**: una clase puede aparecer en varios CVEs si la misma clase pertenece a una libreria con multiples vulnerabilidades - hay que reportar TODOS los CVEs para esa clase. -- **Multiples classloaders** (Gap 6 — no requiere cambios): en app servers la misma clase se carga por ClassLoaders distintos con JARs potencialmente en versiones distintas. El diseno lo maneja correctamente por composicion: (1) el version check es por ProtectionDomain/URL, no por nombre de clase; (2) el cache de version es por URL, distingue JARs distintos; (3) el set de ya-reportados es por `(artifact, vuln_id)` — reportar una vez es suficiente segun el RFC aunque multiples classloaders tengan la version vulnerable. Documentar con comentario de codigo que aclare este comportamiento intencional. -- **Estado compartido entre transformer y periodic action** (Gap 9): NO se necesita `ScaReachabilityService` separado. El transformer es instancia unica y lleva su propio estado: `Map> index` (inmutable), `ConcurrentHashMap> versionCache`, `Set reportedHits = ConcurrentHashMap.newKeySet()`. La unica pieza compartida con el exterior es `ScaReachabilityCollector.INSTANCE` (internal-api, Gap 8) donde el transformer deposita los hits y la periodic action los drena. -- **Multiples version_range por paquete**: una CVE puede afectar a rangos discontinuos de versiones (ej: `< 2.6.7.3` Y `>= 2.7.0, < 2.7.9.5`). El check es OR: la version esta afectada si cae en CUALQUIERA de los rangos. -- **`line` field**: RESUELTO — backend acepta `1` como placeholder para class-level detection. -- **Tipos array**: el transformer recibe `[Ljava/sql/PreparedStatement;` y similares. Filtrar con early return: `if (className == null || className.charAt(0) == '[') return null` antes del lookup. -- **Callsite para method-level** (mirroriza el Python tracer dd-trace-py#17156): `path`/`symbol`/`line` en el payload telemetría deben representar el frame de la APLICACION que invocó el método vulnerable, no el método vulnerable en sí. `ScaReachabilitySystem.findCallsite(dotClassName)` sube el stack usando `AbstractStackWalker.isNotDatadogTraceStackElement` para saltar frames agente/JDK, y busca el primer frame de aplicacion despues de los frames de la clase vulnerable. Fallback: si no encuentra frame de aplicacion, reporta el simbolo vulnerable para que el backend sepa que fue alcanzado. -- **Clases test en namespace agente**: las clases de test en `com.datadog.appsec.*` son filtradas por `isNotDatadogTraceStackElement` como codigo de agente. En tests, `findCallsite()` devuelve null y el fallback reporta el simbolo vulnerable. Esto es correcto y esta documentado en los tests. En produccion, la clase vulnerable es siempre una libreria de terceros (ej: `com.fasterxml.jackson.*`) y el callsite se captura correctamente. -- **`ScaReachabilityCallback` debe ser minimo** (bootstrap classloader): solo dedup (`reported.add(key)`) y dispatch al handler. Sin stack walk, sin dependencias del agente. El callsite se captura en el handler en `ScaReachabilitySystem` que tiene acceso a `internal-api`. -- **`retransformClasses()` — solo para method-level, nunca para class-level**: dispara TODOS los transformers registrados (Byte Buddy, IAST, Debugger...), no solo el nuestro. Para el startup scan de class-level usar `getAllLoadedClasses()` + `Class.getProtectionDomain()` directamente (sin pipeline de transformacion). Para method-level SÍ se usa (via `performPendingRetransforms()`) porque necesitamos modificar el bytecode — el coste es acotado a las clases vulnerables especificas. -- **Dos caminos de matching segun el origen de la clase** (Gap 4 — resuelto): - - **Camino A - Clase 3rd-party** (ej: `com.fasterxml.jackson.databind.ObjectMapper`): - ``` - class load → protectionDomain.getCodeSource() → jackson-databind-2.8.5.jar - → leer pom.properties del JAR → groupId:artifactId + version - → comprobar si artifact coincide con el del CVE Y version esta en rango - → reportar si match - ``` - - **Camino B (ELIMINADO — false positive risk)**: Las clases JDK como `java.sql.PreparedStatement` son cargadas por CUALQUIER app que use JDBC, independientemente del driver que use (MySQL, H2, PostgreSQL, etc.). Detectarlas como indicador de presencia de la librería vulnerable (ej: postgresql.jar en classpath) produce falsos positivos de presencia en classpath, no de reachability real. Los entries del database que incluyen símbolos JDK también incluyen símbolos específicos de la librería (ej: `org.postgresql.ds.PGSimpleDataSource`) que Camino A detecta correctamente. Path B eliminado de `checkAlreadyLoadedClasses()`. - -- **SOLO Camino A en el startup scan**: clases con `protectionDomain == null` (JDK) se IGNORAN en `checkAlreadyLoadedClasses()`. No se escanea el classpath como proxy de "la librería está presente". Solo se reporta cuando la clase de la librería SE CARGA REALMENTE (Camino A). - ---- - -## 5. Checklist de implementacion especifico - -### Fase 1: sca_cves.json (Gradle task) - -#### IDs de vulnerabilidad -- **GHSA ID = identificador de vulnerabilidad**. Se usa directamente como `vuln_id` en `sca_cves.json` y en el payload de telemetria. No se necesita conversion a CVE ID. -- El GHSA ID se extrae del nombre del fichero: `GHSA-645p-88qh-w398.json` → `"GHSA-645p-88qh-w398"` -- No hay llamadas a la GitHub Advisory API, no hay cache de mapping, no hay dependencia de red en el Gradle task para IDs. - -#### Enumeracion de ficheros GHSA -- Para listar todos los ficheros del directorio usar la GitHub Contents API: `GET https://api.github.com/repos/DataDog/sca-reachability-database/contents/enrichments` -- Devuelve array de objetos con `name` (nombre del fichero) y `download_url` (URL raw para descargarlo) -- Usar `GITHUB_TOKEN` si disponible para evitar rate limiting (60 req/hora sin token) -- Si la API no esta disponible: `GradleException` con mensaje claro (no hay fallback) - -#### Brechas entre enrichments y RFC contract (requieren decision del equipo — ver APPSEC-62260) -- **Sin IDs en el fichero**: GHSA ID viene del nombre del fichero — RESUELTO: usar GHSA ID directamente. -- **Root structure distinta**: enrichments son `[{...}]` (array), RFC espera `{"targets":[...]}`. Normalizar en el task. -- **N packages por GHSA**: una entrada puede afectar a 5+ artifacts distintos con rangos de version diferentes (ej: Spring4Shell). El task expande cada entrada en N registros en `sca_cves.json`, uno por artifact. -- **Clases de la JDK como simbolos**: el enrichment de PostgreSQL lista `java.sql.PreparedStatement`, `java.sql.Connection`, etc. (clases JDK, no del driver). Decision pendiente: ¿filtrar `java.*`/`javax.*`? — pregunta abierta en Jira. -- **Solo class-level symbols**: 131/131 simbolos JVM son `type:"class"`, ningun metodo. Decision pendiente: ¿soportar class-load como signal? — pregunta abierta en Jira. - -#### Formato de sca_cves.json — soporta class-level Y method-level desde el primer dia - -Method-level llegara en el futuro (timeline desconocido). El formato debe soportar ambos para no migrar despues: -```json -{ - "version": 1, - "entries": [ - { - "vuln_id": "GHSA-645p-88qh-w398", - "artifact": "com.fasterxml.jackson.core:jackson-databind", - "version_ranges": ["< 2.6.7.3", ">= 2.7.0, < 2.7.9.5"], - "symbols": [ - {"class": "com/fasterxml/jackson/databind/ObjectMapper", "method": null}, - {"class": "com/fasterxml/jackson/databind/ObjectMapper", "method": "readValue"} - ] - } - ] -} -``` -- `method: null` → class-level: transformer devuelve null (observation only), reporta `symbol=""`, `line=1` -- `method: "readValue"` → method-level (futuro): transformer inyecta callback en bytecode con ASM, reporta `symbol="readValue"` + linea real -- Clases en formato interno con slashes (evita conversion en hot path) -- Actualmente el database solo genera entradas con `method: null`; el campo existe para extensibilidad - -#### Checklist de implementacion -- [ ] Task lista ficheros via GitHub Contents API (`GET .../contents/enrichments`), descarga cada uno -- [ ] Solo procesa entradas con `"language": "jvm"` -- [ ] Expande entradas multi-package en N registros (uno por artifact) -- [ ] Por cada simbolo GHSA: genera `{"class": "com/foo/Bar", "method": null}` (siempre null hoy, campo reservado para metodos futuros) -- [ ] Clase en formato interno: `(value + "." + name).replace('.', '/')` -- [ ] Itera TODOS los `imports[]` de `ecosystem_specific` -- [ ] Output: `sca_cves.json` en `build/generated-resources/main/` -- [ ] Wired en `processResources` de `appsec/build.gradle` (sigue el patron existente del hook) -- [ ] JSON minificado (sigue el patron de `JsonOutput.toJson()` del hook existente) - -### Fase 2: Runtime - Version matching -- [ ] `VersionRangeParser` parsea strings GHSA: operadores `<`, `<=`, `>`, `>=`, `=`, separados por coma -- [ ] Usa `ComparableVersion` para comparacion (no reimplementar) -- [ ] `isWithin(start, end)` para rangos `[start, end)` -- [ ] `compareTo() < 0` para `< X` -- [ ] `compareTo() == 0` para `= X` -- [ ] Handles version null/empty (no match) -- [ ] **Gap 10 (version qualifiers) — no requiere codigo extra**: `ComparableVersion` normaliza `RELEASE`/`GA`/`FINAL` → release limpio por ser backport de Maven 3.9.9. `5.2.20.RELEASE == 5.2.20`. Escribir test unitario que cubra explicitamente: `5.2.19.RELEASE < 5.2.20.RELEASE`, `5.2.20.RELEASE == 5.2.20`, `5.2.20 < 5.2.20.RELEASE` (false). - -### Fase 3: ClassFileTransformer -- [ ] Index principal: `Map>` keyed por class name en formato INTERNO (slashes) -- [ ] `SymbolInfo` contiene: `vulnId`, `artifact`, `versionRanges`, `method` (null = class-level) -- [ ] `transform()`: primero filtrar `className == null || className.charAt(0) == '['` → `return null` -- [ ] `transform()`: si `protectionDomain == null` → `return null` (clase JDK/bootstrap, cubierta en startup scan) -- [ ] `transform()`: si `codeSource == null || location == null` → `return null` -- [ ] `transform()` bifurca segun `method`: - - `method == null` → class-level (Camino A): `return null`, encola hit para version check - - `method != null` → method-level (futuro): inyectar callback con ASM, `return transformedBytecode` -- [ ] `transform()` no lanza excepciones (catch all — nunca romper carga de clase) -- [ ] Cache de version por JAR URL: `ConcurrentHashMap>` -- [ ] Version resolution sincrona desde pom.properties del JAR (una vez por JAR, cacheado) -- [ ] Check de ya-reportado: `ConcurrentHashSet` de pares "artifact:vuln_id" ya enviados -- [ ] `addTransformer(transformer, true)` (necesario para method-level futuro que si modifica bytecode) -- [ ] `checkAlreadyLoadedClasses()` al startup: - - Camino A: `getAllLoadedClasses()` + `Class.getProtectionDomain()` — NO `retransformClasses()` - - Camino B: para clases JDK en el set, escanear `URLClassLoader.getURLs()` buscando el artifact asociado - - Comentario de codigo obligatorio: "Assumption: JDK classes (protectionDomain == null) are loaded at startup. If lazy JDBC init or similar patterns cause JDK classes to load after startup, Path B hits would be missed. See APPSEC-62260 for design rationale." - -### Fase 4: Agent startup wiring (Gap 7 — patron de reflexion) -- [ ] `maybeStartScaReachability(Instrumentation)` en `Agent.java`, llamada desde `execute()` despues de `maybeStartAppSec()` -- [ ] Gateada por `Config.get().isAppSecScaEnabled()` -- [ ] Usa `AGENT_CLASSLOADER.loadClass("com.datadog.appsec.sca.ScaReachabilitySystem").getMethod("start", Instrumentation.class).invoke(null, instrumentation)` -- [ ] `Instrumentation` no tiene problema de classloader — es interfaz JDK (bootstrap), visible desde ambos lados -- [ ] `ScaReachabilitySystem.start()` debe ser `public static void start(Instrumentation)` -- [ ] NO necesita `SubscriptionService` ni `SharedCommunicationObjects` — SCA no usa el gateway de AppSec -- [ ] Envolver en `StaticEventLogger.begin/end("ScaReachability")` siguiendo el patron de otros subsistemas - -### Fase 5: Telemetry — modelo STATEFUL (RFC heartbeat behavior) - -**INVARIANTE CRITICO — modelo stateful requerido por el RFC:** - -El RFC define un flujo con estado persistente entre heartbeats: -1. **Al detectar una clase vulnerable** (class load, version match): registrar el CVE con `reached: []` y marcar como `pendingReport=true` -2. **Al detectar un hit de metodo**: actualizar el CVE con el callsite, marcar `pendingReport=true` -3. **En cada heartbeat**: reportar TODAS las dependencias con `pendingReport=true`, incluyendo TODOS sus CVEs (con y sin reached). Luego marcar `pendingReport=false`. -4. **Heartbeats sin cambios**: `dependencies: []` - -**Por que es stateful y no stateless:** -- El backend necesita ver TODOS los CVEs de una dependencia juntos cuando cualquiera cambia -- `metadata: []` (reached vacio) senaliza que el CVE es aplicable pero no ha sido alcanzado aun -- Si un segundo CVE de la misma dependencia tiene un hit, el payload debe incluir ambos CVEs - -**NUNCA** usar el modelo stateless (solo drains de hits): produce payloads incompletos donde el backend no sabe que `cve-2` existe hasta que tiene un hit. - -**Modulo `internal-api`** — `ScaReachabilityDependencyRegistry` (singleton stateful): -- `registerCve(artifact, version, vulnId)` — llamado cuando se detecta una clase vulnerable (class load). Registra CVE con `reached=[]`, marca `pendingReport=true` -- `recordHit(artifact, version, vulnId, className, symbolName, line)` — llamado en hit de metodo. Actualiza callsite, marca `pendingReport=true`. Solo el PRIMER hit por CVE se guarda. -- `drainPendingDependencies()` — devuelve snapshot de todos los entries con `pendingReport=true`, limpia el flag - -**Modulo `telemetry`** — `ScaReachabilityPeriodicAction`: -- Llama `registry.drainPendingDependencies()` -- Para cada dependency con pendientes: reporta con TODOS sus CVEs -- Registrada en `TelemetrySystem.createTelemetryRunnable()` - -**Modulo `telemetry`** — extension de `Dependency` (sin cambios en la extension, mismo formato): -- Campo `reachabilityMetadata` (nullable `List`) -- `TelemetryRequestBody.writeDependency()` escribe `metadata` array si no null/vacio - -**Formato del payload:** -- `metadata: []` → CVE registrado pero no alcanzado aun -- `metadata: [{"type":"reachability","value":"{\"id\":\"GHSA-xxx\",\"reached\":[]}"}]` → CVE conocido, sin hit -- `metadata: [{"type":"reachability","value":"{\"id\":\"GHSA-xxx\",\"reached\":[{\"path\":\"...\",\"symbol\":\"...\",\"line\":N}]}"}]` → CVE alcanzado con callsite - ---- - -## 6. Preguntas abiertas - -1. **CVE vs GHSA ID**: RESUELTO — usar GHSA ID directamente como identificador. - -2. **`line` field**: RESUELTO — el backend acepta `1` como placeholder para class-level detection. - -3. **`symbol` para class-level**: RESUELTO — usar `""` (estandar JVM, aparece en stack traces reales). - -4. **Method-level symbols**: RESUELTO — formato `sca_cves.json` y transformer diseñados para soportar ambos desde el primer dia; implementacion method-level queda como stub. - -5. **N packages por GHSA**: RESUELTO — expandir cada entrada multi-package en N registros independientes en `sca_cves.json` (uno por artifact). Cumple el contrato RFC: cada entrada tiene exactamente un `dependency_name`. - -4. **Version resolution en transformer hot path**: Leer pom.properties del JAR en `transform()` puede ser un bottleneck si hay muchas clases del mismo JAR siendo cargadas simultaneamente. Confirmar si el approach de cache-by-URL es suficiente o si necesitamos una cola async como `DependencyService`. - -5. **Periodicidad del reporte**: Las reachability hits se reportan en el siguiente heartbeat (60s delay). Confirmar si esto es aceptable o si necesitamos un flush mas agresivo. - ---- - -## Historial - -- 2026-05-12 - Creado para tarea: SCA Reachability - Fase completa (Gradle task + ClassFileTransformer + telemetry extension) diff --git a/.gitignore b/.gitignore index efe0ddbf28b..bbb9963e403 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ out/ # Claude Code local custom settings # ##################################### .claude/*.local.* +.claude-invariants.md +.claude-status.md # Vim # ####### From 2fbf3ed86a822bcce2af603f8c78ee60ea0cb75b Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 14 May 2026 17:06:51 +0200 Subject: [PATCH 35/35] fix(forbiddenapis): replace String#split() with pre-compiled Pattern.split() --- .../com/datadog/appsec/sca/ScaReachabilityTransformer.java | 4 +++- .../main/java/com/datadog/appsec/sca/VersionRangeParser.java | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java index 52eaf7c1d42..ed7035075fd 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/ScaReachabilityTransformer.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Pattern; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.ClassWriter; @@ -53,6 +54,7 @@ public final class ScaReachabilityTransformer implements ClassFileTransformer { private static final Logger log = LoggerFactory.getLogger(ScaReachabilityTransformer.class); + private static final Pattern PATH_SEPARATOR = Pattern.compile(Pattern.quote(File.pathSeparator)); private final ScaCveDatabase database; private final Instrumentation instrumentation; @@ -536,7 +538,7 @@ String findArtifactVersionInClasspath(String artifactName) { // no longer extends URLClassLoader, so the loop above misses the main classpath. The // java.class.path system property always contains the classpath entries in this case. String classpath = System.getProperty("java.class.path", ""); - for (String entry : classpath.split(File.pathSeparator)) { + for (String entry : PATH_SEPARATOR.split(classpath)) { if (entry.isEmpty()) { continue; } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java index cfcaec396e5..a90aebb1225 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/sca/VersionRangeParser.java @@ -2,6 +2,7 @@ import datadog.trace.util.ComparableVersion; import java.util.List; +import java.util.regex.Pattern; /** * Checks whether a version string matches GHSA version range expressions. @@ -25,6 +26,8 @@ */ public final class VersionRangeParser { + private static final Pattern COMMA = Pattern.compile(","); + private VersionRangeParser() {} /** @@ -52,7 +55,7 @@ public static boolean matchesAny(String version, List versionRanges) { * single string (comma-separated) are evaluated as AND. */ static boolean matchesRange(ComparableVersion version, String versionRange) { - String[] conditions = versionRange.split(","); + String[] conditions = COMMA.split(versionRange); for (String condition : conditions) { if (!matchesCondition(version, condition.trim())) { return false;