Skip to content

SBOM dependency extraction is inaccurate: silent omissions and flavor over-reporting #324

Description

@hyochan

Summary

The manifest readers in scripts/sbom-dependencies.mjs produce inventories that disagree with the artifacts actually published. Verified by diffing each generated SBOM against the real published POM / nuspec.

Found while verifying #318 / #319 after merge.

1. kmp-iap silently omits its primary runtime dependency (critical)

kmp is declared as kind: "gradle-catalog", and extractGradleCatalog has exactly one matcher — libs.<alias> accessors. It never reads literal string coordinates, so the entire top-level dependencies { } block is invisible.

$ grep -n 'add("' libraries/kmp-iap/library/build.gradle.kts
332:    add("playImplementation", "io.github.hyochan.openiap:openiap-google:$googleVersion")
333:    add("horizonImplementation", "...:openiap-google-horizon:$googleVersion")
334:    add("amazonImplementation", "...:openiap-google-amazon:$googleVersion")

$ jq -r '.components[].purl' kmp-iap-3.3.0.cdx.json
pkg:maven/org.jetbrains.kotlinx/kotlinx-coroutines-core@1.11.0
pkg:maven/org.jetbrains.kotlinx/kotlinx-datetime@0.8.0
pkg:maven/org.jetbrains.kotlinx/kotlinx-serialization-json@1.11.0

The published POM (kmp-iap-android-play-3.3.0.pom) declares io.github.hyochan.openiap:openiap-google:3.3.0 at scope=runtime. That dependency transitively carries Play Billing, Gson, AndroidX and Compose into every Android consumer.

No error is raised. This is the exact failure security/SBOM.md promises cannot happen: "a coordinate they cannot resolve raises an error instead of being dropped."

2. Flavored Gradle configurations are over-reported

isRuntimeGradleConfiguration accepts any *Api / *Implementation, so horizonApi and amazonApi are treated as shipping in the default artifact. But packages/google/openiap/build.gradle.kts publishes one flavor per coordinate — openiap-google ← playRelease, openiap-google-horizon ← horizonRelease, openiap-google-amazon ← amazonRelease.

Result: the openiap-google SBOM lists 6 dependencies that are not in the published artifact (published POM has 10 deps; the SBOM has 15).

3. NuGet ItemGroup Condition is ignored

extractNuget matches every <PackageReference> with no awareness of the enclosing <ItemGroup Condition=...>. OpenIap.Maui.csproj:112 opens a Horizon-only group; the default flavor is play.

published nuspec deps: 22   SBOM: 24
IN SBOM, NOT PUBLISHED:
  + Xamarin.KotlinX.Serialization.Json@1.11.0.1
  + Xamarin.KotlinX.Serialization.Json.Jvm@1.11.0.1

4. Version ranges are asserted as exact versions

extractPub and extractNpm strip the range operator and treat the remainder as the version.

pubspec declares:  http: ^1.2.0    meta: ^1.11.0    platform: ^3.1.4
SBOM asserts:      http@1.2.0      meta@1.11.0      platform@3.1.4
actually resolves: http 1.6.0      meta 1.19.0      platform 3.1.6

A consumer matching CVEs against http@1.2.0 is checking a version nobody installs. There is no pubspec.lock in the repo, so the resolved version is genuinely unknown at generation time — the honest options are to record the range, or resolve it.

5. kotlin-stdlib is absent from every SBOM

Both published Kotlin artifacts declare org.jetbrains.kotlin:kotlin-stdlib at scope=compile. It is added by the Kotlin Gradle plugin rather than a dependencies block, so a manifest-regex reader cannot see it by construction.

6. The "fails rather than emitting a shorter list" guarantee is false

Only one path escalates — parseMavenCoordinate's unresolved-$ branch. Every other failure is a silent return/continue, including:

  • coordinates with a classifier (group:artifact:version:sources → null, dropped)
  • extractGradleCatalog finding no matching alias
  • a version catalog entry with neither version.ref nor a literal
  • the whole literal-coordinate class in item 1 above

Direction

Items 1–3 and 6 are the same root cause: regex over build manifests cannot model conditional or plugin-injected dependency graphs. security/SBOM.md already documents the intended replacement — take the dependency graph from each ecosystem's own resolver:

Ecosystem Replace parser with
Gradle cyclonedx-gradle-plugin, or the published POM
NuGet dotnet list package --include-transitive --format json, or the published nuspec
pub flutter pub deps --json

Reading the published POM/nuspec is worth considering on its own: it is what consumers actually resolve, it is flavor-correct by construction, and it needs no local toolchain.

Until that lands, every silent-drop path should escalate the way the docs already claim it does.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛 bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions