Skip to content

Commit d726890

Browse files
committed
fix(sbom): address review feedback for hash support
- Update existing component hashes when first seen without them, handling cases where Map iteration order causes a package to appear as a source before it appears as a target with hashes - Remove duplicate DependencyEntry typedef in python_pip.js - Add tests for hash update on pre-existing components Implements TC-4330 Assisted-by: Claude Code
1 parent faf295f commit d726890

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

src/cyclone_dx_sbom.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,13 @@ export default class CycloneDxSbom {
126126
// Ensure both components exist in the components list
127127
[sourceRef, targetRef].forEach((ref, index) => {
128128
const purl = index === 0 ? sourcePurl : targetPurl;
129-
if (this.getComponentIndex(purl) < 0) {
129+
const existingIndex = this.getComponentIndex(purl);
130+
if (existingIndex < 0) {
130131
const hashes = index === 1 ? targetHashes : undefined;
131132
this.components.push(getComponent(ref, "library", scope, undefined, hashes));
133+
} else if (index === 1 && targetHashes && targetHashes.length > 0 && !this.components[existingIndex].hashes) {
134+
// Update hashes if the component was first seen without them (e.g. as a source)
135+
this.components[existingIndex].hashes = targetHashes;
132136
}
133137
});
134138

src/providers/python_pip.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ async function provideComponent(manifest, opts = {}) {
7373
}
7474
}
7575

76-
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
77-
7876
/**
7977
*
8078
* @param {PackageURL}source

test/cyclone_dx_sbom_hashes.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,45 @@ suite('CycloneDX SBOM hash support', () => {
9696
expect(comp).to.not.have.property('hashes')
9797
})
9898

99+
/** Verifies that hashes are applied to an existing component that was first created without them as a source. */
100+
test('hashes are updated on component first seen as source without hashes', () => {
101+
// Given a component first created as a source (without hashes)
102+
const sbom = new CycloneDxSbom()
103+
const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined)
104+
const mid = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined)
105+
const leaf = new PackageURL('pypi', undefined, 'urllib3', '2.0.0', undefined, undefined)
106+
const midHashes = [{ alg: 'SHA-256', content: 'abc123' }]
107+
sbom.addRoot(root)
108+
109+
// When mid is first seen as a source (no hashes), then as a target with hashes
110+
sbom.addDependency(mid, leaf)
111+
sbom.addDependency(root, mid, undefined, midHashes)
112+
113+
// Then mid should have hashes despite being created first without them
114+
const midComponent = sbom.components.find(c => c.name === 'requests')
115+
expect(midComponent.hashes).to.deep.equal(midHashes)
116+
})
117+
118+
/** Verifies that hashes are applied when a component already exists as a target without hashes. */
119+
test('hashes are updated on component first seen as target without hashes', () => {
120+
// Given a component first added as a target without hashes
121+
const sbom = new CycloneDxSbom()
122+
const root = new PackageURL('pypi', undefined, 'my-app', '1.0.0', undefined, undefined)
123+
const dep = new PackageURL('pypi', undefined, 'requests', '2.33.1', undefined, undefined)
124+
const depHashes = [{ alg: 'SHA-256', content: 'abc123' }]
125+
sbom.addRoot(root)
126+
sbom.addDependency(root, dep)
127+
128+
// When the same component is added again as a target with hashes
129+
const other = new PackageURL('pypi', undefined, 'other', '1.0.0', undefined, undefined)
130+
sbom.addDependency(other, dep, undefined, depHashes)
131+
132+
// Then the component should have hashes and not be duplicated
133+
const depComponents = sbom.components.filter(c => c.name === 'requests')
134+
expect(depComponents).to.have.lengthOf(1)
135+
expect(depComponents[0].hashes).to.deep.equal(depHashes)
136+
})
137+
99138
/** Verifies that passing an empty hashes array is treated the same as no hashes. */
100139
test('empty hashes array does not add hashes field', () => {
101140
// Given an SBOM with a dependency with an empty hashes array

0 commit comments

Comments
 (0)