Skip to content

SBOM system: automation never fires, extraction is inaccurate, audit tooling is broken #323

Description

@hyochan

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.

  • Make the trigger work. The repo already works around this rule: the Dispatch npm publish on tag ref steps use secrets.DEPENDENCY_UPDATE_PAT, because workflow_dispatch is an explicit exception. Either give the release-creating steps a PAT/App token, or have each release workflow end with gh workflow run sbom.yml -f tag=<tag>.
  • Backfill the 7 releases currently missing an SBOM.
  • Consider a scheduled backfill as a safety net.
  • Correct "automatically / every published release" in SECURITY.md and security/SBOM.md to 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

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.

  • Fix or replace the Gradle catalog reader so literal coordinates cannot be dropped
  • Make flavored/conditional configurations flavor-aware
  • Honour ItemGroup Condition in the NuGet reader
  • Decide range handling: record the range, or resolve it
  • Cover plugin-injected dependencies such as kotlin-stdlib
  • Make every silent-drop path escalate, as the docs already claim

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
  • Fix the three broken /audit-security steps
  • Add a regression check so a command that silently matches nothing fails loudly
  • Correct the documentation claims; prefer a described property or a live command over a count that drifts

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.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions