Skip to content

feat(mobile): publish per-artifact CycloneDX SBOMs on release - #6665

Merged
iscekic merged 11 commits into
mainfrom
kwf/mobile-per-artifact-sbom-067e
Sep 26, 2026
Merged

iscekic merged 11 commits into
mainfrom
kwf/mobile-per-artifact-sbom-067e

Conversation

@iscekic

@iscekic iscekic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Changelog for users

  • Every production mobile build now publishes one CycloneDX JSON SBOM per shipped platform.
  • SBOMs are named kilo-app-<platform>-<version>-build<buildNumber>.cyclonedx.json.
  • Each SBOM is attached to the GitHub Release for that build's kilo-app-release/<date>-<sha> tag.
  • Retrieve one with gh release download kilo-app-release/<date>-<sha> -p '*.cyclonedx.json'.
  • Each document records its platform, app version, build number, EAS build ID, and the submitted artifact's SHA-256.

Changelog for maintainers

  • Release trigger: .github/workflows/kilo-app-release.yml, job build-and-submit, on push to main filtered to apps/mobile/** and its workspace inputs, or workflow_dispatch, gated on should_build == true && github.ref == 'refs/heads/main'.
  • SBOM locations: release assets on the kilo-app-release/<date>-<sha> release plus the retained workflow artifact mobile-sbom-<sha> (90 days); .gitignore ignores /apps/mobile/artifacts/.
  • Coverage limits: an early docs/sbom.md "Coverage limits" section states each gap; nothing claims full coverage, and there is no CVE threshold or --fail-on.
  • Generation runs after artifact inspection and before the first store submission, as a plain step with no if: and no continue-on-error, so an undocumented build fails the job before it is submitted.
  • npm: the production dependency closure of apps/mobile from pnpm-lock.yaml (dependencies plus optionalDependencies, link:/file: workspace recursion, npm aliases resolved to the real package); a package Metro bundles but the graph does not declare is not listed.
  • iOS: the IPA's Mach-O LC_LOAD_DYLIB/weak/reexport/upward load commands plus Payload/*.app/Frameworks/; --podfile-lock prints declared, visible, and missing pods. Statically linked pods are invisible, so the artifact list is a lower bound.
  • Android: the AAB's own BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb through a small direct protobuf reader (no protobufjs import), plus base/lib/**/*.so.
  • Review hints: both documents are assembled in memory before any file is written, so a missing artifact, an incomplete EAS record, or corrupt metadata fails with no output; scripts/mobile-sbom-workflow.test.mjs pins the step wiring and the ignore rules, and the repo-wide sbom.yml family is untouched.

E2E proof

No production build was triggered from this work, and no EAS-downloaded IPA or AAB was available, so the generator was exercised against synthesized fixtures through its integration tests rather than a real shipped artifact. That leaves the run inside the release job and every real-artifact measurement unproven here.

Affected test suites, from this section's check log, all pass:

CHECK PASS (0s): node --test scripts/mobile-sbom-cyclonedx.test.mjs
CHECK PASS (0s): node --test scripts/mobile-sbom-pnpm.test.mjs
CHECK PASS (0s): node --test scripts/mobile-sbom-ipa.test.mjs
CHECK PASS (0s): node --test scripts/mobile-sbom-aab.test.mjs
CHECK PASS (1s): node --test scripts/mobile-sbom.test.mjs
CHECK PASS (0s): node --test scripts/mobile-sbom-workflow.test.mjs
CHECK PASS (2s): pnpm run test:mobile-sbom

The generator's integration test drives the real CLI end to end on a synthesized IPA, AAB, and build record, and asserts one CycloneDX document per platform, the four linkage fields, the artifact SHA-256 against the fixture bytes, and that corrupt dependency metadata fails with no document written.

Retrieve an SBOM for a build:

gh release download kilo-app-release/<date>-<sha> -p '*.cyclonedx.json'

Measured per-ecosystem counts and the iOS Podfile.lock gap are not reported here because no real artifact was processed. Each production build writes them into its release notes, and docs/sbom.md gives the commands to reproduce them.

Owner request

Surface: mobile-app

Add automatic CycloneDX JSON SBOM generation for every production Kilo mobile build: one SBOM per
shipped IPA and AAB, linked to its platform, version, EAS build ID and artifact SHA-256, published
where a user can retrieve it with the matching build.

Report three things in the PR body, in this order: the release trigger, the SBOM locations, and the
coverage limits.
The third is the one that matters most; see "Do not claim full coverage".

What exists today, confirmed at 2f0d76d5d8f0

The repo-wide SBOM, which stays separate. .github/workflows/sbom.yml runs syft over
pnpm-lock.yaml on a push to main, a weekly cron, and manual dispatch. It uploads one retained
workflow artifact named cloud-sbom-<sha> and commits nothing. docs/sbom.md documents it. That
SBOM describes the pnpm dependency tree of the whole monorepo, not any app that ships, so leave it
alone
— requirement 2. This item adds a second, per-artifact family beside it and says so in the
doc.

The production mobile build. .github/workflows/kilo-app-release.yml, job build-and-submit,
gated on should_build == true && github.ref == 'refs/heads/main', triggered by a push to main
touching apps/mobile/** and its workspace inputs, or by workflow_dispatch. Its steps, in order:

  1. pnpx eas-cli@21.8.0 build --profile production --platform all --non-interactive --json --wait > build.json
    — an EAS cloud build of both platforms. build.json holds the build records, including
    build.id (the EAS build ID), build.platform, build.status, and
    build.artifacts.applicationArchiveUrl.
  2. node ../../scripts/inspect-mobile-artifacts.mjs --select build.json > urls.txt — validates the
    records and prints the two archive URLs, iOS then Android.
  3. Downloads them to apps/mobile/artifacts/app.ipa and apps/mobile/artifacts/app.aab. These bytes
    are what gets submitted to the stores, so they are the only honest SBOM subject.
  4. node ../../scripts/inspect-mobile-artifacts.mjs artifacts/app.ipa artifacts/app.aab build.json
    — the inspector.
  5. Submits iOS, then Android, then tags kilo-app-release/<date>-<sha> and pushes the tag.

The inspector is the natural seam. scripts/inspect-mobile-artifacts.mjs is 480 lines and already
unzips the IPA, parses its Info.plist, dumps the AAB manifest with bundletool (it downloads
bundletool-all-1.18.3.jar), and reads AAB BUNDLE-METADATA/ entries. It computes no hash today, so
the SHA-256 is new work.

The constraint that shapes everything: the native projects are not in git. git ls-files apps/mobile/ios and git ls-files apps/mobile/android both return 0 files. They are generated by
Expo (CNG), so in CI there is no apps/mobile/ios/Podfile.lock and no
apps/mobile/android/app/build.gradle. A local prebuild in this checkout produced a
Podfile.lock with 431 pod entries, but CI never sees it. Any requirement that reads a lockfile
from the native directory must say where that file comes from in CI, or use the artifact instead.

Version and build number. apps/mobile/eas.json sets appVersionSource: "remote", so the build
number comes from EAS. The marketing version is apps/mobile/app.config.ts (version: '1.0.12'
today). The EAS build record carries both as appVersion and appBuildVersion; prefer the record,
because it describes the artifact that was built.

Tests. Root scripts are scripts/*.test.mjs run by node --test, and pnpm test already calls
test:mobile-artifacts, which runs scripts/inspect-mobile-artifacts.test.mjs. Follow that shape.

No workflow triggers on release:, so creating a GitHub Release has no side effects on the other
pipelines.

Requirement 3 — one SBOM per shipped artifact

Generate a CycloneDX JSON SBOM for the IPA and another for the AAB, from the downloaded bytes, in the
release job. Not one combined file, and not a copy of the repo-wide SBOM.

Each file name must carry the platform and the version, so a user who has a build can find its SBOM
without opening anything.

Requirement 4 — resolved dependencies for the three ecosystems, per platform

For each platform, include what that platform actually ships. Name the source of each in the SBOM and
in the doc.

JavaScript, both platforms. The repo-wide SBOM is the whole pnpm tree; this one must be the
production dependency closure of apps/mobile, not the monorepo. Derive it from
pnpm-lock.yaml scoped to the app's production dependencies, and say how you scoped it. A minified
shipped JS bundle carries no package metadata, so the lockfile closure is the honest source; say that
too.

iOS: CocoaPods. The authoritative resolved graph is apps/mobile/ios/Podfile.lock — 431 pods in
this checkout — and it does not exist in CI. So use what the artifact carries: scan the unpacked IPA,
and read the app binary's Mach-O load commands (LC_LOAD_DYLIB) plus Payload/*.app/Frameworks/ for
the dynamic pods and frameworks. Then measure the gap against a real Podfile.lock and report it:
how many pods the lockfile declares, how many appear in the IPA, and the names of the ones that do
not. Statically linked pods are compiled into the binary and are invisible to any file scan; that is
a limit to document, not to hide. If a prebuild step in CI can produce a trustworthy Podfile.lock
cheaply, that is a better source — but only if it is deterministic and does not slow the release
path. State which source you chose and why.

Android: Gradle/Maven. The AAB ships its own resolved dependency metadata at
BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb, written by the Android Gradle
Plugin. It carries the group, module, version, and SHA-256 of every resolved Maven artifact, which is
the best source available and it describes the shipped bundle. Parse it. protobufjs is already in
the lockfile, but it is not a declared dependency of the root package or of apps/mobile, so do not
import it as a transitive; either declare it deliberately with a reason or write a small reader for
the few fields you need. Also include the native .so libraries the AAB carries under
base/lib/.

Requirement 5 — link every SBOM to its build

Each generated CycloneDX document must carry, in the document itself, all four of:

  • the platform (ios or android),
  • the app version and the build number,
  • the EAS build ID, from the matching record in build.json,
  • the SHA-256 of the artifact file the SBOM describes.

Use CycloneDX metadata.properties for the link fields and metadata.component for the app and its
version, so the values survive a copy of the file and do not live only in a log. Compute the SHA-256
from the same downloaded file that is submitted to the store, and say in the doc that the hash is of
the submitted artifact.

Requirement 6 — do not claim full coverage

An SBOM that overstates itself is worse than none. Required:

  • Measure what the tools actually find for each platform, on a real artifact, and report the
    numbers: component count per ecosystem, per platform.
  • State the gaps explicitly. At minimum: statically linked iOS pods, any Gradle dependency the
    AAB metadata omits, JS packages that are bundled but unreachable from the app's declared closure,
    and anything syft cannot see inside a signed binary. Do not write "complete" or "full coverage"
    anywhere unless a measurement supports it.
  • Write the limits into docs/sbom.md, in a section a reader finds without digging.

Requirement 7 — store each SBOM where a user can retrieve it with its build

The job already creates and pushes the tag kilo-app-release/<date>-<sha>. Attach the SBOMs to a
GitHub Release for that tag, so the retrieval path is: find the release for your build, download the
SBOM for your platform. Name the release so the app version and the EAS build IDs are visible, and
include the two artifact SHA-256 values in the release notes, so a user can confirm the SBOM matches
the binary they hold. Keep the retained workflow artifact as a second copy, as sbom.yml does. Never
commit a generated SBOM.

Generation must run on every production build and must not be silently skippable. Decide where in
the step order it belongs, state the choice, and make a failure fail the job loudly rather than leave
a shipped build undocumented.

Requirement 8 — the hard constraints

  • Do not commit a generated SBOM. The .gitignore must make that impossible, not merely discouraged.
  • Do not expose a secret. EXPO_TOKEN is in the environment of these steps; keep it out of the SBOM,
    out of the release notes, and out of any log line. No artifact URL that carries a token either.
  • Do not change app runtime behavior. Nothing in apps/mobile/src changes.
  • Do not add a vulnerability gate. No CVE threshold, no --fail-on, no scanner that fails the build
    on a finding. This item inventories; it does not judge.

Requirement 9 — tests

Add scripts/*.test.mjs coverage, run by node --test, wired into the root package.json the way
test:mobile-artifacts is. Cover:

  • Both platform outputs: a valid CycloneDX JSON document per platform, with the expected
    ecosystems present for that platform and absent for the other.
  • The linkage: the four fields are present, and the SHA-256 equals the hash of the fixture bytes
    the test feeds in. A wrong or missing hash must fail.
  • The parsers: the AAB dependency-metadata reader and the IPA component reader against fixtures,
    including a malformed input, which must fail with a clear message rather than emit a partial SBOM.

Fixtures must be small, checked-in, and must not be real signed binaries. Do not commit a real IPA or
AAB.

Files

  • .github/workflows/kilo-app-release.yml — the SBOM steps and the release.
  • scripts/ — the generator, its readers, and its tests (new files here).
  • scripts/inspect-mobile-artifacts.mjs — only if the SHA-256 or the unpacking belongs here.
  • package.json — the test wiring.
  • docs/sbom.md — the generation and retrieval steps, and the coverage limits.
  • .gitignore — so a generated SBOM cannot be committed.

Do not edit a file outside this list. In particular, do not touch .github/workflows/sbom.yml or the
repo-wide SBOM, and do not touch apps/mobile/src.

Proof

A log excerpt is required. This change has no visible surface, so the run is the proof.

  • The release trigger: name it exactly — the workflow, the job, the event and path filter that
    starts a production build — and show the SBOM step running inside that job on a real build. If you
    cannot trigger a production build from this work, say so plainly and show the step's own run
    against a real artifact downloaded from a recent EAS build instead, and say what that leaves
    unproven.
  • The SBOM locations: the exact artifact name and the exact release asset names, plus one
    gh release view or download command a user can paste.
  • The linkage: the four fields from a real generated SBOM, with the SHA-256 checked against the
    artifact by a second command, so the reader can see they agree.
  • The coverage limits: the measured component counts per platform and ecosystem, and the gap
    numbers. For iOS, the pod count in a real Podfile.lock against the count visible in the IPA, and
    the names of the missing ones. For Android, the Maven artifact count from the AAB metadata against
    what syft reports on its own. These numbers are the point of the item; a claim without a number is
    not acceptable here.
  • Keep pnpm test, or at least the affected parts of it, green. Say which parts you ran and which
    you did not.

Comment thread scripts/mobile-sbom-aab.mjs Outdated
Comment thread scripts/mobile-sbom-ipa.mjs Outdated
Comment thread scripts/mobile-sbom-ipa.mjs
Comment thread docs/sbom.md Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The commits since the prior review are the merge of main (the changelog / App Store upload-cap release feature) plus a fix that makes the earlybird trial test end relative to now; the per-artifact SBOM feature files are unchanged and no new defect was found on the changed lines.

Files Reviewed (2 files)
  • .github/workflows/kilo-app-release.yml
  • apps/web/src/routers/admin-kiloclaw-user-router.test.ts
Previous Review Summaries (4 snapshots, latest commit 3d995ac)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 3d995ac)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commits add iOS embedded-app-extension dependency walking, a plist-fallback executable resolver, idempotent tag/release publication with --clobber, and covering tests; no new defects were found on the changed lines.

Files Reviewed (4 files)
  • .github/workflows/kilo-app-release.yml
  • scripts/mobile-sbom-ipa.mjs
  • scripts/mobile-sbom-ipa.test.mjs
  • scripts/mobile-sbom-workflow.test.mjs

Previous review (commit 1640335)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental changes add an OS-provided-library filter to the iOS IPA reader so /usr/lib/ and /System/Library/ load commands no longer become CocoaPods components, update the docs/sbom.md reproduction paths to be repo-root-relative, and add a covering unit test; the filter is correctly scoped and no new defects were found on the changed lines.

Files Reviewed (4 files)
  • docs/sbom.md
  • scripts/mobile-sbom-ipa.mjs
  • scripts/mobile-sbom-ipa.test.mjs
  • scripts/mobile-sbom.mjs

Previous review (commit e343993)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental changes resolve all prior findings in scripts/mobile-sbom-aab.mjs (raised execFileSync buffer for unzip -Z1) and scripts/mobile-sbom-ipa.mjs (bounded fat-slice recursion, short dylib-command bounds check), each with a regression test; no new issues were found on the changed lines.

Files Reviewed (4 files)
  • scripts/mobile-sbom-aab.mjs
  • scripts/mobile-sbom-aab.test.mjs
  • scripts/mobile-sbom-ipa.mjs
  • scripts/mobile-sbom-ipa.test.mjs

Previous review (commit 11faaf3)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
scripts/mobile-sbom-aab.mjs 320 listAabEntries omits maxBuffer, so a large AAB entry listing can fail the release job

SUGGESTION

File Line Issue
scripts/mobile-sbom-ipa.mjs 116 Unbounded recursion in walkFat on a self-referential fat image
scripts/mobile-sbom-ipa.mjs 147 readU32 can read past the buffer on a short dylib load command
docs/sbom.md 91 Reproduction command mixes repo-root and apps/mobile paths
Files Reviewed (15 files)
  • .github/workflows/kilo-app-release.yml
  • .gitignore
  • docs/sbom.md - 1 issue
  • package.json
  • scripts/mobile-sbom-aab.mjs - 1 issue
  • scripts/mobile-sbom-aab.test.mjs
  • scripts/mobile-sbom-cyclonedx.mjs
  • scripts/mobile-sbom-cyclonedx.test.mjs
  • scripts/mobile-sbom-ipa.mjs - 2 issues
  • scripts/mobile-sbom-ipa.test.mjs
  • scripts/mobile-sbom-pnpm.mjs
  • scripts/mobile-sbom-pnpm.test.mjs
  • scripts/mobile-sbom-workflow.test.mjs
  • scripts/mobile-sbom.mjs
  • scripts/mobile-sbom.test.mjs

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic marked this pull request as draft September 23, 2026 22:02
@iscekic
iscekic force-pushed the kwf/mobile-per-artifact-sbom-067e branch from 6fbc2fc to 1987fc3 Compare September 23, 2026 22:48
@iscekic
iscekic marked this pull request as ready for review September 23, 2026 23:40
@iscekic

iscekic commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

kilo-review — independent audit of the published diff.

Status: 1 Issues

File Line Issue
scripts/mobile-sbom-ipa.mjs 294 The iOS reader turns every Mach-O LC_LOAD_DYLIB install name into a component labelled ecosystem 'cocoapods' (line 231), so OS-provided libraries such as /usr/lib/libSystem.B.dylib and /System/Library/Frameworks/UIKit.framework/UIKit are published as CocoaPods dependencies and inflate the cocoapods count in the release notes.

@iscekic
iscekic marked this pull request as draft September 24, 2026 00:17
The evidence-reproduction block mixed roots: `node scripts/mobile-sbom.mjs`
resolves only from the repository root, while `artifacts/app.ipa`,
`build.json` and `--out-dir artifacts` are the apps/mobile-relative paths the
release workflow uses. Rewrite both commands root-relative so each one can be
pasted as printed, and name the Podfile.lock a local prebuild generates.
@iscekic

iscekic commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Audit finding validated and already fixed in ef8d92da12.

scripts/mobile-sbom-ipa.mjs:294 — the OS-provided load commands named in the finding no longer become CocoaPods components:

  • scripts/mobile-sbom-ipa.mjs:56-66 — OS_INSTALL_NAME_PREFIXES = ["/usr/lib/", "/System/Library/"] and isOsProvidedDylib().
  • scripts/mobile-sbom-ipa.mjs:307-312 — readIpaComponents skips those install names before add(...), so the cocoapods count no longer includes /usr/lib/libSystem.B.dylib or /System/Library/Frameworks/UIKit.framework/UIKit.
  • scripts/mobile-sbom-ipa.test.mjs:287-311 — new case feeds exactly those two install names plus @rpath/React.framework/React and asserts ["React.framework"] with counts = { dylibs: 0, frameworks: 1 }.
  • docs/sbom.md:81-84 and the cocoapods source string in scripts/mobile-sbom.mjs:45-46 now state the exclusion, so the release-notes count matches the reader.

A follow-up docs fix is in 1640335493 (thread on docs/sbom.md:93).

@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 25, 2026
@iscekic
iscekic marked this pull request as ready for review September 25, 2026 13:49
@iscekic iscekic added the merge-by-human the merge bot routed this PR to a human label Sep 25, 2026
Comment thread scripts/mobile-sbom-ipa.mjs Outdated
Comment thread .github/workflows/kilo-app-release.yml Outdated
The IPA scan read only the main app executable and the app's top-level
Frameworks/ directory, so a framework or dylib that only an embedded
PlugIns/*.appex extension uses never reached the iOS SBOM. Each extension
bundle is now walked the same way: its executable's load commands, then
its own Frameworks/ directory. An extension whose plist cannot be read
falls back to the bundle name, and the result is checked against the
file system, so a bundle with no dependencies cannot be reported
silently.
The release tag is deterministic, so a rerun of the same commit
recomputed the tag the failed attempt had pushed and died at the tag
step. The tag step now reuses a tag that already exists on origin, and
the publication step updates the release and re-uploads the CycloneDX
assets with --clobber when the release already exists, so a failed
upload can be retried.
The added fixture code was not oxfmt-shaped, which failed format-check.
@iscekic
iscekic marked this pull request as draft September 25, 2026 19:12
@iscekic
iscekic marked this pull request as ready for review September 25, 2026 19:12
@iscekic

iscekic commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Audit finding (scripts/mobile-sbom-ipa.mjs, OS dylibs reported as CocoaPods): already fixed in ef8d92d. addBundleComponents skips every install name that isOsProvidedDylib matches (/usr/lib/, /System/Library/), so libSystem and system frameworks are not reported as cocoapods components and do not count in the release notes.

@iscekic
iscekic enabled auto-merge (squash) September 26, 2026 01:16
@iscekic
iscekic merged commit ec82391 into main Sep 26, 2026
27 checks passed
@iscekic
iscekic deleted the kwf/mobile-per-artifact-sbom-067e branch September 26, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review. merge-by-human the merge bot routed this PR to a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants