-
Notifications
You must be signed in to change notification settings - Fork 11
feat(sbom): add SHA-256 hash support to CycloneDX SBOM component model #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
faf295f
feat(sbom): add SHA-256 hash support to CycloneDX SBOM component model
a-oren d726890
fix(sbom): address review feedback for hash support
a-oren a64017c
refactor(sbom): introduce NO_SCOPE constant for addDependency readabi…
a-oren 120f910
Merge branch 'main' into worktree-TC-4330
a-oren File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { expect } from 'chai' | ||
| import { PackageURL } from 'packageurl-js' | ||
|
|
||
| import CycloneDxSbom from '../src/cyclone_dx_sbom.js' | ||
|
|
||
| const sampleHashes = [{ alg: 'SHA-256', content: 'abc123def456' }] | ||
|
|
||
| suite('CycloneDX SBOM hash support', () => { | ||
|
|
||
| /** Verifies that addDependency with hashes produces a component containing the hashes array. */ | ||
| test('getComponent with hashes includes hashes array in component', () => { | ||
| // Given an SBOM with a root and a dependency with hashes | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const purl = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| sbom.addRoot(root) | ||
|
|
||
| // When adding a dependency with hashes | ||
| sbom.addDependency(root, purl, undefined, sampleHashes) | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Then the target component should include the hashes | ||
| const targetComponent = sbom.components.find(c => c.name === 'requests') | ||
| expect(targetComponent).to.exist | ||
| expect(targetComponent.hashes).to.deep.equal(sampleHashes) | ||
| }) | ||
|
|
||
| /** Verifies that addDependency without hashes produces a component with no hashes field. */ | ||
| test('getComponent without hashes does not include hashes field', () => { | ||
| // Given an SBOM with a root and a dependency without hashes | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| sbom.addRoot(root) | ||
|
|
||
| // When adding a dependency without hashes | ||
| sbom.addDependency(root, dep) | ||
|
|
||
| // Then the target component should not have a hashes property | ||
| const targetComponent = sbom.components.find(c => c.name === 'requests') | ||
| expect(targetComponent).to.exist | ||
| expect(targetComponent).to.not.have.property('hashes') | ||
| }) | ||
|
|
||
| /** Verifies that hashes are attached only to the target component, not the source. */ | ||
| test('addDependency forwards hashes only to target component', () => { | ||
| // Given an SBOM with a chain: root -> dep1 -> dep2 (only dep2 has hashes) | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep1 = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| const dep2 = new PackageURL('pypi', undefined, 'numpy', '1.24.0', undefined, undefined) | ||
| const dep2Hashes = [{ alg: 'SHA-256', content: '789xyz' }] | ||
| sbom.addRoot(root) | ||
| sbom.addDependency(root, dep1) | ||
|
|
||
| // When adding dep2 as a dependency of dep1 with hashes | ||
| sbom.addDependency(dep1, dep2, undefined, dep2Hashes) | ||
|
|
||
| // Then dep1 (source) should have no hashes, dep2 (target) should have hashes | ||
| const dep1Component = sbom.components.find(c => c.name === 'requests') | ||
| expect(dep1Component).to.not.have.property('hashes') | ||
| const dep2Component = sbom.components.find(c => c.name === 'numpy') | ||
| expect(dep2Component.hashes).to.deep.equal(dep2Hashes) | ||
| }) | ||
|
|
||
| /** Verifies that hashes are included in the serialized CycloneDX JSON output. */ | ||
| test('hashes appear in serialized SBOM JSON', () => { | ||
| // Given an SBOM with a hashed dependency | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| sbom.addRoot(root) | ||
| sbom.addDependency(root, dep, undefined, sampleHashes) | ||
|
|
||
| // When serializing to JSON | ||
| const json = JSON.parse(sbom.getAsJsonString({})) | ||
|
|
||
| // Then the component in JSON should contain hashes | ||
| const comp = json.components.find(c => c.name === 'requests') | ||
| expect(comp.hashes).to.deep.equal(sampleHashes) | ||
| }) | ||
|
|
||
| /** Verifies that components without hashes have no hashes field in serialized JSON. */ | ||
| test('component without hashes has no hashes in serialized SBOM JSON', () => { | ||
| // Given an SBOM with a dependency without hashes | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| sbom.addRoot(root) | ||
| sbom.addDependency(root, dep) | ||
|
|
||
| // When serializing to JSON | ||
| const json = JSON.parse(sbom.getAsJsonString({})) | ||
|
|
||
| // Then the component in JSON should not have a hashes property | ||
| const comp = json.components.find(c => c.name === 'requests') | ||
| expect(comp).to.not.have.property('hashes') | ||
| }) | ||
|
|
||
| /** Verifies that hashes are applied to an existing component that was first created without them as a source. */ | ||
| test('hashes are updated on component first seen as source without hashes', () => { | ||
| // Given a component first created as a source (without hashes) | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const mid = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| const leaf = new PackageURL('pypi', undefined, 'urllib3', '2.0.0', undefined, undefined) | ||
| const midHashes = [{ alg: 'SHA-256', content: 'abc123' }] | ||
| sbom.addRoot(root) | ||
|
|
||
| // When mid is first seen as a source (no hashes), then as a target with hashes | ||
| sbom.addDependency(mid, leaf) | ||
| sbom.addDependency(root, mid, undefined, midHashes) | ||
|
|
||
| // Then mid should have hashes despite being created first without them | ||
| const midComponent = sbom.components.find(c => c.name === 'requests') | ||
| expect(midComponent.hashes).to.deep.equal(midHashes) | ||
| }) | ||
|
|
||
| /** Verifies that hashes are applied when a component already exists as a target without hashes. */ | ||
| test('hashes are updated on component first seen as target without hashes', () => { | ||
| // Given a component first added as a target without hashes | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| const depHashes = [{ alg: 'SHA-256', content: 'abc123' }] | ||
| sbom.addRoot(root) | ||
| sbom.addDependency(root, dep) | ||
|
|
||
| // When the same component is added again as a target with hashes | ||
| const other = new PackageURL('pypi', undefined, 'other', '1.0.0', undefined, undefined) | ||
| sbom.addDependency(other, dep, undefined, depHashes) | ||
|
|
||
| // Then the component should have hashes and not be duplicated | ||
| const depComponents = sbom.components.filter(c => c.name === 'requests') | ||
| expect(depComponents).to.have.lengthOf(1) | ||
| expect(depComponents[0].hashes).to.deep.equal(depHashes) | ||
| }) | ||
|
|
||
| /** Verifies that passing an empty hashes array is treated the same as no hashes. */ | ||
| test('empty hashes array does not add hashes field', () => { | ||
| // Given an SBOM with a dependency with an empty hashes array | ||
| const sbom = new CycloneDxSbom() | ||
| const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined) | ||
| const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined) | ||
| sbom.addRoot(root) | ||
|
|
||
| // When adding a dependency with empty hashes | ||
| sbom.addDependency(root, dep, undefined, []) | ||
|
|
||
| // Then the component should not have a hashes property | ||
| const targetComponent = sbom.components.find(c => c.name === 'requests') | ||
| expect(targetComponent).to.not.have.property('hashes') | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.