Symptom

Kraken’s nightly dependency-CVE scan opened #850: three open Dependabot alerts on org.bouncycastle:bcprov-jdk18on (GHSA-574f-3g2m-x479, critical, <= 1.80.1; GHSA-c3fc-8qff-9hwx, < 1.84) and org.bouncycastle:bcpkix-jdk18on (GHSA-wg6q-6289-32hp, < 1.84). The lead concluded “no change needed — the buildscript block already forces 1.84.”

The buildscript block does force 1.84, and it landed in #376, an ancestor of the commit Kraken scanned. The alerts were open anyway. A fix that is already present and an alert that is still open means the fix does not cover what the alert sees.

Root cause

buildscript { configurations.all { resolutionStrategy.force(...) } } in the root build.gradle.kts constrains exactly one thing: the buildscript classpath. It does not reach project configurations, which Gradle resolves independently. The bc-jdk18on-bom platform import in server/build.gradle.kts likewise constrains only :server.

Bouncy Castle is not a :server dependency at all — :server:dependencyInsight --configuration jvmRuntimeClasspath --dependency bcprov-jdk18on returns no match. It arrives through the Android Gradle Plugin, on project configurations belonging to :androidApp, :composeApp and :shared:

Those are ordinary project configurations, so the buildscript force never applied and they kept resolving 1.79. GitHub’s dependency graph reflected exactly that — the SBOM carried sdk-common:32.2.1 twice, once depending on BC 1.84 (the forced buildscript context) and once on BC 1.79 (the unguarded project context). Dependabot was reading a real, still-vulnerable resolution, not a stale one.

The alert’s “build / toolchain only — NOT on the shipped runtime classpath” classification was correct: lint and the test-platform result listener are never packaged. The real cost was three permanently-unclosable alerts, one of them critical, masking any genuine BC regression that might land later.

Fix

build.gradle.kts — added an allprojects { configurations.configureEach { resolutionStrategy.eachDependency { … } } } floor for org.bouncycastle:*-jdk18on, reading the existing bouncycastle = "1.84" ref from gradle/libs.versions.toml. This mirrors the floors already in the file for gson, guava, log4j2, httpclient and commons-lang3, and reaches project configurations the buildscript block cannot.

The rule only raises: a helper compares dot-separated numeric versions so a future 1.85 is never dragged back down to the 1.84 floor. This follows the guava rule’s conditional shape rather than gson’s unconditional useVersion.

Verified with :androidApp, :composeApp and :shared dependency trees — every 1.79 coordinate now shows 1.79 -> 1.84, and :server still resolves no Bouncy Castle at all. ./gradlew :androidApp:lintDebug is green, proving AGP’s lint tool runs against the raised classpath rather than silently needing the 1.79 API.

Prevention