feat(security): per-release SBOM, CRA reporting path, and supply-chain docs - #318
Conversation
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.
|
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ 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. Additional details and impacted files@@ Coverage Diff @@
## main #318 +/- ##
=======================================
Coverage 71.91% 71.91%
=======================================
Files 134 134
Lines 14411 14411
Branches 4023 4023
=======================================
Hits 10364 10364
Misses 4047 4047
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
PreviewWalkthrough of the three new pages under Docs → Security: Overview, SBOM, and Compliance. pr-318-security-docs.webmRecorded against the production build ( Browser attachment upload is not available from this environment, so the recording is committed under |
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.
There was a problem hiding this comment.
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 winSpecify the release repository in the download command.
This command fails for consumers who run it outside an OpenIAP checkout because
ghcannot 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 winThe apple distribution reference ignores the version.
Line 60 accepts
versionand 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, whosedistributionresolves 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 winA 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 isundefined:
--resolvedyieldsresolvedFileundefined, which is falsy at line 638. The transitive merge is skipped and the SBOM claims to be complete with direct dependencies only.--commityieldsundefined, so line 631 falls back toHEADand ignores the requested commit.--output-diryieldsundefined, andresolveat line 740 throws an opaqueTypeError.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 winThe 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.ymlline 88 passes--with-licenses. A registry outage or a429during 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 winRequire at least one
affectsentry for every VEX statement.The current
statement.affects ?? []loop skips validation whenaffectsis absent, allowing an unmatchable VEX claim into the SBOM. Add this validation and a regression test.VEX_STATESalready 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 winMap the deprecated SPDX spelling to an SPDX expression
Remove
GPL-2.0-with-classpath-exceptionfromKNOWN_SPDX_IDS. AddClasspath-exception-2.0, and normalize the spelling toGPL-2.0-only WITH Classpath-exception-2.0as anexpression. Update the alias branch so expression aliases are not emitted aslicense.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 winRead the repository-root
gradle.properties.
readGradlePropertiesstops beforedepth === 0, so it omits Gradle’s root fallback layer. The current repository has no root file, andCOMPOSE_UI_VERSIONresolves correctly frompackages/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 winAssert byte equality, not only serial-number equality.
The module docstring in
scripts/generate-sbom.mjslines 12-15 claims that re-running reproduces the same bytes. This test only comparesserialNumber. 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 winThe release path
--with-licenseshas no test coverage.This test asserts the offline default.
.github/workflows/sbom.ymlline 88 runs the generator with--with-licenses, so the release always exercisesattachLicensesandlookupLicense, 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
licenseskey 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 winThis assertion breaks whenever a Flutter runtime dependency is added.
assert.deepEqualpins the exact dependency list oflibraries/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 valueRegister cleanup before the operations that can throw.
t.afteris registered at line 334, aftermkdirSyncat line 333. IfmkdirSyncthrows, 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 atscripts/generate-sbom.mjsline 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 winLicense lookup fans out without a concurrency limit.
Promise.allstarts 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. A429response returnsnullat 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
⛔ Files ignored due to path filters (1)
.github/pr-previews/pr-318-security-docs.webmis excluded by!**/*.webm
📒 Files selected for processing (20)
.github/workflows/ci.yml.github/workflows/sbom.yml.github/workflows/scorecard.yml.gitignoreSECURITY.mdpackage.jsonpackages/docs/src/pages/docs/index.tsxpackages/docs/src/pages/docs/security/compliance.tsxpackages/docs/src/pages/docs/security/overview.tsxpackages/docs/src/pages/docs/security/sbom.tsxscripts/assert-release-tag.mjsscripts/generate-sbom.mjsscripts/generate-sbom.test.mjsscripts/release-branch-policy.mjsscripts/sbom-dependencies.mjssecurity/CRA.mdsecurity/README.mdsecurity/SBOM.mdsecurity/openchain.mdsecurity/vex/README.md
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
.claude/skills/loop-review/SKILL.md.codex/skills/loop-review/SKILL.md
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.
Summary
Securitydocs 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:
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)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.ymlreacts torelease: publishedinstead of editing the existing release workflows, so components added later are covered by the same path.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_affectedclaim without a justification is rejected. Absent by default — an emptyvulnerabilitiesarray 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
Securitysection — 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.mdself-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 entireyarn.lockdev 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.mddocuments 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.mdrecords that a CRA steward must be a legal person — an unincorporated project is generally outside that definition.Test plan
node --test scripts/generate-sbom.test.mjs)cyclonedx validate)bun audit:docscleanbun audit:release-statecleanNot verifiable locally:
sbom.ymlandscorecard.ymlhave never executed — CI and the first release after merge are their first real run.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests