feat(sbom): add SHA-256 hash support to CycloneDX SBOM component model#503
Conversation
Add optional hashes parameter to getComponent() and addDependency() in the CycloneDX SBOM model, allowing downstream providers to attach SHA-256 artifact hashes to components. Update Sbom wrapper, base_pyproject graph structure, python_controller DependencyEntry, and python_pip SBOM creation paths to forward hash data through the full pipeline. Hashes are omitted when not provided, maintaining backward compatibility. Implements TC-4330 Assisted-by: Claude Code
Reviewer's GuideAdds optional hash metadata (e.g., SHA-256) to CycloneDX SBOM components and threads it through the SBOM model, Sbom wrapper, and Python providers while preserving backward compatibility and verifying behavior with new unit tests. Sequence diagram for hash-aware dependency additionsequenceDiagram
participant Provider as Base_pyproject_or_Python_pip
participant Sbom as Sbom
participant Model as CycloneDxSbom
participant PURL as PackageURL
Provider->>Sbom: addDependency(sourceRef, targetRef, scope, targetHashes)
Sbom->>Model: addDependency(sourceRef, targetRef, scope, targetHashes)
Model->>PURL: sourceRef.toString()
Model->>PURL: targetRef.toString()
loop ensure_components_exist
Model->>Model: getComponentIndex(purl)
alt component_not_found
Model->>Model: getComponent(ref, type, scope, licenses, hashes)
Model->>Model: push new Component into components
else component_exists
alt target_component_and_hashes_provided_and_missing
Model->>Model: set components[existingIndex].hashes = targetHashes
end
end
end
Model-->>Sbom: CycloneDxSbom (for chaining)
Sbom-->>Provider: Sbom (for chaining)
Class diagram for updated SBOM hash propagation modelclassDiagram
class Sbom {
- any sbomModel
+ addDependency(sourceRef, targetRef, scope, targetHashes)
}
class CycloneDxSbom {
- Array~Component~ components
+ addDependency(sourceRef, targetRef, scope, targetHashes) CycloneDxSbom
+ getComponentIndex(purl) number
}
class Component {
string bom_ref
string name
string purl
string type
string version
string scope
Array~License~ licenses
Array~Hash~ hashes
}
class Hash {
string alg
string content
}
class License {
}
class Base_pyproject {
- Map~string, GraphEntry~ graph
+ buildSbom(sbom)
}
class GraphEntry {
string name
string version
Array~string~ children
Array~Hash~ hashes
}
class Python_pip_provider {
+ addAllDependencies(source, dep, sbom)
+ getSbomForComponentAnalysis(manifest, opts)
}
class DependencyEntry {
string name
string version
Array~DependencyEntry~ dependencies
Array~Hash~ hashes
}
class Python_controller {
Array~DependencyEntry~ dependencies
}
class PackageURL {
+ toString() string
}
%% Relationships
Sbom --> CycloneDxSbom : wraps
CycloneDxSbom o--> Component : manages
Component o--> Hash : uses
GraphEntry o--> Hash : optional_hashes
DependencyEntry o--> Hash : optional_hashes
Base_pyproject --> Sbom : uses_addDependency
Python_pip_provider --> Sbom : uses_addDependency
Python_controller --> DependencyEntry : produces
CycloneDxSbom --> PackageURL : component_keys
Base_pyproject --> GraphEntry : uses
Python_pip_provider --> DependencyEntry : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
CycloneDxSbom.addDependency, hashes are only applied when the component is first created (whengetComponentIndex(purl) < 0); if a dependency is added earlier without hashes and later with hashes, the hash data will be silently dropped—if this is not intentional, consider merging or updating hashes on subsequent calls or documenting the requirement to pass hashes on first use. - In
python_pip.jstheDependencyEntrytypedef is duplicated (nearprovideComponentand again nearaddAllDependencies); consider consolidating this to a single shared typedef to avoid divergence when the shape changes in the future (e.g., the newly addedhashesfield).
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `CycloneDxSbom.addDependency`, hashes are only applied when the component is first created (when `getComponentIndex(purl) < 0`); if a dependency is added earlier without hashes and later with hashes, the hash data will be silently dropped—if this is not intentional, consider merging or updating hashes on subsequent calls or documenting the requirement to pass hashes on first use.
- In `python_pip.js` the `DependencyEntry` typedef is duplicated (near `provideComponent` and again near `addAllDependencies`); consider consolidating this to a single shared typedef to avoid divergence when the shape changes in the future (e.g., the newly added `hashes` field).
## Individual Comments
### Comment 1
<location path="src/cyclone_dx_sbom.js" line_range="127-130" />
<code_context>
[sourceRef, targetRef].forEach((ref, index) => {
const purl = index === 0 ? sourcePurl : targetPurl;
if (this.getComponentIndex(purl) < 0) {
- this.components.push(getComponent(ref, "library", scope));
+ const hashes = index === 1 ? targetHashes : undefined;
+ this.components.push(getComponent(ref, "library", scope, undefined, hashes));
}
</code_context>
<issue_to_address>
**issue:** Components never get their hashes updated if they are first seen without hashes and only later with hashes.
`targetHashes` are only applied when a component is first created (`getComponentIndex(purl) < 0`). If a component is first seen via an edge without hashes and later via one with hashes, the existing component will never be updated. If that discovery order is possible, consider updating existing components when hashes become available, or clearly document that only the first occurrence is used and later hashes are ignored.
</issue_to_address>
### Comment 2
<location path="src/providers/python_pip.js" line_range="19" />
<code_context>
}
-/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
+/** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
export default class Python_controller {
</code_context>
<issue_to_address>
**suggestion:** There are two `DependencyEntry` typedefs; consider consolidating to a single definition.
Defining `DependencyEntry` only once (e.g., at the top of the module) will prevent the shapes from drifting over time and make future maintenance simpler.
</issue_to_address>
### Comment 3
<location path="test/cyclone_dx_sbom_hashes.test.js" line_range="10-19" />
<code_context>
+
+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)
+
+ // 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)
+ })
+
</code_context>
<issue_to_address>
**issue (testing):** Add a test for calling addDependency with hashes when the target component already exists
This only tests the case where addDependency creates the component. Please add a test for when the target component already exists (e.g., from addRoot or a prior dependency) and addDependency is later called with hashes, to document whether hashes are merged, ignored, or otherwise handled without creating a duplicate component.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- 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
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
python_pip.addAllDependencies, the recursive call toaddAllDependenciesdoes not forwarddependency.hashes, so hashes for transitive dependencies are silently dropped; consider passing hashes through in the recursive call to preserve them across the full tree. - In
CycloneDxSbom.addDependency, hashes are only applied to an existing target component if it has nohashesfield; if the same component is later observed with different hash data this is ignored, so it may be worth explicitly deciding whether to overwrite, merge, or validate consistency and documenting that behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `python_pip.addAllDependencies`, the recursive call to `addAllDependencies` does not forward `dependency.hashes`, so hashes for transitive dependencies are silently dropped; consider passing hashes through in the recursive call to preserve them across the full tree.
- In `CycloneDxSbom.addDependency`, hashes are only applied to an existing target component if it has no `hashes` field; if the same component is later observed with different hash data this is ignored, so it may be worth explicitly deciding whether to overwrite, merge, or validate consistency and documenting that behavior.
## Individual Comments
### Comment 1
<location path="src/cyclone_dx_sbom.js" line_range="58-61" />
<code_context>
});
}
+ // Add hashes if provided (CycloneDX 1.4 format).
+ if (hashes && hashes.length > 0) {
+ componentObject.hashes = hashes;
+ }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid storing the caller’s `hashes` array by reference to prevent accidental mutation.
Because `hashes` is assigned directly to `componentObject`, any later changes to `hashes` by the caller will also change the SBOM. To avoid this shared mutable state, clone the array (and ideally its elements), for example:
```ts
componentObject.hashes = hashes.map(h => ({ alg: h.alg, content: h.content }));
```
At minimum, shallow-copy the array:
```ts
componentObject.hashes = [...hashes];
```
```suggestion
// Add hashes if provided (CycloneDX 1.4 format).
if (hashes && hashes.length > 0) {
// Clone the hashes array (and its elements) to avoid shared mutable state
componentObject.hashes = hashes.map(h => ({
alg: h.alg,
content: h.content,
}));
}
```
</issue_to_address>
### Comment 2
<location path="src/providers/base_pyproject.js" line_range="18-19" />
<code_context>
const DEFAULT_ROOT_VERSION = '0.0.0'
-/** @typedef {{name: string, version: string, children: string[]}} GraphEntry */
+/** @typedef {{name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}} GraphEntry */
/** @typedef {{name: string, version: string, dependencies: DepTreeEntry[]}} DepTreeEntry */
/** @typedef {{directDeps: string[], graph: Map<string, GraphEntry>}} DependencyData */
/** @typedef {{ecosystem: string, content: string, contentType: string}} Provided */
</code_context>
<issue_to_address>
**suggestion:** The `hashes` shape typedef is duplicated across providers and could drift over time.
`hashes?: Array<{alg: string, content: string}>` is now repeated across `GraphEntry`, `DependencyEntry`, etc. Consider extracting a shared `HashEntry` typedef and referencing it in these types to keep them consistent as the structure evolves.
Suggested implementation:
```javascript
/** @typedef {{alg: string, content: string}} HashEntry */
/** @typedef {{name: string, version: string, children: string[], hashes?: HashEntry[]}} GraphEntry */
/** @typedef {{name: string, version: string, dependencies: DepTreeEntry[]}} DepTreeEntry */
```
1. Search this file (and related provider files, if they share typedefs) for other inline uses of `Array<{alg: string, content: string}>` or `{alg: string, content: string}` in typedefs such as `DependencyEntry`, `DependencyNode`, etc.
2. Replace those inline shapes with `HashEntry[]` or `HashEntry` as appropriate, reusing the new shared typedef to keep the structure consistent.
3. If there is a central types file for providers, consider moving `HashEntry` there and importing/reusing it instead of redefining it per file.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
ruromero
left a comment
There was a problem hiding this comment.
Looks good, I'm missing the updated expected python resulting sbom (stack/component) for pip
Verification Report — TC-4330Review Feedback
Ruben's review body ("missing updated expected python resulting sbom"): This is a plumbing task — no provider populates hashes yet, so golden files remain unchanged. The existing pip golden file tests pass identically to clean main. Deterministic Checks
Acceptance Criteria
Test Requirements
Overall Result: WARNAll acceptance criteria pass. One open code change request from @ruromero ( This comment was AI-generated by sdlc-workflow/verify-pr v0.5.11. |
…lity Replace bare `undefined` scope arguments with a named `NO_SCOPE` constant to make the intent explicit at call sites. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
hashesparameter togetComponent()andaddDependency()in the CycloneDX SBOM modelSbomwrapper,base_pyprojectgraph structure,python_controllerDependencyEntry, andpython_pipSBOM creation paths to forward hash dataImplements TC-4330
Test plan
🤖 Generated with Claude Code
Summary by Sourcery
Add optional hash support to CycloneDX SBOM components and propagate hash data through Python dependency providers while keeping existing behavior unchanged when hashes are absent.
New Features:
Enhancements:
Tests: