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.
Summary
The manifest readers in
scripts/sbom-dependencies.mjsproduce 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)
kmpis declared askind: "gradle-catalog", andextractGradleCataloghas exactly one matcher —libs.<alias>accessors. It never reads literal string coordinates, so the entire top-leveldependencies { }block is invisible.The published POM (
kmp-iap-android-play-3.3.0.pom) declaresio.github.hyochan.openiap:openiap-google:3.3.0atscope=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.mdpromises cannot happen: "a coordinate they cannot resolve raises an error instead of being dropped."2. Flavored Gradle configurations are over-reported
isRuntimeGradleConfigurationaccepts any*Api/*Implementation, sohorizonApiandamazonApiare treated as shipping in the default artifact. Butpackages/google/openiap/build.gradle.ktspublishes one flavor per coordinate —openiap-google←playRelease,openiap-google-horizon←horizonRelease,openiap-google-amazon←amazonRelease.Result: the
openiap-googleSBOM lists 6 dependencies that are not in the published artifact (published POM has 10 deps; the SBOM has 15).3. NuGet
ItemGroup Conditionis ignoredextractNugetmatches every<PackageReference>with no awareness of the enclosing<ItemGroup Condition=...>.OpenIap.Maui.csproj:112opens a Horizon-only group; the default flavor isplay.4. Version ranges are asserted as exact versions
extractPubandextractNpmstrip the range operator and treat the remainder as the version.A consumer matching CVEs against
http@1.2.0is checking a version nobody installs. There is nopubspec.lockin 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-stdlibis absent from every SBOMBoth published Kotlin artifacts declare
org.jetbrains.kotlin:kotlin-stdlibatscope=compile. It is added by the Kotlin Gradle plugin rather than adependenciesblock, 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 silentreturn/continue, including:group:artifact:version:sources→null, dropped)extractGradleCatalogfinding no matching aliasversion.refnor a literalDirection
Items 1–3 and 6 are the same root cause: regex over build manifests cannot model conditional or plugin-injected dependency graphs.
security/SBOM.mdalready documents the intended replacement — take the dependency graph from each ecosystem's own resolver:cyclonedx-gradle-plugin, or the published POMdotnet list package --include-transitive --format json, or the published nuspecflutter pub deps --jsonReading 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.