This plan covers adding CycloneDX 1.7 and SPDX 3.0 support to sbom-tools. These are fundamentally different in scope: CycloneDX 1.7 is an incremental, additive update from 1.6 (no breaking changes, ~2-3 weeks of work), while SPDX 3.0 is a ground-up redesign requiring a new parser, model adaptations, and careful normalization (~6-8 weeks). The existing architecture's format-agnostic canonical model makes both feasible without disrupting the diff/matching/quality/enrichment layers.
CycloneDX 1.7 (published October 2025) is backward-compatible with 1.6. All changes are
additive: new fields, new top-level elements, and some structural relaxations. The existing
serde-based parser handles unknown fields gracefully via Option<T>, so 1.7 documents
already parse today -- they just lose the new fields.
Effort: ~3 days
-
Add "1.7" to
supported_versions()- File:
src/parsers/cyclonedx.rs - Trivial: add
"1.7"to the returned vec
- File:
-
New serde structs for 1.7 fields:
// Top-level citations array struct CdxCitation { bom_ref: Option<String>, pointers: Option<Vec<String>>, // JSON Pointer (RFC 6901) expressions: Option<Vec<String>>, // JSONPath/XPath timestamp: String, // Required attributed_to: Option<String>, // bom-ref to entity process: Option<String>, // bom-ref to formulation note: Option<String>, signature: Option<CdxSignature>, } // Distribution constraints on metadata struct CdxDistributionConstraints { tlp: Option<TlpClassification>, // CLEAR/GREEN/AMBER/AMBER_AND_STRICT/RED } // License expression details (1.7 relaxation) struct CdxExpressionDetail { license_identifier: String, bom_ref: Option<String>, text: Option<CdxAttachment>, url: Option<String>, } -
Extend existing structs:
CycloneDxBom: addcitations: Option<Vec<CdxCitation>>CdxMetadata: adddistribution_constraints: Option<CdxDistributionConstraints>CdxComponent: addis_external: Option<bool>,version_range: Option<String>,patent_assertions: Option<serde_json::Value>(preserve but don't deeply parse)CdxExternalReference: addproperties: Option<Vec<CdxProperty>>
-
License array parsing change:
- 1.7 moves the
oneOffrom array-level to item-level, allowing mixed license+expression entries - Current
deserialize_licensesneeds to handle both 1.6 (array-level oneOf) and 1.7 (item-level oneOf) - Since serde already tries each variant per-item, this may already work; needs testing
- 1.7 moves the
-
New hash algorithms:
- Add
Streebog256andStreebog512variants toHashAlgorithmenum - Already has
Other(String)fallback, but named variants are better for quality scoring
- Add
Effort: ~2 days
-
DocumentMetadata additions:
- Add
distribution_classification: Option<String>(TLP value) - Add
citations_count: usize(how many citations present -- useful for quality scoring)
- Add
-
Component additions:
- Add
is_external: Option<bool>toComponentstruct - Add
version_range: Option<String>toComponentstruct - These are format-agnostic concepts (SPDX 3.0 has similar
supportLevelon Artifact)
- Add
-
FormatExtensions:
- Store full
citationsarray inFormatExtensions::cyclonedxfor lossless round-trip - Store patent assertions similarly (domain-specific, not worth canonical modeling yet)
- Store full
-
HashAlgorithm enum:
- Add
Streebog256,Streebog512variants - Update
Display,from_str, quality scoring hash strength classification
- Add
-
ExternalRefType enum:
- Add
Citation,Patent,PatentAssertion,PatentFamilyvariants
- Add
Effort: ~2 days
-
Quality scoring:
- Provenance category: award bonus points for documents with
citations(data provenance) - Integrity category: recognize Streebog hash algorithms
check_format_specific(): update CycloneDX version warning threshold (< 1.5 instead of < 1.4)
- Provenance category: award bonus points for documents with
-
Compliance updates:
- CRA:
citationsprovides Art.13(3) provenance evidence; check and report - NIST SSDF: citations support PS.1 provenance traceability
- CRA:
-
CBOM awareness (light touch):
- Don't deeply model the crypto property changes (too specialized)
- Store in
ComponentExtensions::rawfor preservation - Quality: detect
component_type == Cryptographicand check for crypto properties presence
Effort: ~2 days
-
Fixture files:
- Create
tests/fixtures/cyclonedx/minimal-1.7.cdx.jsonwith citations, is_external, TLP - Create
tests/fixtures/cyclonedx/cbom-1.7.cdx.jsonwith crypto components - Create
tests/fixtures/cyclonedx/mixed-licenses-1.7.cdx.jsontesting the license relaxation
- Create
-
Unit tests:
- Parse 1.7 document, verify citations extracted
- Parse 1.7 document, verify is_external flag on components
- Parse 1.7 document, verify TLP distribution constraint
- Verify 1.4/1.5/1.6 documents still parse identically (regression)
- Cross-format diff: 1.7 CycloneDX vs SPDX 2.3
-
Integration tests:
- Diff between 1.6 and 1.7 documents (same components, different format versions)
- Quality scoring of 1.7 document with citations
- Compliance checking with CRA standard against 1.7 document
SPDX 3.0 is not an incremental update from 2.3 -- it is a completely new data model. The flat document structure is replaced by a graph of typed Elements. Relationships are first-class objects. Vulnerabilities are native (Security profile). Licensing uses relationships instead of inline properties. The serialization is JSON-LD.
The good news: our canonical model (NormalizedSbom) already abstracts away format
differences, so the diff/matching/quality/enrichment layers need minimal changes. The
challenge is entirely in the parser and normalization layer.
Effort: ~3 days
-
JSON-LD strategy decision:
- Option A: Use a JSON-LD library (
json-ldcrate) for full spec compliance - Option B: Treat SPDX 3.0 JSON-LD as "JSON with a
@contextfield" and parse with serde - Recommendation: Option B -- SPDX 3.0 JSON-LD is relatively flat in practice;
full JSON-LD processing (context resolution, graph expansion) is overkill for parsing.
The
@contextURL just maps short names to RDF URIs. Real-world SPDX 3.0 files use the compact JSON-LD form which is structurally regular JSON.
- Option A: Use a JSON-LD library (
-
Profile scope decision:
- Must support: Core (mandatory) + Software (packages/files) + SimpleLicensing
- Should support: Security (vulnerabilities/VEX -- aligns with existing enrichment model)
- Defer: AI, Dataset, Build, Lite, Extension, ExpandedLicensing (can be added incrementally)
-
Relationship mapping design:
- SPDX 3.0 has ~80 relationship types vs our 21
DependencyTypevariants - Strategy: map known types to existing enum variants, use
Other(String)for the rest - Key new mappings needed:
affects,fixedIn,foundBy,hasAssessmentFor,hasConcludedLicense,hasDeclaredLicense
- SPDX 3.0 has ~80 relationship types vs our 21
Effort: ~8 days
File: src/parsers/spdx3.rs (new file, separate from existing spdx.rs)
Rationale for separate parser: The data models are so different that trying to share code between 2.x and 3.0 parsers would create a maintenance nightmare. Better to have clean separation with shared normalization logic.
-
Serde models for SPDX 3.0 JSON-LD:
// Top-level document struct Spdx3Document { context: serde_json::Value, // @context (JSON-LD, ignored for parsing) spdx_id: String, // URI type_: String, // "SpdxDocument" name: Option<String>, creation_info: Spdx3CreationInfo, data_license: Option<String>, // Always CC0-1.0 namespace_map: Option<Vec<Spdx3NamespaceMap>>, imports: Option<Vec<Spdx3ExternalMap>>, root_element: Option<Vec<String>>, // URIs of root elements element: Option<Vec<Spdx3Element>>, // All elements in the document profile_conformance: Option<Vec<String>>, } // Polymorphic element (uses serde tag) #[serde(tag = "type")] enum Spdx3Element { Package(Spdx3Package), File(Spdx3File), Snippet(Spdx3Snippet), Relationship(Spdx3Relationship), Vulnerability(Spdx3Vulnerability), Annotation(Spdx3Annotation), Person(Spdx3Agent), Organization(Spdx3Agent), Tool(Spdx3Agent), SoftwareAgent(Spdx3Agent), Bom(Spdx3Bom), Bundle(Spdx3Bundle), // Fallback for unknown element types #[serde(other)] Unknown, } struct Spdx3Package { spdx_id: String, name: Option<String>, package_version: Option<String>, package_url: Option<String>, // First-class PURL download_location: Option<String>, home_page: Option<String>, copyright_text: Option<String>, description: Option<String>, supplied_by: Option<Vec<String>>, // URIs to Agent elements originated_by: Option<Vec<String>>, verified_using: Option<Vec<Spdx3IntegrityMethod>>, external_identifier: Option<Vec<Spdx3ExternalIdentifier>>, external_ref: Option<Vec<Spdx3ExternalRef>>, primary_purpose: Option<String>, additional_purpose: Option<Vec<String>>, content_identifier: Option<String>, attribution_text: Option<Vec<String>>, support_level: Option<String>, valid_until_time: Option<String>, built_time: Option<String>, release_time: Option<String>, creation_info: Option<Spdx3CreationInfo>, } struct Spdx3Relationship { spdx_id: String, from: String, // URI to: Vec<String>, // URIs (1..*) relationship_type: String, start_time: Option<String>, end_time: Option<String>, completeness: Option<String>, creation_info: Option<Spdx3CreationInfo>, } struct Spdx3Vulnerability { spdx_id: String, name: Option<String>, description: Option<String>, published_time: Option<String>, modified_time: Option<String>, withdrawn_time: Option<String>, external_identifier: Option<Vec<Spdx3ExternalIdentifier>>, external_ref: Option<Vec<Spdx3ExternalRef>>, } -
Element resolution pipeline:
- First pass: deserialize all elements into typed structs
- Build element index:
HashMap<String, &Spdx3Element>(spdx_id URI -> element) - Resolve Agent references: look up
supplied_by/originated_byURIs in the index - Resolve Relationship targets: look up
from/toURIs in the index
-
License extraction via relationships:
- Find all Relationship elements where
relationship_typeishasDeclaredLicenseorhasConcludedLicense - The
totarget is a license element (LicenseExpression or CustomLicense) - Map to existing
LicenseInfomodel
- Find all Relationship elements where
-
Vulnerability extraction (Security profile):
- Find Vulnerability elements
- Find VulnAssessmentRelationship elements (CVSS, VEX, EPSS, etc.)
- Map assessment relationships to
VulnerabilityRefwith scoring data - Map VEX relationships to
VexStatus
Effort: ~2 days
-
Detection for SPDX 3.0 vs 2.x:
- SPDX 3.0 JSON-LD has
@contextfield and"type": "SpdxDocument" - SPDX 2.x has
spdxVersion: "SPDX-2.x"at top level - Detection confidence:
@contextcontaining "spdx.org/rdf/3" → CERTAIN for SPDX 3.0"type": "SpdxDocument"→ HIGH for SPDX 3.0- Presence of
spdxVersionstarting with "SPDX-2" → dispatch to existing 2.x parser
- File:
src/parsers/detection.rs-- add SPDX 3.0 detection branch
- SPDX 3.0 JSON-LD has
-
Parser dispatch:
ParserKindenum: addSpdx3variant (or detect version in Spdx parser and dispatch)- Recommended: Keep
ParserKind::Spdxbut have the SPDX detection logic route toSpdx3ParservsSpdxParserbased on detected version
-
Streaming parser:
src/parsers/streaming.rsneeds SPDX 3.0 format detection in peek buffer- Look for
@contextin first 4KB
Effort: ~5 days
This is the critical translation layer. SPDX 3.0's graph model must map to our flat
NormalizedSbom with components HashMap + edges Vec.
-
Element-to-Component mapping:
Spdx3Package→Component(primary mapping)Spdx3File→Componentwithcomponent_type: FileSpdx3Snippet→ skip or store in extensions (too granular for SBOM diffing)- Agent elements →
Creator/Organization(not components) - Vulnerability elements →
VulnerabilityRef(attached to affected components)
-
Identifier mapping:
package_url(first-class in 3.0) →identifiers.purlexternal_identifierwith typecpe23→identifiers.cpeexternal_identifierwith typeswid→identifiers.swidspdx_idURI →identifiers.format_idCanonicalIduses existing tiered fallback (PURL > CPE > SWID > synthetic)
-
Relationship-to-Edge mapping:
- Relationship elements with dependency-related types →
DependencyEdge - License relationships → populate
Component.licenseson thefromcomponent - Vulnerability assessment relationships → populate
Component.vulnerabilities rootElement→primary_component_id
- Relationship elements with dependency-related types →
-
CreationInfo handling:
- Per-element CreationInfo in 3.0 vs document-level in
DocumentMetadata - Strategy: Use SpdxDocument's CreationInfo for
DocumentMetadata.created/creators - Store per-element creation info in
ComponentExtensions.annotations(preserves provenance)
- Per-element CreationInfo in 3.0 vs document-level in
-
Profile-aware parsing:
- Check
profile_conformanceto know which element types to expect - If Security profile present, parse Vulnerability + VulnAssessment elements
- If only Core + Software, skip vulnerability elements gracefully
- Check
-
Handling
NoneElementandNoAssertionElement:- These are SPDX 3.0 sentinel values meaning "explicitly none" vs "unknown"
- Map to appropriate
Nonevs default values in the canonical model
Effort: ~3 days
-
DependencyType enum -- new variants:
// SPDX 3.0 relationship types that don't map to existing variants Affects, // Vulnerability → Package FixedIn, // Vulnerability → Package@version FoundBy, // Vulnerability → Agent HasAssessmentFor, // VulnAssessment → Vulnerability HasConcludedLicense, // Package → License HasDeclaredLicense, // Package → License AmendedBy, // Element → Element (modification) ConfiguredBy, // Package → configuration CoordinatedBy, // Vulnerability → Agent ExpandedFromArchive, // File → Package (extraction) FileAdded, // already exists FileDeleted, // already exists HasPrerequisite, // already mapped via SPDX 2.3 InvokedBy, // Tool → Agent PackagedBy, // Package → Agent ReportedBy, // Vulnerability → Agent TestedOn, // Tool → Package TrainedOn, // AI model → Dataset UnderInvestigationFor, // VEX → Vulnerability UsesTool, // Agent → Tool- Most of these won't affect diff/matching (they're vulnerability/agent relationships)
- Only dependency-related types flow through the diff engine
-
SbomFormat considerations:
- Keep
SbomFormat::Spdxfor both 2.x and 3.0 (same standard family) - Distinguish via
format_version("2.3" vs "3.0.1") - This preserves cross-version diffing without special-casing the format enum
- Keep
-
DocumentMetadata additions:
- Add
profile_conformance: Vec<String>(which SPDX 3.0 profiles the document uses) - Add
root_elements: Vec<CanonicalId>(SPDX 3.0 can have multiple root elements)
- Add
-
FormatExtensions:
- Store the full
@contextinFormatExtensions::spdxfor round-trip capability - Store unrecognized element types for lossless preservation
- Store the full
Effort: ~2 days
-
Quality scoring adjustments:
- Identifiers: SPDX 3.0 has richer
ExternalIdentifierwith typed identifiers; award higher scores for documents with typed CPE/PURL/SWHID identifiers - Completeness: check
Relationship.completenessfield - VulnDocs: if Security profile present, score native vulnerability data
- Provenance: per-element CreationInfo provides better provenance; score accordingly
- Identifiers: SPDX 3.0 has richer
-
Compliance updates:
check_format_specific(): add SPDX 3.0 version detection- NTIA: SPDX 3.0 satisfies all NTIA minimum elements natively (supplier as typed Agent)
- CRA: SPDX 3.0's Security profile provides Art.13(9) vulnerability statements
- EO 14028: SPDX 3.0 format itself satisfies the "machine-readable" requirement
-
Report generation:
- Format version display: show "SPDX 3.0.1" in reports
- Profile conformance display in summary reports
Effort: ~4 days
-
Fixture files:
tests/fixtures/spdx3/minimal.spdx3.json-- Core + Software onlytests/fixtures/spdx3/with-vulnerabilities.spdx3.json-- Core + Software + Securitytests/fixtures/spdx3/with-licenses.spdx3.json-- License relationshipstests/fixtures/spdx3/multi-root.spdx3.json-- Multiple root elements- Source: Generate from SPDX 3.0 examples at https://github.com/spdx/spdx-3-model or create manually following the spec
-
Unit tests (parser):
- Parse minimal SPDX 3.0 document
- Verify element type dispatch (Package, File, Relationship, Vulnerability)
- Verify license extraction via relationship traversal
- Verify vulnerability mapping from Security profile
- Verify Agent resolution for supplier/originator
- Verify per-element CreationInfo preservation
- Verify
@contextdetection and version routing
-
Integration tests:
- Cross-format diff: SPDX 3.0 vs CycloneDX 1.7 (the key use case)
- Cross-version diff: SPDX 2.3 vs SPDX 3.0 (same components, different formats)
- Quality scoring of SPDX 3.0 document
- Pipeline: parse → enrich → diff → report with SPDX 3.0 input
-
Regression tests:
- All existing SPDX 2.2/2.3 tests must continue passing
- All existing CycloneDX 1.4-1.6 tests must continue passing
- Cross-format diff between SPDX 2.3 and CycloneDX 1.6 unchanged
Effort: ~2 days (mostly testing)
The canonical model already enables this. Both formats normalize to NormalizedSbom,
and the diff engine is format-agnostic. The work here is:
-
Verify CanonicalId stability across formats:
- Same PURL in CycloneDX 1.7 and SPDX 3.0 must produce same CanonicalId
- SPDX 3.0's
packageUrl(first-class field) should match CycloneDX'spurlfield - Test with real-world PURLs (npm, pypi, maven, cargo)
-
Verify relationship equivalence:
- CycloneDX
dependsOn↔ SPDX 3.0DEPENDS_ONrelationship type - Both should produce identical
DependencyEdgerecords
- CycloneDX
-
Handle metadata differences:
- CycloneDX 1.7
citationshas no SPDX 3.0 equivalent (format-specific) - SPDX 3.0
profileConformancehas no CycloneDX equivalent (format-specific) - These should appear in diff as metadata differences, not false positives
- CycloneDX 1.7
-
Parser coexistence:
- Existing
SpdxParserhandles 2.2/2.3 (unchanged) - New
Spdx3Parserhandles 3.0+ (separate code path) CycloneDxParserhandles 1.4-1.7 (extended, not replaced)- Format detection routes to correct parser automatically
- Existing
-
Model backward compatibility:
- All new fields on
Component,DocumentMetadataareOption<T>(default None) - New enum variants are behind
#[non_exhaustive](already in place) - Serialized output format unchanged for existing fields
- All new fields on
-
CLI backward compatibility:
- No new CLI flags needed (format detection is automatic)
- Existing
--formathints should work with new versions - Reports should display correct format version strings
- Update README.md format support table
- Update ARCHITECTURE.md with SPDX 3.0 parser architecture notes
- Update CLI help text for
--formatflag
- Phase 1.1: Parser updates
- Phase 1.2: Model extensions
- Phase 1.3: Quality/compliance updates
- Phase 1.4: Testing
- Ship as PR #1 -- low risk, high immediate value
- Phase 2.1: Research & design decisions
- Phase 2.2: New parser (spdx3.rs)
- Phase 2.3: Format detection updates
- Ship as PR #2 -- parser + detection only, no model changes yet
- Phase 2.4: Normalization pipeline
- Phase 2.5: Model extensions
- Phase 2.6: Quality/compliance
- Phase 2.7: Testing
- Phase 3.1: Cross-format diffing verification
- Ship as PR #3 -- full integration with tests
- Phase 3.2: Backward compatibility verification
- Phase 3.3: Documentation
- Cross-version regression testing
- Ship as PR #4 -- docs + final verification
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| SPDX 3.0 JSON-LD has edge cases that break serde parsing | Medium | High | Test against official SPDX 3.0 examples; fall back to json-ld crate if needed |
| Real-world SPDX 3.0 files are rare (low adoption) | High | Low | Create comprehensive fixtures; monitor community adoption |
| SPDX 3.0 spec is still evolving (3.0.1 published) | Medium | Medium | Pin to 3.0.1; design parser to handle minor version bumps |
| CycloneDX 1.7 license structure change breaks existing parsing | Low | Medium | Custom deserializer already handles variant formats; add tests |
| New DependencyType variants break exhaustive matches downstream | Low | Low | Already using #[non_exhaustive] + Other(String) fallback |
| Performance regression with SPDX 3.0 element graph resolution | Low | Medium | Single-pass element indexing; benchmark against large documents |
- Existing
serde,serde_json,quick-xmlare sufficient
- None required if using Option B (serde-based JSON-LD parsing)
- Consider
json-ldcrate only if real-world files use advanced JSON-LD features (context imports, graph expansion, blank nodes) -- unlikely for SPDX 3.0 in practice
- Currently used only for SPDX license expression parsing, NOT for document parsing
- Does not need updating for SPDX 3.0 document support (license expressions unchanged)
- May need update if ExpandedLicensing profile support is added later
- Don't merge SPDX 2.x and 3.0 parsers -- the data models are too different; shared code would be a maintenance burden
- Don't add full JSON-LD processing -- real-world SPDX 3.0 files are compact JSON-LD that parses as regular JSON; full graph expansion is unnecessary
- Don't deeply model CycloneDX CBOM changes -- crypto property restructuring is highly specialized; store in extensions and parse on demand
- Don't deeply model SPDX 3.0 AI/Dataset/Build profiles -- defer until there's user demand; the Core + Software + Security profiles cover 95% of use cases
- Don't change the canonical model fundamentally -- the flat
HashMap<CanonicalId, Component>works well; SPDX 3.0's graph model flattens naturally into components + edges during normalization - Don't break the
spdxcrate dependency -- it's used for license expression parsing only, which is unchanged in SPDX 3.0