Post-merge verification of #318 / #319 found 27 confirmed defects (4 critical, 16 major, 6 minor, 1 observation). Each survived adversarial re-verification by at least 2 of 3 independent checks; 18 further candidates were raised and refuted.
The structure is sound — schema validity, provenance attestation, tag binding, determinism, and absence of secrets were all verified working. What is broken is automation wiring, data accuracy, and the audit tooling meant to catch both.
Suggested order: §1 first — without it, fixes in §2 and §3 never reach a release.
1. Automation never triggers (critical)
sbom.yml declares on: release: published, but that trigger can never fire here. GitHub's rule:
Events triggered by the GITHUB_TOKEN will not create a new workflow run, with the following exceptions: workflow_dispatch and repository_dispatch.
Every release in this repo is created by GITHUB_TOKEN — 8 workflows call softprops/action-gh-release@v3 with no token: input (defaults to ${{ github.token }}), and release-apple.yml:448 / release-google.yml:572 set GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} explicitly.
$ gh run list --limit 200 --json event -q '[.[].event] | group_by(.) | map({event: .[0], n: length})'
[{"event":"dynamic","n":2},{"event":"pull_request","n":153},{"event":"push","n":30},{"event":"workflow_dispatch","n":15}]
No release-event run has ever occurred. 7 of the 9 latest component releases have no .cdx.json asset — the only two that do came from manual dispatch during verification.
2. Dependency extraction is inaccurate (critical / major)
Generated inventories disagree with the artifacts actually published, verified by diffing each SBOM against the real POM / nuspec.
kmp-iap silently omits its primary runtime dependency
kmp uses kind: "gradle-catalog", and extractGradleCatalog matches only libs.<alias> accessors — literal string coordinates are invisible, so the whole top-level dependencies { } block is skipped.
$ grep -n 'add("' libraries/kmp-iap/library/build.gradle.kts
332: add("playImplementation", "io.github.hyochan.openiap:openiap-google:$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 declares io.github.hyochan.openiap:openiap-google:3.3.0 at scope=runtime — the dependency that transitively carries Play Billing, Gson, AndroidX and Compose. No error is raised, which is exactly what security/SBOM.md promises cannot happen.
Flavored Gradle configurations are over-reported
isRuntimeGradleConfiguration accepts any *Api/*Implementation, so horizonApi and amazonApi count toward the default artifact — but each flavor publishes its own coordinate. The openiap-google SBOM lists 6 dependencies absent from the published artifact (POM has 10, SBOM has 15).
NuGet ItemGroup Condition is ignored
extractNuget matches every <PackageReference> regardless of the enclosing conditional group, so two Horizon-only packages appear in the default (play) inventory. Published nuspec: 22 deps; SBOM: 24.
Version ranges are asserted as exact versions
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.
kotlin-stdlib is absent from every SBOM
Declared at scope=compile in both published Kotlin artifacts. The Kotlin Gradle plugin injects it, so a manifest-regex reader cannot see it by construction.
The "fails rather than emitting a shorter list" guarantee is false
Only parseMavenCoordinate's unresolved-$ branch escalates. Every other failure silently returns — classifier coordinates (group:artifact:version:sources), unmatched catalog aliases, catalog entries with no version, and the literal-coordinate class above.
Direction: these share one root cause — regex over build manifests cannot model conditional or plugin-injected graphs. security/SBOM.md already documents the intended replacement: take the graph from each ecosystem's resolver (cyclonedx-gradle-plugin, dotnet list package --include-transitive, flutter pub deps --json). Reading the published POM/nuspec deserves consideration too — it is what consumers resolve, flavor-correct by construction, and needs no local toolchain.
3. Audit tooling is broken, docs drifted (major / minor)
/audit-security passes checks it never runs. This is the most damaging item after §1, because it is what should have caught the rest.
- Step 6's injection scan uses
\s inside awk, unsupported by BSD/BWK awk. On macOS it matches nothing and reports clean regardless of input.
- Step 8's URL check misses a trailing single quote in its
sed cleanup, so 10 of 12 URLs report a bogus 404; real breakage would be invisible.
- Step 3 regenerates into the same directory step 5 diffs against, so the determinism check can pass trivially.
Documentation drift
| Claim |
Reality |
SECURITY.md: every supported release carries an SBOM |
7 of 9 have none (§1) |
security/README.md: dependency-graph command returns "0 packages" |
Returns HTTP 404; the conclusion is right, the command is wrong |
security/README.md: "nothing else needs to change" for a new component |
TAG_PREFIXES is a second hardcoded list |
security/README.md: /audit-security re-checks all three known gaps |
It checks one |
security/openchain.md: MAINTAINERS.md missing |
It exists and predates the document |
security/openchain.md: per-package LICENSE files a known gap |
12 of 13 have one |
security/SBOM.md: name always equals the published package name |
False for apple |
security/SBOM.md: consumer verification snippet |
Omits --repo, fails outside a clone |
security/SBOM.md: worked reproduction recipe |
Fails on the only release it names |
compliance.tsx: CRA deadline row |
Contradicts Article 14 and the repo's own CRA.md |
overview.tsx: CRA deadlines |
Presents OpenIAP's internal SLA as statutory, contradicting SECURITY.md |
overview.tsx: release-state audits on "merge to main" |
They run on every pull request |
Verified correct — no action
Adversarial verification confirmed these work: CycloneDX 1.6 schema validity (10/10), provenance attestation and tag binding (gh attestation verify exit 0, commit matches the tag), byte-identical regeneration, absence of secrets and local paths, the component/tag table, licence exception handling, the CRA legal claims in CRA.md, internal links, docs routes, and test-coverage claims.
Post-merge verification of #318 / #319 found 27 confirmed defects (4 critical, 16 major, 6 minor, 1 observation). Each survived adversarial re-verification by at least 2 of 3 independent checks; 18 further candidates were raised and refuted.
The structure is sound — schema validity, provenance attestation, tag binding, determinism, and absence of secrets were all verified working. What is broken is automation wiring, data accuracy, and the audit tooling meant to catch both.
Suggested order: §1 first — without it, fixes in §2 and §3 never reach a release.
1. Automation never triggers (critical)
sbom.ymldeclareson: release: published, but that trigger can never fire here. GitHub's rule:Every release in this repo is created by
GITHUB_TOKEN— 8 workflows callsoftprops/action-gh-release@v3with notoken:input (defaults to${{ github.token }}), andrelease-apple.yml:448/release-google.yml:572setGH_TOKEN: ${{ secrets.GITHUB_TOKEN }}explicitly.No release-event run has ever occurred. 7 of the 9 latest component releases have no
.cdx.jsonasset — the only two that do came from manual dispatch during verification.Dispatch npm publish on tag refsteps usesecrets.DEPENDENCY_UPDATE_PAT, becauseworkflow_dispatchis an explicit exception. Either give the release-creating steps a PAT/App token, or have each release workflow end withgh workflow run sbom.yml -f tag=<tag>.SECURITY.mdandsecurity/SBOM.mdto match the chosen mechanism.2. Dependency extraction is inaccurate (critical / major)
Generated inventories disagree with the artifacts actually published, verified by diffing each SBOM against the real POM / nuspec.
kmp-iap silently omits its primary runtime dependency
kmpuseskind: "gradle-catalog", andextractGradleCatalogmatches onlylibs.<alias>accessors — literal string coordinates are invisible, so the whole top-leveldependencies { }block is skipped.The published POM declares
io.github.hyochan.openiap:openiap-google:3.3.0atscope=runtime— the dependency that transitively carries Play Billing, Gson, AndroidX and Compose. No error is raised, which is exactly whatsecurity/SBOM.mdpromises cannot happen.Flavored Gradle configurations are over-reported
isRuntimeGradleConfigurationaccepts any*Api/*Implementation, sohorizonApiandamazonApicount toward the default artifact — but each flavor publishes its own coordinate. Theopeniap-googleSBOM lists 6 dependencies absent from the published artifact (POM has 10, SBOM has 15).NuGet
ItemGroup Conditionis ignoredextractNugetmatches every<PackageReference>regardless of the enclosing conditional group, so two Horizon-only packages appear in the default (play) inventory. Published nuspec: 22 deps; SBOM: 24.Version ranges are asserted as exact versions
A consumer matching CVEs against
http@1.2.0is checking a version nobody installs.kotlin-stdlibis absent from every SBOMDeclared at
scope=compilein both published Kotlin artifacts. The Kotlin Gradle plugin injects it, so a manifest-regex reader cannot see it by construction.The "fails rather than emitting a shorter list" guarantee is false
Only
parseMavenCoordinate's unresolved-$branch escalates. Every other failure silently returns — classifier coordinates (group:artifact:version:sources), unmatched catalog aliases, catalog entries with no version, and the literal-coordinate class above.Direction: these share one root cause — regex over build manifests cannot model conditional or plugin-injected graphs.
security/SBOM.mdalready documents the intended replacement: take the graph from each ecosystem's resolver (cyclonedx-gradle-plugin,dotnet list package --include-transitive,flutter pub deps --json). Reading the published POM/nuspec deserves consideration too — it is what consumers resolve, flavor-correct by construction, and needs no local toolchain.ItemGroup Conditionin the NuGet readerkotlin-stdlib3. Audit tooling is broken, docs drifted (major / minor)
/audit-securitypasses checks it never runs. This is the most damaging item after §1, because it is what should have caught the rest.\sinsideawk, unsupported by BSD/BWK awk. On macOS it matches nothing and reports clean regardless of input.sedcleanup, so 10 of 12 URLs report a bogus 404; real breakage would be invisible.Documentation drift
SECURITY.md: every supported release carries an SBOMsecurity/README.md: dependency-graph command returns "0 packages"security/README.md: "nothing else needs to change" for a new componentTAG_PREFIXESis a second hardcoded listsecurity/README.md:/audit-securityre-checks all three known gapssecurity/openchain.md:MAINTAINERS.mdmissingsecurity/openchain.md: per-package LICENSE files a known gapsecurity/SBOM.md: name always equals the published package nameapplesecurity/SBOM.md: consumer verification snippet--repo, fails outside a clonesecurity/SBOM.md: worked reproduction recipecompliance.tsx: CRA deadline rowCRA.mdoverview.tsx: CRA deadlinesSECURITY.mdoverview.tsx: release-state audits on "merge to main"/audit-securitystepsVerified correct — no action
Adversarial verification confirmed these work: CycloneDX 1.6 schema validity (10/10), provenance attestation and tag binding (
gh attestation verifyexit 0, commit matches the tag), byte-identical regeneration, absence of secrets and local paths, the component/tag table, licence exception handling, the CRA legal claims inCRA.md, internal links, docs routes, and test-coverage claims.