Skip to content

feat(security): per-release SBOM, CRA reporting path, and supply-chain docs - #318

Merged
hyochan merged 10 commits into
mainfrom
feat/sbom-supply-chain
Aug 12, 2026
Merged

hyochan merged 10 commits into
mainfrom
feat/sbom-supply-chain

Conversation

@hyochan

@hyochan hyochan commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Every published release now gets a CycloneDX 1.6 SBOM as a release asset, with a provenance attestation, generated automatically from the manifests the build already reads.
  • Adds the CRA Article 14 reporting path ahead of its 11 September 2026 start date, plus a bug bar and support-lifecycle statement.
  • Adds a Security docs section covering what consumers get, how to verify it, and what OpenIAP does and does not claim.

Why the SBOM is not redundant here

GitHub's dependency graph reports zero packages for this repository — Bun lockfiles are not a supported format and Gradle is not resolved from source:

gh api repos/hyodotdev/openiap/dependency-graph/sbom  →  0 packages

Dependabot's version updates still work (they read manifests directly), but the platform cannot derive a component inventory on its own. These SBOMs are that inventory.

Changes

SBOM generation (scripts/, .github/workflows/sbom.yml)

  • One SBOM per releasable component, not one per repository. The component list, version, and release tag are read from the existing release SSOT (release-branch-policy.mjs, assert-release-tag.mjs), so a component cannot be released without also being describable. A test asserts the two lists stay equal.
  • sbom.yml reacts to release: published instead of editing the existing release workflows, so components added later are covered by the same path.
  • Generation is deterministic: the document timestamp is the commit timestamp and the serial number is derived from the release identity, so regenerating at a released commit reproduces the file byte for byte.
  • Licenses resolve from each dependency's own registry, including Google's Maven repository — Maven Central does not mirror it, and every androidx coordinate needs it. Licenses are never guessed: a registry value becomes an SPDX id only when it is one.
  • Test-only and build-only dependencies are excluded. An unresolvable Gradle coordinate fails generation rather than silently shortening the inventory.

Vulnerability analysis (security/vex/)

Whether a CVE is reachable is a human judgement, so this is the one input that cannot be generated. A per-component file is merged into the SBOM when present; a not_affected claim without a justification is rejected. Absent by default — an empty vulnerabilities array would read as "checked, none found".

Repository posture (.github/workflows/scorecard.yml)

OpenSSF Scorecard checks what neither Dependabot nor the SBOM covers: branch protection, workflow token permissions, action pinning, dangerous workflow patterns.

Docs (packages/docs, security/, SECURITY.md)

New Security section — Overview (including a when-each-thing-runs table), SBOM (download / verify / reproduce), and Compliance (CRA timeline, the conformance suite as behavioral evidence, OpenChain gap assessment). security/openchain.md self-assesses against ISO/IEC 18974 and 5230 as a gap list, not a conformance claim.

Design notes for review

Syft was evaluated and rejected. On this repository its directory scan reported zero components for the Gradle and NuGet modules that have real runtime dependencies, collected react-native-iap's entire yarn.lock dev tree, picked up CI workflow actions as Apple components, and embedded local filesystem paths in the output.

The Gradle/NuGet/pub manifest parsers are a deliberate stopgap. security/SBOM.md documents the plan to replace them with each ecosystem's own resolver, which removes ~350 lines and delivers the transitive closure in the same change. Not done here because no JDK, Flutter, or .NET toolchain was available to verify the result. Until then they fail loudly rather than dropping a dependency, and the tests read the real manifests so drift fails CI.

No compliance is claimed. OpenIAP issues no attestation on a downstream manufacturer's behalf, and security/CRA.md records that a CRA steward must be a legal person — an unincorporated project is generally outside that definition.

Test plan

  • 23 new tests pass (node --test scripts/generate-sbom.test.mjs)
  • All 10 components generate and pass CycloneDX 1.6 schema validation (cyclonedx validate)
  • Regeneration is byte-identical; no local paths in any output
  • Existing release automation tests still pass (38)
  • Docs typecheck, lint, and build pass; bun audit:docs clean
  • bun audit:release-state clean
  • The three new docs pages render with zero console errors (verified in Chrome)

Not verifiable locally: sbom.yml and scorecard.yml have never executed — CI and the first release after merge are their first real run.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added deterministic CycloneDX SBOM generation with dependency, license, vulnerability, and provenance metadata.
    • Added automated SBOM publication, validation, and release-asset uploads.
    • Added OpenSSF Scorecard security analysis and code-scanning integration.
  • Documentation

    • Added security, SBOM, VEX, CRA readiness, supply-chain, and compliance guidance.
    • Added dedicated security documentation pages and navigation.
  • Tests

    • Expanded SBOM generation and dependency extraction coverage.
    • Included SBOM tests in release automation.

Every published release now gets a CycloneDX 1.6 SBOM attached as a
release asset, with a provenance attestation, generated automatically
from the manifests the build already reads.

Component identity, version, and release tag come from the existing
release SSOT (release-branch-policy.mjs, assert-release-tag.mjs), so a
component cannot be released without also being describable. A test
asserts the two lists stay equal.

sbom.yml reacts to `release: published` rather than editing the nine
release workflows, so every component — including ones added later —
is covered by one code path.

Generation is deterministic: the document timestamp is the commit
timestamp and the serial number is derived from the release identity,
so regenerating at a released commit reproduces the file byte for byte.

Syft was evaluated and rejected. On this repository its directory scan
reported zero components for Gradle and NuGet modules that have real
runtime dependencies, collected react-native-iap's entire yarn.lock
dev tree, picked up CI workflow actions as Apple components, and
embedded local filesystem paths in the output.

Test and build-only dependencies are excluded, and an unresolvable
Gradle coordinate fails generation rather than silently shortening the
inventory.

Verified: 10/10 components generate, all pass CycloneDX 1.6 schema
validation, byte-identical across runs, no local paths, 19 new tests.
Builds on the SBOM foundation with the parts a consumer or regulator
actually asks for.

CRA Article 14 reporting path. Obligations apply from 11 September 2026:
24-hour early warning, 72-hour notification, 14-day final report, to
ENISA and the national CSIRT. SECURITY.md now documents that path,
plus a bug bar and support-lifecycle statement. CRA.md records the
manufacturer/steward/neither distinction — a steward must be a legal
person, so an unincorporated project is generally outside it. No
compliance is claimed and no attestation is issued on a downstream
manufacturer's behalf.

Dependency licenses, 0/47 to 43/47. Resolved from each dependency's own
registry, including Google's Maven repository, which Maven Central does
not mirror and which every androidx coordinate needs. Licenses are never
guessed: a registry value becomes an SPDX id only when it is one, and a
lookup failure leaves the field empty rather than failing a release.
Opt-in via --with-licenses so local runs stay offline and deterministic.

VEX. Whether a CVE is reachable is a human judgement, so this is the one
input that cannot be generated. security/vex/<component>.json is merged
into the SBOM when present; a not_affected claim without a justification
is rejected. Absent by default — an empty vulnerabilities array would
read as "checked, none found".

OpenSSF Scorecard. Checks the repository's own posture — branch
protection, token permissions, action pinning — which neither Dependabot
nor the SBOM covers. Worth noting: GitHub's dependency graph reports
zero packages for this repository, because bun lockfiles are unsupported
and Gradle is not resolved from source. Dependabot's version updates
still work, but the published SBOMs are the only real inventory.

Docs gain a Security section: overview with a when-each-thing-runs
table, SBOM download/verify/reproduce, and compliance covering the CRA
timeline, the conformance suite as behavioral evidence, and the
OpenChain gap assessment.

Verified: 23 tests, 10/10 SBOMs pass CycloneDX 1.6 validation with
licenses attached, docs typecheck + lint + build, audit-docs clean.
Self-review findings before opening the PR.

The docs claimed nine release workflows create GitHub Releases; there are
ten. Replaced the count with "the existing release workflows" so the
statement cannot go stale again, and did the same for the hardcoded
license-coverage and kit-dependency counts — those drift the moment a
dependency changes. The structural license gaps (pub.dev metadata, NuGet
license URLs) are stated instead, and `--with-licenses` prints the live
count.

sbom.yml interpolated the release tag and component id directly into
run: blocks. A ref name must not be able to extend the command it is an
argument to, and this is exactly what the Scorecard workflow added in the
same change would flag. Both now pass through env.
@hyochan hyochan added 🎯 feature New feature 📖 documentation Improvements or additions to documentation 💨 ci Cloud integration cross-platform Cross-platform (both Android & iOS) labels Aug 12, 2026
@codecov-commenter

codecov-commenter commented Aug 12, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.91%. Comparing base (1f85ca3) to head (4905c84).
⚠️ Report is 1 commits behind head on main.

❌ Your project status has failed because the head coverage (89.29%) is below the target coverage (90.00%). You can increase the head coverage or adjust the target coverage.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #318   +/-   ##
=======================================
  Coverage   71.91%   71.91%           
=======================================
  Files         134      134           
  Lines       14411    14411           
  Branches     4023     4023           
=======================================
  Hits        10364    10364           
  Misses       4047     4047           
Flag Coverage Δ
iapkit 59.18% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
React Native IAP 91.12% <ø> (ø)
Expo IAP 89.29% <ø> (ø)
flutter_inapp_purchase 90.07% <ø> (ø)
IAPKit Server 90.45% <ø> (ø)
IAPKit Convex 52.78% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hyochan

hyochan commented Aug 12, 2026 •

Copy link
Copy Markdown
Member Author

Preview

Walkthrough of the three new pages under Docs → Security: Overview, SBOM, and Compliance.

pr-318-security-docs.webm

Recorded against the production build (vite preview) in Chrome at 1280×800. All three pages render with zero console errors, and the sidebar Security section sits between Features and Foundation with the current page highlighted.

Browser attachment upload is not available from this environment, so the recording is committed under .github/pr-previews/ per the documented fallback.

Exact-head self-review findings, standing in for CodeRabbit while its
review limit is reached.

The npm license lookup put the package name straight into the registry
URL. A scoped name contains a slash, so it would have been read as a path
separator and 404'd. No current component hits this path — the npm
packages have no runtime dependencies — but it would fail silently the
first time one did.

security/vex/README.md requires a statement's affects[].ref to match a
bom-ref in the component's SBOM; nothing enforced it. A statement naming
a component the SBOM does not contain is an analysis no consumer's
scanner can match, so generation now rejects it.
loop-review could merge a PR that changes native code, an SDK, or the
GraphQL spec on CI alone. CI never exercises purchase dialogs, store
accounts, or device wiring, so a green run there says nothing about the
flows those paths affect.

The loop now decides before merging whether the diff needs $e2e-tests,
and stops without merging when it does — device regression needs real
hardware and store accounts, so it cannot run unattended inside the
loop. A change confined to docs, automation, or release tooling states
that explicitly instead of staying silent.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (7)
security/SBOM.md-234-240 (1)

234-240: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Specify the release repository in the download command.

This command fails for consumers who run it outside an OpenIAP checkout because gh cannot infer the repository. Add --repo hyodotdev/openiap, as the documentation page already does.

Proposed fix
-gh release download react-native-iap-16.3.0 -p '*.cdx.json'
+gh release download react-native-iap-16.3.0 \
+  --repo hyodotdev/openiap -p '*.cdx.json'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@security/SBOM.md` around lines 234 - 240, Update the SBOM download command in
the documented release verification steps to explicitly pass the OpenIAP
repository via gh’s --repo option, matching the repository used by the
subsequent attestation verification command.
scripts/generate-sbom.mjs-59-61 (1)

59-61: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The apple distribution reference ignores the version.

Line 60 accepts version and never uses it, and the template literal has no interpolation. Every apple SBOM therefore points at the same unversioned CocoaPods page, unlike the other components, whose distribution resolves to the released version.

🔧 Proposed fix
     purl: (version) => `pkg:cocoapods/openiap@${version}`,
-    distribution: (version) => `https://cocoapods.org/pods/openiap`,
+    distribution: () => "https://cocoapods.org/pods/openiap",

If a versioned landing page exists for this pod, prefer it so the reference resolves to the released version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 59 - 61, Update the apple component’s
distribution function in the SBOM generation configuration to incorporate its
version when constructing the CocoaPods URL, using the versioned landing-page
format if supported. Keep the existing purl and directory mappings unchanged.
scripts/generate-sbom.mjs-666-689 (1)

666-689: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A flag with a missing value is accepted silently.

Each option reads argv[++index] without a bounds check. If the flag is the last argument, the value is undefined:

  • --resolved yields resolvedFile undefined, which is falsy at line 638. The transitive merge is skipped and the SBOM claims to be complete with direct dependencies only.
  • --commit yields undefined, so line 631 falls back to HEAD and ignores the requested commit.
  • --output-dir yields undefined, and resolve at line 740 throws an opaque TypeError.

Reject a flag that has no value.

🔧 Proposed fix
 function parseArguments(argv) {
   const options = { componentId: "", outputDir: "sbom", toStdout: false };
+  const valueFor = (flag, index) => {
+    const value = argv[index];
+    if (value === undefined || value.startsWith("--")) {
+      throw new Error(`Option ${flag} requires a value`);
+    }
+    return value;
+  };
   for (let index = 0; index < argv.length; index += 1) {
     const argument = argv[index];
     if (argument === "--output-dir") {
-      options.outputDir = argv[++index];
+      options.outputDir = valueFor(argument, ++index);
     } else if (argument === "--commit") {
-      options.commit = argv[++index];
+      options.commit = valueFor(argument, ++index);
     } else if (argument === "--resolved") {
-      options.resolvedFile = argv[++index];
+      options.resolvedFile = valueFor(argument, ++index);
     } else if (argument === "--tag") {
-      options.tag = argv[++index];
+      options.tag = valueFor(argument, ++index);
     } else if (argument === "--stdout") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 666 - 689, Update parseArguments to
validate that each value-taking option (--output-dir, --commit, --resolved, and
--tag) has a following argument before incrementing the index; reject missing
values with a clear error instead of assigning undefined, while preserving
existing parsing for supplied values and boolean flags.
scripts/generate-sbom.mjs-462-473 (1)

462-473: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The determinism guarantee does not hold with --with-licenses.

Lines 12-15 state that re-running on the same commit reproduces the same bytes. This comment states that a failed lookup leaves the component without a license rather than failing. .github/workflows/sbom.yml line 88 passes --with-licenses. A registry outage or a 429 during the release run therefore produces a published SBOM whose bytes differ from a later regeneration, and the attestation at line 132 of that workflow signs the degraded document.

State the limitation where the determinism claim is made, so a consumer who re-generates and gets different bytes knows why.

📝 Proposed comment correction
  * Output is deterministic for a given (component, version, commit): the
  * document timestamp comes from the commit, and the serial number is derived
  * from the release identity rather than randomly generated. Re-running this on
- * the same commit reproduces the same bytes.
+ * the same commit reproduces the same bytes, except under `--with-licenses`:
+ * license data comes from live registry lookups, and a lookup that fails is
+ * omitted rather than fatal, so an outage changes the output.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 462 - 473, Update the determinism
documentation in scripts/generate-sbom.mjs to explicitly state that
reproducibility does not apply when --with-licenses is enabled and registry
lookups fail or are rate-limited. Keep the existing explanation of
registry-sourced licenses, but document that such failures can produce an SBOM
without license metadata and therefore different bytes on regeneration.
scripts/generate-sbom.mjs-575-597 (1)

575-597: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Require at least one affects entry for every VEX statement.

The current statement.affects ?? [] loop skips validation when affects is absent, allowing an unmatchable VEX claim into the SBOM. Add this validation and a regression test. VEX_STATES already matches the CycloneDX 1.6 enum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 575 - 597, Update the VEX statement
validation loop around the visible state and justification checks to require
every statement to contain at least one affects entry, rejecting missing or
empty affects arrays before publication. Preserve existing validation behavior,
and add a regression test covering a statement without affects.
scripts/generate-sbom.mjs-291-306 (1)

291-306: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Map the deprecated SPDX spelling to an SPDX expression

Remove GPL-2.0-with-classpath-exception from KNOWN_SPDX_IDS. Add Classpath-exception-2.0, and normalize the spelling to GPL-2.0-only WITH Classpath-exception-2.0 as an expression. Update the alias branch so expression aliases are not emitted as license.id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 291 - 306, Update the SPDX
normalization logic around KNOWN_SPDX_IDS to remove
GPL-2.0-with-classpath-exception, add Classpath-exception-2.0, and map the
deprecated spelling to the expression GPL-2.0-only WITH Classpath-exception-2.0.
Adjust the alias branch so this mapped value is emitted as expression rather
than license.id.
scripts/sbom-dependencies.mjs-49-72 (1)

49-72: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Read the repository-root gradle.properties.

readGradleProperties stops before depth === 0, so it omits Gradle’s root fallback layer. The current repository has no root file, and COMPOSE_UI_VERSION resolves correctly from packages/google/gradle.properties. Include the root layer to prevent incorrect SBOM versions when a modeled manifest uses a root-only property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sbom-dependencies.mjs` around lines 49 - 72, Update
readGradleProperties so its directory-walk loop includes depth === 0, allowing
it to read the repository-root gradle.properties fallback while preserving the
existing nearest-property precedence behavior.
🧹 Nitpick comments (5)
scripts/generate-sbom.test.mjs (4)

76-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert byte equality, not only serial-number equality.

The module docstring in scripts/generate-sbom.mjs lines 12-15 claims that re-running reproduces the same bytes. This test only compares serialNumber. A non-deterministic field elsewhere in the document, for example a dependency order change, would not fail here.

♻️ Proposed addition
   assert.equal(first.serialNumber, second.serialNumber);
+  assert.equal(JSON.stringify(first), JSON.stringify(second));
   assert.match(first.serialNumber, /^urn:uuid:[0-9a-f-]{36}$/u);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.test.mjs` around lines 76 - 102, Update the test around
buildSbom to serialize the complete outputs from the two identical inputs and
assert byte-for-byte equality, not just matching serialNumber values. Retain the
existing UUID format assertion and differing-commit assertion so release
identity behavior remains covered.

319-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The release path --with-licenses has no test coverage.

This test asserts the offline default. .github/workflows/sbom.yml line 88 runs the generator with --with-licenses, so the release always exercises attachLicenses and lookupLicense, and neither has a test. The workflow also runs this suite at line 74 as its pre-generation gate, so a defect in the license path reaches a published, attested artifact unchecked.

Add a test that injects a stub fetch or a stub lookup and asserts that a failed lookup degrades to a component with no licenses key rather than throwing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.test.mjs` around lines 319 - 328, Add coverage for the
with-licenses generation path by supplying a stub fetch or lookup that fails,
then assert generation completes and the affected component omits the licenses
key. Keep the existing offline-default test unchanged and exercise the
attachLicenses/lookupLicense flow through generateSbom.

260-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This assertion breaks whenever a Flutter runtime dependency is added.

assert.deepEqual pins the exact dependency list of libraries/flutter_inapp_purchase/pubspec.yaml. Adding a legitimate runtime dependency fails this test even though the extractor is correct. The test name states the intent narrowly: exclude the Flutter SDK entry.

The neighbouring tests at lines 180-186 use inclusion and exclusion assertions and tolerate manifest growth.

♻️ Proposed refactor
 test("pub dependencies exclude the Flutter SDK itself", () => {
   const names = extractPub(repoRoot, COMPONENTS.flutter.source).map(
     (entry) => entry.name,
   );
-  assert.deepEqual(names, ["http", "meta", "platform"]);
+  assert.ok(names.includes("http"));
+  assert.ok(names.includes("meta"));
+  // `flutter: sdk: flutter` is the SDK, not a pub.dev package.
+  assert.ok(!names.includes("flutter"));
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.test.mjs` around lines 260 - 265, Update the “pub
dependencies exclude the Flutter SDK itself” test to assert that the extracted
names exclude the Flutter SDK entry and include the expected current
dependencies without requiring an exact full list. Follow the neighboring
inclusion/exclusion assertion style so legitimate runtime dependencies added to
the manifest do not break the test.

330-341: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Register cleanup before the operations that can throw.

t.after is registered at line 334, after mkdirSync at line 333. If mkdirSync throws, the temporary directory created at line 331 is never removed.

The array form of a VEX file and the VEX file must be an array or {"vulnerabilities": [...]} error at scripts/generate-sbom.mjs line 570 are also untested.

♻️ Proposed refactor
   const scratch = mkdtempSync(resolve(tmpdir(), "openiap-vex-"));
+  t.after(() => rmSync(scratch, { recursive: true, force: true }));
   const vexDir = resolve(scratch, "security/vex");
   mkdirSync(vexDir, { recursive: true });
-  t.after(() => rmSync(scratch, { recursive: true, force: true }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.test.mjs` around lines 330 - 341, Move the t.after
cleanup registration in the “VEX statements are validated, and absent by
default” test to immediately after creating scratch, before mkdirSync can throw.
Extend the test to cover array-form VEX files and assert that an invalid object
shape triggers the “VEX file must be an array or {"vulnerabilities": [...]}"
validation error from readVexStatements.
scripts/generate-sbom.mjs (1)

533-541: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

License lookup fans out without a concurrency limit.

Promise.all starts one lookup per dependency at once. The maven branch issues up to two requests per entry. With a merged transitive closure this becomes hundreds of simultaneous requests to Maven Central, Google Maven, npm, and NuGet. Those registries rate-limit. A 429 response returns null at line 458, so the result is silently missing license data rather than an error.

Bound the concurrency so a large closure does not trigger throttling.

♻️ Proposed refactor to cap concurrency
 async function attachLicenses(dependencies) {
-  const resolved = await Promise.all(
-    dependencies.map(async (entry) => {
-      const license = await lookupLicense(entry);
-      return license ? { ...entry, licenses: [license] } : entry;
-    }),
-  );
-  return resolved;
+  const LOOKUP_CONCURRENCY = 8;
+  const resolved = new Array(dependencies.length);
+  let next = 0;
+  const worker = async () => {
+    while (next < dependencies.length) {
+      const index = next++;
+      const entry = dependencies[index];
+      const license = await lookupLicense(entry);
+      resolved[index] = license ? { ...entry, licenses: [license] } : entry;
+    }
+  };
+  await Promise.all(
+    Array.from({ length: Math.min(LOOKUP_CONCURRENCY, dependencies.length) }, worker),
+  );
+  return resolved;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-sbom.mjs` around lines 533 - 541, Update attachLicenses to
process dependencies with a bounded concurrency limit instead of launching every
lookup through Promise.all at once. Ensure lookupLicense calls are throttled
while preserving each entry’s existing license-enrichment behavior and the
resolved result order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/sbom.yml:
- Around line 51-61: Pin the privileged workflow’s actions/checkout,
actions/setup-node, and actions/attest-build-provenance references to full
immutable commit SHAs, adding each action’s version in a trailing comment.
Update the action usages in the checkout, Node setup, and attestation steps;
leave the setup-node cache configuration unchanged because no cache is enabled
here.

Apply the same fix in @.github/workflows/scorecard.yml around lines 38 - 43: The
Scorecard workflow also uses mutable action references and its pinning check
requires immutable references.
- Around line 90-128: Update the SBOM workflow around the “Verify the SBOM
describes this release” step to validate "$SBOM_FILE" against the CycloneDX 1.6
schema before any attestation or publication steps run. Add a reproducibly
pinned CycloneDX CLI installation or invocation, then run its validation with
input version v1_6 and fail the workflow on validation errors; keep the existing
version, tag, commit, and local-path checks intact.

Apply the same fix in `@scripts/sbom-dependencies.mjs` around lines 496 - 506:
Resolver entries need field-level validation before they are merged into the
generated document.

In `@scripts/generate-sbom.mjs`:
- Around line 438-446: The dependency graph construction around the
`dependencies` array must make every transitive component reachable from
`componentRef`; update `dependsOn` so transitive entries are attached to the
root, or preserve their actual resolver parent relationships if available. Keep
`openiap:sbom:relationship` as the marker distinguishing transitive components,
and retain empty dependency lists only for components without known child edges.

Apply the same fix in `@scripts/generate-sbom.test.mjs` around lines 424 - 446.
- Line 427: Update the SBOM generation logic around COMPONENTS so each
component’s license metadata is emitted from its declared license or resolved
from its manifest, rather than hardcoding MIT in the licenses field. Ensure the
kmp component produces Apache-2.0 while preserving the existing license
structure for other components.
- Around line 226-263: Add round-trip tests for componentFromTag and
releaseTagFor covering every canonical release tag defined by PACKAGE_CONFIG,
asserting each generated tag maps back to the expected component and version.
Also cover the bare Apple version tag and Google legacy aliases, ensuring all
supported aliases are recognized rather than returning null.

Apply the same fix in @.github/workflows/sbom.yml around lines 144 - 149.

In `@scripts/sbom-dependencies.mjs`:
- Around line 140-159: Update stripTestSourceSets so an unmatched test
source-set block is detected when brace scanning reaches the end without
returning depth to zero. Fail closed by throwing an error instead of slicing
through the end of the manifest; preserve the existing removal behavior only for
balanced blocks.

In `@SECURITY.md`:
- Around line 102-105: Update the “Supply Chain” section’s SBOM statement to
limit the guarantee to supported component release tags, rather than claiming
every published release carries an SBOM. Preserve the guidance about checking a
matched component version’s dependencies.

In `@security/CRA.md`:
- Around line 41-44: Revise the steward liability statement in the CRA security
guidance to remove the absolute claim that stewards cannot be fined. Describe
only the scoped exclusion under Article 64(10), while preserving the separate
point that stewards must not issue compliance attestations or warranties; use
legally reviewed wording consistent with Articles 64(2), 64(10), and 24(3).
- Around line 52-62: Correct the final-report timing language across all three
sites: in security/CRA.md lines 52-62, state that vulnerability reports are due
within 14 days after a corrective or mitigating measure becomes available and
severe-incident reports within one month after incident notification; in
packages/docs/src/pages/docs/security/overview.tsx lines 119-123, remove any
wording that makes the 14-day deadline begin at awareness; and in
packages/docs/src/pages/docs/security/compliance.tsx lines 43-68, add the
corrective-or-mitigating-measure trigger for vulnerability final reports.

Apply the same fix in `@SECURITY.md` around lines 71 - 87: The reporting table
uses the incorrect awareness-based trigger.

---

Minor comments:
In `@scripts/generate-sbom.mjs`:
- Around line 59-61: Update the apple component’s distribution function in the
SBOM generation configuration to incorporate its version when constructing the
CocoaPods URL, using the versioned landing-page format if supported. Keep the
existing purl and directory mappings unchanged.
- Around line 666-689: Update parseArguments to validate that each value-taking
option (--output-dir, --commit, --resolved, and --tag) has a following argument
before incrementing the index; reject missing values with a clear error instead
of assigning undefined, while preserving existing parsing for supplied values
and boolean flags.
- Around line 462-473: Update the determinism documentation in
scripts/generate-sbom.mjs to explicitly state that reproducibility does not
apply when --with-licenses is enabled and registry lookups fail or are
rate-limited. Keep the existing explanation of registry-sourced licenses, but
document that such failures can produce an SBOM without license metadata and
therefore different bytes on regeneration.
- Around line 575-597: Update the VEX statement validation loop around the
visible state and justification checks to require every statement to contain at
least one affects entry, rejecting missing or empty affects arrays before
publication. Preserve existing validation behavior, and add a regression test
covering a statement without affects.
- Around line 291-306: Update the SPDX normalization logic around KNOWN_SPDX_IDS
to remove GPL-2.0-with-classpath-exception, add Classpath-exception-2.0, and map
the deprecated spelling to the expression GPL-2.0-only WITH
Classpath-exception-2.0. Adjust the alias branch so this mapped value is emitted
as expression rather than license.id.

In `@scripts/sbom-dependencies.mjs`:
- Around line 49-72: Update readGradleProperties so its directory-walk loop
includes depth === 0, allowing it to read the repository-root gradle.properties
fallback while preserving the existing nearest-property precedence behavior.

In `@security/SBOM.md`:
- Around line 234-240: Update the SBOM download command in the documented
release verification steps to explicitly pass the OpenIAP repository via gh’s
--repo option, matching the repository used by the subsequent attestation
verification command.

---

Nitpick comments:
In `@scripts/generate-sbom.mjs`:
- Around line 533-541: Update attachLicenses to process dependencies with a
bounded concurrency limit instead of launching every lookup through Promise.all
at once. Ensure lookupLicense calls are throttled while preserving each entry’s
existing license-enrichment behavior and the resolved result order.

In `@scripts/generate-sbom.test.mjs`:
- Around line 76-102: Update the test around buildSbom to serialize the complete
outputs from the two identical inputs and assert byte-for-byte equality, not
just matching serialNumber values. Retain the existing UUID format assertion and
differing-commit assertion so release identity behavior remains covered.
- Around line 319-328: Add coverage for the with-licenses generation path by
supplying a stub fetch or lookup that fails, then assert generation completes
and the affected component omits the licenses key. Keep the existing
offline-default test unchanged and exercise the attachLicenses/lookupLicense
flow through generateSbom.
- Around line 260-265: Update the “pub dependencies exclude the Flutter SDK
itself” test to assert that the extracted names exclude the Flutter SDK entry
and include the expected current dependencies without requiring an exact full
list. Follow the neighboring inclusion/exclusion assertion style so legitimate
runtime dependencies added to the manifest do not break the test.
- Around line 330-341: Move the t.after cleanup registration in the “VEX
statements are validated, and absent by default” test to immediately after
creating scratch, before mkdirSync can throw. Extend the test to cover
array-form VEX files and assert that an invalid object shape triggers the “VEX
file must be an array or {"vulnerabilities": [...]}" validation error from
readVexStatements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38cf4331-bc74-4ff7-83ea-ea6683831c97

📥 Commits

Reviewing files that changed from the base of the PR and between 84b1d90 and df3be42.

⛔ Files ignored due to path filters (1)
  • .github/pr-previews/pr-318-security-docs.webm is excluded by !**/*.webm
📒 Files selected for processing (20)
  • .github/workflows/ci.yml
  • .github/workflows/sbom.yml
  • .github/workflows/scorecard.yml
  • .gitignore
  • SECURITY.md
  • package.json
  • packages/docs/src/pages/docs/index.tsx
  • packages/docs/src/pages/docs/security/compliance.tsx
  • packages/docs/src/pages/docs/security/overview.tsx
  • packages/docs/src/pages/docs/security/sbom.tsx
  • scripts/assert-release-tag.mjs
  • scripts/generate-sbom.mjs
  • scripts/generate-sbom.test.mjs
  • scripts/release-branch-policy.mjs
  • scripts/sbom-dependencies.mjs
  • security/CRA.md
  • security/README.md
  • security/SBOM.md
  • security/openchain.md
  • security/vex/README.md

Comment thread .github/workflows/sbom.yml
Comment thread scripts/generate-sbom.mjs
Comment thread scripts/generate-sbom.mjs Outdated
Comment thread scripts/generate-sbom.mjs
Comment thread scripts/sbom-dependencies.mjs
Comment thread SECURITY.md Outdated
Comment thread security/CRA.md Outdated
Comment thread security/CRA.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.codex/skills/loop-review/SKILL.md:
- Around line 122-125: Resolve the contradiction in the loop-review merge gate
around the “stop without merging” instruction: remove the option for the user to
confirm coverage without running $e2e-tests, so required device-regression rows
always require that test before merging. Keep the requirement to report
implicated regression-matrix rows and record the test result on the PR.
- Around line 113-120: Expand the `$e2e-tests` gate scope to explicitly include
package-manifest files under `packages/apple/`, `packages/google/`,
`packages/kit/`, and `libraries/<sdk>/`. Ensure manifest-only dependency or
script changes trigger the gate, either by listing these paths or clearly
defining “dependency placement” to cover them.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 92ef14a2-6ec2-4786-bf65-cc0d034c4ce5

📥 Commits

Reviewing files that changed from the base of the PR and between df3be42 and ae021d3.

📒 Files selected for processing (2)
  • .claude/skills/loop-review/SKILL.md
  • .codex/skills/loop-review/SKILL.md

Comment thread .codex/skills/loop-review/SKILL.md
Comment thread .codex/skills/loop-review/SKILL.md Outdated
Measured the generated SBOMs against the NTIA minimum elements that
OpenSSF recommends checking, and closed the two that failed.

Component supplier went from 0/47 to 44/47 and every document now names
its author. The supplier comes from the same POM, nuspec, or registry
document the license already required, so this costs no extra request.
The remaining three are pub.dev packages, whose metadata exposes neither
field.

/audit-security makes this repeatable: generation, schema validity, NTIA
completeness, leaked paths, determinism, workflow injection and
permissions, gitignore coverage, documentation drift, and release
integrity — each step a command that prints evidence. Running it against
this branch found no injection sites left, and surfaced eight existing
workflows with no permissions block, which is left as separate work.

Docs pages now separate content data from rendering: repeated rows are
typed module-level constants fed through a new DataTable component,
rather than hand-written JSX that differs only in its text. Added as a
mandatory rule in 05-docs-patterns.md so later pages follow it.

References went from 3 links to 40, all verified 200 — the standards
(CycloneDX, purl, SPDX, NTIA, SLSA, in-toto, Sigstore), the registries
read at generation time, the actions used in CI, the regulation and
guidance behind CRA.md, and the OpenChain specs behind the gap
assessment. security/README.md also stated that Dependabot alerts match
this repository's manifests; that was wrong, because the dependency
graph is empty here.
CodeRabbit review of 553b52a. Seven findings were valid, three were not.

The SBOM asserted MIT for every component. kmp-iap publishes under
Apache-2.0 — its POM and podspec both say so — so its SBOM claimed a
licence it does not ship. Components now declare their own licence and a
test compares each against the manifest that publishes it.

Transitive entries were orphaned in the dependency graph: they appeared
under components but had no inbound edge, so a consumer walking from the
root would never reach one. They are now in the root's dependsOn, with
the transitive marker left as the property that distinguishes them.

The brace scanner that strips KMP test source sets could silently
discard the rest of a manifest when braces did not balance — the exact
failure mode this module exists to prevent. It now fails closed.

CRA deadlines: only the 24h and 72h clocks run from awareness. A
vulnerability's final report runs from a fix or mitigation being
available, and an incident's from the 72-hour notification. SECURITY.md
is now labelled as an internal service level rather than a restatement
of statutory deadlines. The Article 64(10) fine exemption is scoped to
subsections (3)-(9) instead of stated absolutely.

SECURITY.md also promised an SBOM for "every published release"; the
workflow skips tags with no matching component.

The loop-review gate's "or confirm it is covered" escape had no
criteria. It now takes either an e2e-tests run or an explicit written
waiver naming the rows, recorded on the PR — and the loop can never
grant one to itself.
Three gaps surfaced during review that belong to the repository rather
than to this change: actions pinned by tag instead of commit SHA, CI
workflows without a permissions block, and GitHub's empty dependency
graph. Recording them beats carrying them silently, and /audit-security
re-checks each one.
CodeRabbit was right that the one-off verification proved the current
state without protecting the future one. The check now iterates
PACKAGE_CONFIG in CI, so adding a tag pattern to the release SSOT
without teaching TAG_PREFIXES fails here instead of silently shipping a
release with no SBOM.
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Aug 12, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Aug 12, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Aug 12, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Aug 12, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Aug 12, 2026
@hyochan
hyochan merged commit ce9ec53 into main Aug 12, 2026
15 checks passed
@hyochan
hyochan deleted the feat/sbom-supply-chain branch August 12, 2026 23:41
hyochan added a commit that referenced this pull request Aug 13, 2026
Complete the remaining issue #323 SBOM verification and guarded recovery work after #318, #319, and #332.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💨 ci Cloud integration cross-platform Cross-platform (both Android & iOS) 📖 documentation Improvements or additions to documentation 🎯 feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants