Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
28f12e0
Implement SCA Reachability: detect vulnerable library classes at runtime
jandro996 May 12, 2026
e607887
Commit sca_cves.json as versioned resource; update generateScaCvesJso…
jandro996 May 13, 2026
fb9d011
Fix Path B classpath scan for Java 9+: fall back to java.class.path
jandro996 May 13, 2026
62b290d
Add Java 9+ test for Path B classpath fallback; make method package-p…
jandro996 May 13, 2026
93d58f2
Implement method-level symbol detection with ASM bytecode injection
jandro996 May 13, 2026
a5ccd80
Retransform classes for method-level detection: already-loaded and ve…
jandro996 May 13, 2026
f8f9d02
Fix: remove incorrect dedup from injectCallbacks; update invariants
jandro996 May 13, 2026
82ea806
pr-review: fix null guard, encapsulate periodicWorkCallback, update J…
jandro996 May 13, 2026
39eef44
Fix two Codex review issues: java.nio in premain and transitive JAR r…
jandro996 May 13, 2026
dc8ffd3
Refactor: extract CLASS_LEVEL_SYMBOL constant and reportClassLevelHit…
jandro996 May 13, 2026
849f376
Move CLASS_LEVEL_SYMBOL to ScaReachabilityHit; fix misleading comment
jandro996 May 13, 2026
3ea0e05
Move java.nio comment to usage site; add tests for transitive JAR fal…
jandro996 May 13, 2026
525a81c
Remove dead visitCode() override and redundant CLASS_LEVEL_SYMBOL alias
jandro996 May 13, 2026
7f5e116
Capture callsite for method-level hits (mirrors Python tracer)
jandro996 May 13, 2026
e0c7fee
Move callsite detection from bootstrap to ScaReachabilitySystem handler
jandro996 May 13, 2026
a79907d
Use AbstractStackWalker.isNotDatadogTraceStackElement for callsite fi…
jandro996 May 13, 2026
77ba03f
Add tests for ScaReachabilitySystem.findCallsite(); document fallback…
jandro996 May 13, 2026
7c69a89
Update ScaReachabilityHit Javadoc to reflect dual callsite/symbol sem…
jandro996 May 13, 2026
19a5813
Move findCallsite() after start() — helpers after main public method
jandro996 May 13, 2026
3b76b33
Use ConcurrentHashMap.newKeySet() instead of verbose newSetFromMap idiom
jandro996 May 13, 2026
6008ac9
Lazy entryHasMethodLevelSymbol check — avoid stream alloc on normal path
jandro996 May 13, 2026
17146c0
Remove Path B from startup scan — JDK symbols are false positive indi…
jandro996 May 13, 2026
750b3c3
Remove dead processPathB() — never called after Path B removal
jandro996 May 13, 2026
fdb74e4
Fix dedup key to include class name for method-level hits
jandro996 May 13, 2026
b90b654
Implement stateful RFC heartbeat model for SCA telemetry
jandro996 May 14, 2026
fdcb421
Add smoke test for SCA Reachability telemetry (APPSEC-62260)
jandro996 May 14, 2026
579bbd0
Add method-level symbols for jackson-databind deserialization CVEs
jandro996 May 14, 2026
9c89c1b
Add method-level symbols for xstream, log4j, snakeyaml, jackson-mappe…
jandro996 May 14, 2026
00185f1
Fix SCA smoke test, RFC compliance and add heartbeat flow tests
jandro996 May 14, 2026
b3582e8
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 14, 2026
a12d3f9
fix(smoke): add braces to if statement to satisfy CodeNarc IfStatemen…
jandro996 May 14, 2026
32fb0d1
fix(spotbugs): make periodicWorkCallback private, expose via getter
jandro996 May 14, 2026
54332fa
refactor: replace Map<?,?> casts with typed Moshi DTOs in ScaCveDatabase
jandro996 May 14, 2026
d469821
cleanup: remove stale Path A/B terminology after Path B was removed
jandro996 May 14, 2026
ab5850b
chore: remove .claude-invariants.md from tracking, add to .gitignore
jandro996 May 14, 2026
2fbf3ed
fix(forbiddenapis): replace String#split() with pre-compiled Pattern.…
jandro996 May 14, 2026
bbd32bb
fix: remove class-level symbols from all xstream entries
jandro996 May 15, 2026
d312b48
feat: emit metadata:[] for all deps in DependencyPeriodicAction when …
jandro996 May 15, 2026
d6a419e
fix(spotbugs): replace URL collections with URI to avoid DNS lookups …
jandro996 May 15, 2026
310aa66
chore: remove dead ScaReachabilityCollector, fix stale Javadoc, drop …
jandro996 May 15, 2026
e0a2067
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 15, 2026
b0421ab
refactor: unify dep reporting into ScaReachabilityPeriodicAction when…
jandro996 May 18, 2026
364fc13
fix(techdebt): static imports, remove inline java.util refs, replace …
jandro996 May 18, 2026
b529d09
fix: restore ScaReachabilityPeriodicActionTest; fix raw type Class[0]…
jandro996 May 18, 2026
90be403
fix(techdebt): move pendingRetransformNames to field section; json-es…
jandro996 May 18, 2026
5dd23b1
fix(techdebt): extract depKey helper; delete empty test stub ScaReach…
jandro996 May 18, 2026
164a838
fix(thread-safety): use AtomicReference.compareAndSet for first-hit-w…
jandro996 May 18, 2026
2e1e8ee
fix: remove stale .claude-invariants.md reference from ScaReachabilit…
jandro996 May 18, 2026
058a344
fix: wrap onMethodHit in try/catch to prevent exception propagation t…
jandro996 May 18, 2026
b425f33
fix: use knownDeps to enrich CVE snapshots with source/hash from prio…
jandro996 May 18, 2026
6925358
fix: emit CVE data immediately in Step 3, use knownDeps only for sour…
jandro996 May 18, 2026
24c53f0
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 18, 2026
0da4c37
fix(sca): force snakeyaml class load in smoke test via PostConstruct
jandro996 May 19, 2026
354cd35
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 19, 2026
5a1400a
ci: retrigger CI
jandro996 May 19, 2026
50df877
refactor(sca): remove dead markPending, inline scheduleRetransformByN…
jandro996 May 19, 2026
58df388
fix(sca): register only ScaReachabilityPeriodicAction when SCA enable…
jandro996 May 19, 2026
c327257
revert: restore pre-existing em dashes in GatewayBridge, ObjectIntros…
jandro996 May 19, 2026
ba0aa7a
fix: hoist dotClassName conversion outside inner loop in processClass
jandro996 May 19, 2026
7b449cf
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 20, 2026
05e2537
fix(sca): deduplicate index entries per class when entry has multiple…
jandro996 May 20, 2026
42039ba
fix(sca): include version in hit dedup keys to isolate multi-version …
jandro996 May 20, 2026
7eb20bf
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 20, 2026
1d29fb6
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 21, 2026
b00c531
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 21, 2026
53c36f5
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 21, 2026
ca35526
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 21, 2026
834c65c
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 21, 2026
54a7fa7
fix(sca): skip intermediate library frames in callsite detection
jandro996 May 22, 2026
6904082
fix(sca): add TODO for inner-class format in GhsaEnrichmentParser
jandro996 May 28, 2026
0255588
refactor(sca): defer class processing off class-loading thread; cleanup
jandro996 May 28, 2026
17b4b4a
refactor(sca): remove unused 4-arg convenience constructor from ScaRe…
jandro996 May 28, 2026
41af332
test(sca): add regression tests for multi-classloader retransform fix
jandro996 May 28, 2026
11fd65f
bench(sca): add SCA variant to startup and load petclinic benchmarks
jandro996 May 28, 2026
e6ba094
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 28, 2026
a2dc4fc
fix(sca): widen catch to Throwable in performPendingRetransforms
jandro996 May 29, 2026
782e028
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 29, 2026
504349a
ci: retrigger pipeline
jandro996 May 29, 2026
2de3cb2
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 29, 2026
baf5389
bench(sca): add sca variant to petclinic k6 load test
jandro996 May 29, 2026
c2a70f2
fix(sca): log swallowed exceptions in ScaReachabilityCallback at debu…
jandro996 May 29, 2026
2d83337
Merge branch 'master' into alejandro.gonzalez/sca-reachability
jandro996 May 29, 2026
7c7aff3
refactor(sca): move generateScaCvesJson into ScaEnrichmentsPlugin in …
jandro996 May 29, 2026
a8b04c3
test(sca): add ScaEnrichmentsPluginTest; fix processResources wiring
jandro996 May 29, 2026
a5e55c8
fix(sca): scope processResources JSON minification to sca_cves.json only
jandro996 May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,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
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ out/
# Claude Code local custom settings #
#####################################
.claude/*.local.*
.claude-invariants.md
.claude-status.md

# Vim #
#######
Expand Down
3 changes: 3 additions & 0 deletions benchmark/load/petclinic/k6.js
Copy link
Copy Markdown
Contributor

@sarahchen6 sarahchen6 May 29, 2026

Choose a reason for hiding this comment

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

Hi Alejandro! Our benchmarks are being migrated to the apm-sdks-benchmarks implementation, e.g. https://github.com/DataDog/apm-sdks-benchmarks/blob/main/.gitlab/ci-java-load-parallel.yml and https://github.com/DataDog/apm-sdks-benchmarks/blob/main/.gitlab/ci-java-startup-parallel.yml, so ideally no more changes are made to the local benchmarks/ folder 😅

WDYT of this PR that ports your changes here to the new implementation? https://github.com/DataDog/apm-sdks-benchmarks/pull/161 (It's tested here: #11504)

Apologies for the confusion -- my plan was to remove these dd-trace-java benchmarks (#11502) earlier, but I had been waiting for more data from the apm-sdks-benchmarks one.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const variants = {
},
"code_origins": {
"APP_URL": 'http://localhost:8085',
},
"sca": {
"APP_URL": 'http://localhost:8086',
}
}

Expand Down
1 change: 1 addition & 0 deletions benchmark/load/petclinic/start-servers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ start_server "profiling" "-javaagent:${TRACER} -Ddd.profiling.enabled=true -Dser
start_server "appsec" "-javaagent:${TRACER} -Ddd.appsec.enabled=true -Dserver.port=8083" "taskset -c 37-38 " &
start_server "iast" "-javaagent:${TRACER} -Ddd.iast.enabled=true -Dserver.port=8084" "taskset -c 39-40 " &
start_server "code_origins" "-javaagent:${TRACER} -Ddd.code.origin.for.spans.enabled=true -Dserver.port=8085" "taskset -c 41-42 " &
start_server "sca" "-javaagent:${TRACER} -Ddd.appsec.enabled=true -Ddd.appsec.sca.enabled=true -Dserver.port=8086" "taskset -c 43-44 " &
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you validate that the cores 43 and 44 are available so we can pin?

Copy link
Copy Markdown
Member Author

@jandro996 jandro996 May 29, 2026

Choose a reason for hiding this comment

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

Not explicitly - assumed it from the pattern (second socket 24–47, existing servers use up to 41–42). That said, looking at the previous benchmark run the SCA variant was missing from the load results entirely; turns out we also forgot to add it to k6.js (fixed in baf5389). Will validate once the next benchmark run includes the complete setup.


wait
2 changes: 1 addition & 1 deletion benchmark/load/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ for app in *; do

echo "Waiting for serves to start..."
if [ "${app}" == "petclinic" ]; then
for port in $(seq 8080 8085); do
for port in $(seq 8080 8086); do
healthcheck http://localhost:$port
done
elif [ "${app}" == "insecure-bank" ]; then
Expand Down
6 changes: 6 additions & 0 deletions benchmark/startup/petclinic/benchmark.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
"JAVA_OPTS": "-Ddd.appsec.enabled=true"
}
},
"sca": {
"env": {
"VARIANT": "sca",
"JAVA_OPTS": "-Ddd.appsec.enabled=true -Ddd.appsec.sca.enabled=true"
}
},
"iast": {
"env": {
"VARIANT": "iast",
Expand Down
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ gradlePlugin {
id = "dd-trace-java.instrumentation-naming"
implementationClass = "datadog.gradle.plugin.naming.InstrumentationNamingPlugin"
}

create("sca-enrichments-plugin") {
id = "dd-trace-java.sca-enrichments"
implementationClass = "datadog.gradle.plugin.sca.ScaEnrichmentsPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package datadog.gradle.plugin.sca

import datadog.gradle.sca.GhsaEnrichmentParser
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.net.HttpURLConnection
import java.net.URL
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project

/**
* Registers the [generateScaCvesJson] task that downloads GHSA enrichments from
* `sca-reachability-database` and generates `sca_cves.json` bundled in the appsec JAR.
*
* This is a **temporary** build-time approach. The symbol database will be delivered
* via Remote Config in a future iteration, at which point this plugin and the committed
* `sca_cves.json` file will be removed.
*
* Usage: `apply plugin: 'dd-trace-java.sca-enrichments'`. The task runs only when
* `-PrefreshSca` is passed or the output file is absent; CI uses the committed copy.
*/
@Suppress("unused")
class ScaEnrichmentsPlugin : Plugin<Project> {

companion object {
private const val SCA_ENRICHMENTS_API =
"https://api.github.com/repos/DataDog/sca-reachability-database/contents/enrichments"
}

override fun apply(project: Project) {
val outputFile = project.file("src/main/resources/sca_cves.json")

val generateTask =
project.tasks.register("generateScaCvesJson") {
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."
group = "build"
outputs.file(outputFile)
onlyIf { project.hasProperty("refreshSca") || !outputFile.exists() }

doLast {
val token = System.getenv("GITHUB_TOKEN")

logger.lifecycle("Fetching GHSA enrichment index from GitHub...")
@Suppress("UNCHECKED_CAST")
val fileList = githubFetch(SCA_ENRICHMENTS_API, token) as List<Map<String, Any>>
val ghsaFiles =
fileList.filter {
it["name"]?.toString()?.endsWith(".json") == true && it["type"] == "file"
}
logger.lifecycle("Found ${ghsaFiles.size} enrichment files")

val entries = mutableListOf<Any>()
ghsaFiles.forEach { fileInfo ->
val ghsaId = fileInfo["name"]!!.toString().removeSuffix(".json")
val rawContent = githubFetchRaw(fileInfo["download_url"]!!.toString(), token)
entries.addAll(GhsaEnrichmentParser.parse(ghsaId, rawContent))
}

outputFile.writeText(JsonOutput.toJson(mapOf("version" to 1, "entries" to 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.")
}
}

// Defer wiring until after the java plugin adds processResources.
project.pluginManager.withPlugin("java") {
project.tasks.named("processResources") {
dependsOn(generateTask)
doLast {
// Minify only sca_cves.json — not all JSON files in the module output.
project
.fileTree(mapOf("dir" to outputs.files.asPath, "includes" to listOf("**/sca_cves.json")))
.forEach { f -> f.writeText(JsonOutput.toJson(JsonSlurper().parse(f))) }
}
}
}
}

private fun githubConnect(url: String, token: String?): HttpURLConnection {
val connection = URL(url).openConnection() as HttpURLConnection
connection.setRequestProperty("Accept", "application/vnd.github+json")
connection.setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
if (!token.isNullOrEmpty()) {
connection.setRequestProperty("Authorization", "Bearer $token")
}
connection.connectTimeout = 10_000
connection.readTimeout = 30_000
val code = connection.responseCode
if (code != 200) {
throw GradleException(
"GitHub API returned HTTP $code for $url.\n" +
"Unauthenticated rate limit is 60 req/hr. Set GITHUB_TOKEN to raise it.")
}
return connection
}

private fun githubFetch(url: String, token: String?): Any {
val conn = githubConnect(url, token)
return try {
JsonSlurper().parse(conn.inputStream)
} finally {
conn.disconnect()
}
}

private fun githubFetchRaw(url: String, token: String?): String {
val conn = githubConnect(url, token)
return try {
conn.inputStream.bufferedReader().readText()
} finally {
conn.disconnect()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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<Map<String, Any?>> {
val root = mapper.readTree(jsonContent)
require(root.isArray) { "GHSA enrichment file $ghsaId must be a JSON array, got ${root.nodeType}" }

val entries = mutableListOf<Map<String, Any?>>()

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<Map<String, Any?>> {
val symbols = mutableListOf<Map<String, Any?>>()
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.
// TODO(APPSEC-62260): verify inner-class format when database adds method-level symbols.
// If GHSA uses dot notation for inner classes (e.g. name="Outer.Inner"), the replace below
// produces com/example/Outer/Inner instead of the correct com/example/Outer$Inner.
// When the database team defines the format, update this to handle the $ separator.
val internalName = "$pkg.$name".replace('.', '/')
symbols += mapOf("class" to internalName, "method" to null)
Comment thread
jandro996 marked this conversation as resolved.
}
}

return symbols
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package datadog.gradle.plugin.sca

import datadog.gradle.plugin.GradleFixture
import org.assertj.core.api.Assertions.assertThat
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ScaEnrichmentsPluginTest : GradleFixture() {

@BeforeEach
fun setup() {
writeSettings("""rootProject.name = "test-appsec"""")
writeRootProject(
"""
plugins {
java
id("dd-trace-java.sca-enrichments")
}
"""
)
}

@Test
fun `generateScaCvesJson is SKIPPED when file exists and refreshSca is not set`() {
file("src/main/resources/sca_cves.json").also {
it.parentFile.mkdirs()
it.writeText("{\"version\":1,\"entries\":[]}")
}

val result = run("generateScaCvesJson")

assertThat(result.task(":generateScaCvesJson")?.outcome).isEqualTo(TaskOutcome.SKIPPED)
}

@Test
fun `generateScaCvesJson attempts to run when refreshSca is set even if file exists`() {
file("src/main/resources/sca_cves.json").also {
it.parentFile.mkdirs()
it.writeText("{}")
}

// With -PrefreshSca the onlyIf condition is true; task will fail at the GitHub fetch
// (no network in tests) but must NOT be SKIPPED
val result = run("generateScaCvesJson", "-PrefreshSca", expectFailure = true)

assertThat(result.task(":generateScaCvesJson")?.outcome)
.isNotNull
.isNotEqualTo(TaskOutcome.SKIPPED)
}

@Test
fun `generateScaCvesJson attempts to run when output file does not exist`() {
// File absent: onlyIf returns true; task will fail at GitHub fetch but must not be SKIPPED
val result = run("generateScaCvesJson", expectFailure = true)

assertThat(result.task(":generateScaCvesJson")?.outcome)
.isNotNull
.isNotEqualTo(TaskOutcome.SKIPPED)
}

@Test
fun `processResources depends on generateScaCvesJson`() {
file("src/main/resources/sca_cves.json").also {
it.parentFile.mkdirs()
it.writeText("{\"version\":1,\"entries\":[]}")
}

val result = run("processResources")

// generateScaCvesJson must appear as SKIPPED (file exists, no -PrefreshSca)
assertThat(result.task(":generateScaCvesJson")?.outcome).isEqualTo(TaskOutcome.SKIPPED)
assertThat(result.task(":processResources")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
}
}
Loading