Skip to content

release: fail the SBOM check unless every declared runtime dependency is enumerated - #141

Open
yakimoto wants to merge 1 commit into
mainfrom
fix/sbom-after-install
Open

yakimoto wants to merge 1 commit into
mainfrom
fix/sbom-after-install

Conversation

@yakimoto

@yakimoto yakimoto commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

User description

What

sbom.yml already generates both SBOMs from the installed tree (npm ci, then npm sbom --omit dev for CycloneDX and SPDX), so they describe what a consumer of npm install @wave-av/sdk actually receives. The validator that gates the upload, scripts/supply-chain/validate-sbom.mjs, only required the SPDX document to list at least one package. A document produced from an uninstalled checkout satisfies that with a single self-describing entry and zero dependencies, so a regression to scanning before install would have shipped silently.

This PR extends validateSboms() with a per-name check and keeps every existing check intact:

  • takes the dependencies keys of package.json as the declared runtime set, so the floor tracks the manifest and is never a hardcoded number;
  • requires a versioned SPDX packages[] entry for the package itself and for every declared name (entries with a missing, empty, or NOASSERTION versionInfo do not count);
  • requires a CycloneDX component for every declared name, matched by name or by npm purl (scoped, percent-encoded names such as pkg:npm/%40wave-av/sdk@2.1.3 decode correctly);
  • refuses a manifest that declares no runtime dependencies at all, because a floor of 0 validates nothing;
  • failures name the missing packages; the success line now reports how many declared dependencies were enumerated in both documents.

Three small pure helpers are exported (declaredDependencies, spdxVersionedNames, cyclonedxComponentNames) so the parsing is unit-testable on its own. No workflow YAML changes; the step in sbom.yml that calls the script is unchanged.

Tests

scripts/supply-chain/__tests__/validate-sbom.test.ts: fixtures now carry the versionInfo that npm sbom actually emits and a package.json with the real single runtime dependency. New cases cover the uninstalled shape (root only), NOASSERTION / missing versionInfo, a missing CycloneDX component, a missing root SPDX entry, the zero-floor refusal, the success summary, and the purl name parser.

Local receipts (GitHub Actions is billing-locked, so nothing ran in CI)

  • ./node_modules/.bin/vitest run scripts/supply-chain (repo-pinned vitest 4.1.11): 2 files, 38 tests passed.
  • Real npm sbom --omit dev on the installed 2.1.3 tree: SPDX 2 packages, CycloneDX 1 component; validator exit 0 with all 1 declared runtime dependencies enumerated in both.
  • Same documents with every non-root entry stripped (the uninstalled shape): validator exit 1, no versioned package entry for declared runtime dependency(ies): eventemitter3.
  • npm sbom on a tree with no node_modules refuses outright (ESBOMPROBLEMS), which is the property the workflow comment already relies on; the validator is the second, independent gate.
  • actionlint .github/workflows/*.yml: only a pre-existing info-level SC2016 in governance-enforce.yml, untouched here.
  • eslint on this script reports the same 7 no-undef process findings on main and on this branch (CI lints src/ only), so no new lint debt.

Why it matters

An SBOM that enumerates only the top-level package is a claim, not an inventory. Checking every declared dependency by name in both documents makes the release job fail loudly if the installed tree ever stops being what npm sbom reads.

🤖 Generated with Claude Code


Note

Low Risk
Supply-chain release gating only: stricter validation with broad test coverage and clearer failure messages, with no changes to runtime SDK behavior or auth/data paths.

Overview
Tightens pre-upload SBOM validation so a non-empty SPDX document alone can’t pass when dependencies are missing—closing the “scan before npm ci” regression where only the root package appears.

validateSboms() now derives the required set from package.json dependencies and insists each name shows up in both SBOMs: SPDX entries must have real versionInfo (not missing, empty, or NOASSERTION), including the released package itself; CycloneDX matches by name or decoded npm purl. Manifests with zero runtime dependencies are rejected outright. Success output includes declaredDependencyCount.

Three exported helpers (declaredDependencies, spdxVersionedNames, cyclonedxComponentNames) support the checks and unit tests. Fixtures and new Vitest cases cover the uninstalled shape, weak SPDX entries, missing CycloneDX components, missing root SPDX entry, and purl parsing. No workflow YAML changes—the existing sbom.yml step still calls the same script.

Reviewed by Cursor Bugbot for commit 0686f0d. Bugbot is set up for automated code reviews on this repo. Configure here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic


CodeAnt-AI Description

Require release SBOMs to include every runtime dependency

What Changed

  • Release validation now rejects SBOMs missing any dependency declared for runtime use.
  • Both SPDX and CycloneDX documents must include the package’s dependencies, while SPDX entries must include real versions.
  • SBOMs are rejected when the package has no declared runtime dependencies or when the package itself is missing from SPDX.
  • Validation errors identify the missing packages, and successful checks report the number of dependencies covered.
  • Tests cover uninstalled trees, missing or unversioned entries, scoped package names, and empty dependency manifests.

Impact

✅ Prevents incomplete dependency inventories from shipping
✅ Catches SBOMs generated before dependencies are installed
✅ Clearer release failures for missing packages

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

… is enumerated

Both SBOMs are generated from the installed tree (npm ci, then npm sbom
--omit dev) so they describe what a consumer's `npm install` receives. The
validator that gates the upload, however, only required the SPDX document to
list at least one package, which a document produced from an uninstalled
checkout satisfies with a single self-describing entry and zero dependencies.

Extend scripts/supply-chain/validate-sbom.mjs: take the `dependencies` keys
of package.json as the declared runtime set (never a hardcoded number),
require a versioned SPDX entry for the package itself and for every declared
name (missing/empty/NOASSERTION versionInfo does not count), require a
matching CycloneDX component for every declared name (by `name` or npm purl,
scoped and percent-encoded names included), and refuse a manifest that
declares no runtime dependencies at all. Failures name the missing packages.

Tests: fixtures now carry the versionInfo npm sbom actually emits; new cases
cover the uninstalled shape, NOASSERTION entries, a missing CycloneDX
component, a missing root entry, the zero-floor refusal, and the purl parser.
vitest (repo-pinned 4.1.11): 38 passed. Local npm sbom receipt on 2.1.3:
installed tree -> exit 0; stripped document -> exit 1 (missing eventemitter3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your workspace is out of credits. Ask your workspace admin to add credits to resume reviews. Manage billing

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @yakimoto, this account has used its review budget of 2,500,000 diff characters for the last 7 days.

You can request another review in 52 minutes by commenting @sourcery-ai review.

@codeant-ai

codeant-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 0686f0d Sep 11, 2026 · 18:37 18:40

@codeant-ai

codeant-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@cursor

cursor Bot commented Sep 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_af8b68bf-a650-4983-81f1-12718b7c695d)

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 54 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 72 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: ed02c97b-9441-41bd-951e-972a0cc64c7c

📥 Commits

Reviewing files that changed from the base of the PR and between 3aec164 and 0686f0d.

📒 Files selected for processing (2)
  • scripts/supply-chain/__tests__/validate-sbom.test.ts
  • scripts/supply-chain/validate-sbom.mjs

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR adds a manifest-driven second gate for SBOM completeness, requiring every declared runtime dependency to appear with usable identity data in both SPDX and CycloneDX documents while preserving existing format and release-version checks. It also adds focused pure helpers, actionable failures, success reporting, and regression tests for uninstalled-tree and npm purl edge cases.

Flow diagram for manifest-driven SBOM validation

flowchart TD
    Manifest[package.json dependencies] --> Declared[declaredDependencies]
    SPDX[SPDX document] --> SPDXNames[spdxVersionedNames]
    CycloneDX[CycloneDX document] --> CDXNames[cyclonedxComponentNames]
    Declared --> Gate{Every declared dependency present?}
    SPDXNames --> Gate
    CDXNames --> Gate
    Gate -->|No runtime dependencies| RejectFloor[Reject zero dependency floor]
    Gate -->|Missing versioned SPDX entry| RejectSPDX[Fail with missing package names]
    Gate -->|Missing CycloneDX component| RejectCDX[Fail with missing package names]
    Gate -->|All checks pass| Summary[Report dependencies enumerated in both SBOMs]
Loading

File-Level Changes

Change Details Files
Strengthen SBOM validation from a non-empty-document check to complete enumeration of declared runtime dependencies.
  • Derive the required dependency set from package.json dependencies and reject a zero-dependency floor.
  • Require versioned SPDX entries for the package itself and every declared dependency.
  • Require every declared dependency as a CycloneDX component, supporting component names and decoded npm purls.
  • Return the declared dependency count and include it in the successful validation output.
  • Add explicit errors identifying missing packages and likely uninstalled-tree generation.
scripts/supply-chain/validate-sbom.mjs
Expand unit coverage for installed-tree SBOM validation and name extraction.
  • Update fixtures with realistic package versions and a runtime dependency.
  • Cover missing, unversioned, and NOASSERTION SPDX entries, missing root and dependency entries, missing CycloneDX components, and zero dependencies.
  • Test success summaries and scoped percent-encoded npm purl parsing.
  • Export and test pure dependency/name extraction helpers.
scripts/supply-chain/__tests__/validate-sbom.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Sep 11, 2026
@gitar-bot

gitar-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your team has used its included automatic processing for this billing period (headroom scales with your seat count). You can still comment "Gitar review" to run one anytime, and automatic reviews resume on their own by October 1. Add seats for more headroom.
Learn more

Code Review ✅ Approved

Strengthens pre-upload SBOM validation to require every declared runtime dependency in both SPDX and CycloneDX documents, closing the regression where an uninstalled tree would ship with only the root package. Exports three helpers for unit-testable parsing and includes comprehensive test coverage for missing versions, scoped names, and edge cases. No issues found.

Options

Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@macroscopeapp

macroscopeapp Bot commented Sep 11, 2026

Copy link
Copy Markdown

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This focused change hardens the release SBOM validator and can block SBOM publication when declared dependencies are not fully represented. Because it modifies a security-sensitive supply-chain control in the release process, human review is warranted.

Not approved because:

  • Credit balance exhausted. Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 2 files

Confidence score: 3/5

  • In scripts/supply-chain/validate-sbom.mjs, whitespace-only versionInfo can bypass the fail-closed dependency validation, allowing malformed SPDX entries through; trim the value before checking it against empty or NOASSERTION.
  • In scripts/supply-chain/validate-sbom.mjs, a missing usable package.json name can skip the required root SPDX validation, weakening release-manifest checks; reject nameless manifests first and make the root check unconditional.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/supply-chain/validate-sbom.mjs">

<violation number="1" location="scripts/supply-chain/validate-sbom.mjs:50">
P2: A whitespace-only `versionInfo` is accepted as versioned, allowing a malformed SPDX entry to pass the fail-closed dependency check. Trim `versionInfo` before testing it against the empty and `NOASSERTION` values.</violation>

<violation number="2" location="scripts/supply-chain/validate-sbom.mjs:107">
P2: When `package.json` has no usable `name`, this condition skips the required root SPDX check entirely. Reject a nameless release manifest before checking the root entry, then make the root check unconditional.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant CI as Release CI
    participant NPM as npm CLI
    participant FS as Installed Tree (node_modules)
    participant SBOM as SBOM Generator
    participant VAL as validateSbom.mjs
    participant PKG as package.json
    participant SPDX as SPDX Doc
    participant CDX as CycloneDX Doc

    Note over CI,CDX: Release SBOM Validation Flow

    CI->>NPM: npm ci
    NPM->>FS: Install dependencies
    FS-->>NPM: Installed tree ready

    CI->>SBOM: npm sbom --omit dev (SPDX)
    SBOM->>FS: Read installed packages
    FS-->>SBOM: Package inventory
    SBOM-->>CI: SPDX document

    CI->>SBOM: npm sbom --omit dev (CycloneDX)
    SBOM->>FS: Read installed packages
    FS-->>SBOM: Package inventory
    SBOM-->>CI: CycloneDX document

    CI->>VAL: validateSboms(tag, pkg, cyclonedx, spdx)
    
    VAL->>PKG: Extract dependencies keys
    PKG-->>VAL: Declared runtime deps

    alt No runtime dependencies declared
        VAL-->>CI: Error: refuses floor of 0
    else Dependencies declared
        VAL->>SPDX: Check versioned entries for each dep
        SPDX-->>VAL: Package names with versionInfo
        
        alt SPDX missing deps or weak versionInfo
            VAL-->>CI: Error: names missing packages
        else SPDX passes
            VAL->>SPDX: Check root package entry
            SPDX-->>VAL: Root package with version
            
            alt No versioned root entry
                VAL-->>CI: Error: package itself missing
            else Root entry valid
                VAL->>CDX: Check components for each dep
                CDX-->>VAL: Component names and purls
                
                alt CDX missing components
                    VAL-->>CI: Error: names missing packages
                else All checks pass
                    VAL-->>CI: declaredDependencyCount + success summary
                end
            end
        end
    end

    Note over VAL,CDX: Validation ensures SBOMs reflect installed tree, not pre-install state
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

export function spdxVersionedNames(spdx) {
const names = new Set();
for (const p of Array.isArray(spdx?.packages) ? spdx.packages : []) {
const version = typeof p?.versionInfo === 'string' ? p.versionInfo : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A whitespace-only versionInfo is accepted as versioned, allowing a malformed SPDX entry to pass the fail-closed dependency check. Trim versionInfo before testing it against the empty and NOASSERTION values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/supply-chain/validate-sbom.mjs, line 50:

<comment>A whitespace-only `versionInfo` is accepted as versioned, allowing a malformed SPDX entry to pass the fail-closed dependency check. Trim `versionInfo` before testing it against the empty and `NOASSERTION` values.</comment>

<file context>
@@ -27,10 +35,42 @@ export function versionFromTag(tag) {
+export function spdxVersionedNames(spdx) {
+  const names = new Set();
+  for (const p of Array.isArray(spdx?.packages) ? spdx.packages : []) {
+    const version = typeof p?.versionInfo === 'string' ? p.versionInfo : '';
+    if (typeof p?.name === 'string' && p.name && !NO_VERSION.has(version)) names.add(p.name);
+  }
</file context>
Suggested change
const version = typeof p?.versionInfo === 'string' ? p.versionInfo : '';
const version = typeof p?.versionInfo === 'string' ? p.versionInfo.trim() : '';

);
}
const spdxNames = spdxVersionedNames(spdx);
if (typeof pkg?.name === 'string' && pkg.name && !spdxNames.has(pkg.name)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When package.json has no usable name, this condition skips the required root SPDX check entirely. Reject a nameless release manifest before checking the root entry, then make the root check unconditional.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/supply-chain/validate-sbom.mjs, line 107:

<comment>When `package.json` has no usable `name`, this condition skips the required root SPDX check entirely. Reject a nameless release manifest before checking the root entry, then make the root check unconditional.</comment>

<file context>
@@ -54,11 +94,41 @@ export function validateSboms({ tag, pkg, cyclonedx, spdx }) {
+    );
+  }
+  const spdxNames = spdxVersionedNames(spdx);
+  if (typeof pkg?.name === 'string' && pkg.name && !spdxNames.has(pkg.name)) {
+    throw new Error(`spdx: no versioned package entry for the package itself, ${pkg.name}@${version}`);
+  }
</file context>

@yakimoto
yakimoto enabled auto-merge September 12, 2026 16:46
@yakimoto
yakimoto disabled auto-merge September 12, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant