Skip to content

fix(parser): flatten nested CycloneDX components so they are diffed#29

Open
dmchaledev wants to merge 1 commit into
mainfrom
claude/magical-ptolemy-zuonl9
Open

fix(parser): flatten nested CycloneDX components so they are diffed#29
dmchaledev wants to merge 1 commit into
mainfrom
claude/magical-ptolemy-zuonl9

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

CycloneDX components form a tree: a component may carry its own nested components array (sub-assemblies / bundled dependencies). This is a valid, documented part of the CycloneDX spec and is emitted by several generators. parseCycloneDX only read the top-level obj.components array (src/parser.ts:22), so every nested component was silently dropped before diff() ever saw it.

For a package keyworded supply-chain-security / vulnerability-management, that's a silent false-negative: a nested dependency that was added, removed, upgraded, or replaced never shows up in the report at all.

Reproduction (before this change)

import { parse } from '@hailbytes/sbom-diff';

const sbom = parse(JSON.stringify({
  bomFormat: 'CycloneDX', specVersion: '1.5',
  components: [
    { name: 'app', version: '1.0.0', purl: 'pkg:npm/app@1.0.0',
      components: [
        { name: 'lodash',  version: '4.17.21', purl: 'pkg:npm/lodash@4.17.21' },
        { name: 'express', version: '4.18.2',  purl: 'pkg:npm/express@4.18.2' },
      ] },
  ],
}));

console.log(sbom.components.map(c => c.name));
// before: [ 'app' ]                       ← lodash + express vanish
// after:  [ 'app', 'lodash', 'express' ]

Change

  • Add a small recursive flattenCycloneDXComponents() helper that walks the component tree depth-first (parent before children), preserving document order, and collects every component — top-level and nested.
  • Use it in parseCycloneDX() in place of the single top-level read.

JSON input cannot contain reference cycles, so the recursion always terminates. Non-object / non-array entries are skipped defensively.

Scope & compatibility

  • Confined to parser.ts. The canonical SBOM model is unchanged, so diff.ts, reporter.ts, and cli.ts are untouched — this composes cleanly with every open PR (they all operate on the post-parse model).
  • Backward compatible: flat SBOMs (no nested components) parse exactly as before.
  • Distinct from all current open issues/PRs — none touch the nested-component input path.

Tests

Adds a parser.test.ts case covering two levels of nesting, asserting depth-first order (['app', 'lodash', 'express', 'qs']) and that a deeply-nested component's fields are preserved. Full suite: 30 passing; typecheck, lint, and build all clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_015zPMEojF5G1qFdQUrxfaXC


Generated by Claude Code

CycloneDX components form a tree: a component may carry its own nested
`components` array (sub-assemblies / bundled dependencies), a valid and
documented structure emitted by several generators. parseCycloneDX only
read the top-level array, so every nested component was silently dropped
and never appeared as added/removed/upgraded — a false-negative for a
supply-chain tool.

Recursively flatten the component tree depth-first (parent before
children, preserving document order). The change is confined to the
parser; the canonical SBOM model is unchanged, so diff/reporter/CLI are
untouched. Adds a test covering two levels of nesting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zPMEojF5G1qFdQUrxfaXC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants