SBOM Refactoring: Clean Architecture for SPDX and CycloneDX
Overview
This document describes the refactored SBOM architecture that provides clean separation of concerns between SPDX and CycloneDX formats, making the codebase more maintainable, extensible, and testable.
Problem Statement
The original SBOM implementation had several issues:
- Monolithic Design:
SBOMGenerator.cpp
was a massive 2315-line file handling both SPDX and CycloneDX - Code Duplication: Similar logic repeated across different SPDX versions (2.3, 3.0.0, 3.0.1)
- Tight Coupling: Format-specific logic mixed with general SBOM generation
- Hard to Maintain: Large files made it difficult to modify individual formats
- Inconsistent Interfaces: Different approaches for different formats
Solution: Clean Architecture
1. Interface-Based Design
The new architecture uses clean interfaces to separate concerns:
// Abstract base for all SBOM format handlers
class ISBOMFormatHandler {
virtual std::string generateSBOM(...) = 0;
virtual ValidationResult validateContent(...) = 0;
virtual std::string getFormatName() const = 0;
// ...
};
// SPDX-specific interface
class ISPDXHandler : public ISBOMFormatHandler {
virtual void setVersion(const std::string& version) = 0;
virtual std::string generateComponentEntry(...) = 0;
// ...
};
// CycloneDX-specific interface
class ICycloneDXHandler : public ISBOMFormatHandler {
virtual void setVersion(const std::string& version) = 0;
virtual std::string generateComponentEntry(...) = 0;
// ...
};
2. Modular Format Handlers
Each format and version has its own dedicated handler:
SPDX Handlers
SPDX2_3Handler
- Handles SPDX 2.3 tag-value formatSPDX3_0_0Handler
- Handles SPDX 3.0.0 JSON formatSPDX3_0_1Handler
- Handles SPDX 3.0.1 JSON format
CycloneDX Handlers
CycloneDX1_4Handler
- Handles CycloneDX 1.4 formatCycloneDX1_5Handler
- Handles CycloneDX 1.5 formatCycloneDX1_6Handler
- Handles CycloneDX 1.6 format
3. Factory Pattern
A factory class creates the appropriate handler based on format and version:
class SBOMFormatFactory {
static std::unique_ptr<ISPDXHandler> createSPDXHandler(const std::string& version);
static std::unique_ptr<ICycloneDXHandler> createCycloneDXHandler(const std::string& version);
static std::unique_ptr<ISBOMFormatHandler> createHandler(const std::string& format, const std::string& version);
};
4. Clean SBOM Generator
The new SBOMGeneratorV2
is much simpler and focused:
class SBOMGeneratorV2 {
void processComponent(const ComponentInfo& component);
bool generateSBOM();
std::string generateSBOMContent();
void setFormat(const std::string& format);
// ...
};
File Structure
src/common/
├── SBOMFormats.hpp # Interface definitions
├── SBOMFormatFactory.cpp # Factory implementation
├── SPDXHandler.hpp # SPDX handler interfaces
├── SPDX2_3Handler.cpp # SPDX 2.3 implementation
├── SPDX3_0_0Handler.cpp # SPDX 3.0.0 implementation
├── SPDX3_0_1Handler.cpp # SPDX 3.0.1 implementation
├── CycloneDXHandler.hpp # CycloneDX handler interfaces
├── CycloneDX1_4Handler.cpp # CycloneDX 1.4 implementation
├── CycloneDX1_5Handler.cpp # CycloneDX 1.5 implementation
├── CycloneDX1_6Handler.cpp # CycloneDX 1.6 implementation
├── SBOMGeneratorV2.hpp # Clean SBOM generator interface
└── SBOMGeneratorV2.cpp # Clean SBOM generator implementation
Benefits
1. Maintainability
- Each format handler is focused on a single responsibility
- Changes to one format don't affect others
- Easier to understand and modify individual components
2. Extensibility
- Adding new formats requires implementing the interface
- Adding new versions requires creating a new handler class
- No changes needed to the main generator
3. Testability
- Each handler can be tested independently
- Mock handlers can be easily created for testing
- Unit tests are more focused and reliable
4. Code Reuse
- Common functionality shared through base classes
- Utilities can be reused across different formats
- Consistent interfaces across all handlers
5. Performance
- Only the required handler is loaded
- No unnecessary code execution
- Better memory usage
Usage Examples
Basic Usage
// Create generator
heimdall::SBOMGeneratorV2 generator;
// Set format and version
generator.setFormat("spdx");
generator.setSPDXVersion("2.3");
// Add components
ComponentInfo component("myapp", "/path/to/myapp");
generator.processComponent(component);
// Generate SBOM
generator.setOutputPath("myapp.spdx");
generator.generateSBOM();
Advanced Usage
// Create specific handler directly
auto spdxHandler = heimdall::SBOMFormatFactory::createSPDXHandler("3.0.1");
// Generate content without file I/O
std::string content = spdxHandler->generateSBOM(components, metadata);
// Validate content
auto result = spdxHandler->validateContent(content);
if (!result.isValid) {
for (const auto& error : result.errors) {
std::cerr << "Error: " << error << std::endl;
}
}
Adding New Formats
// 1. Implement the interface
class MyCustomHandler : public heimdall::ISBOMFormatHandler {
std::string generateSBOM(...) override { /* implementation */ }
ValidationResult validateContent(...) override { /* implementation */ }
// ... other required methods
};
// 2. Add to factory
std::unique_ptr<ISBOMFormatHandler> SBOMFormatFactory::createHandler(const std::string& format) {
if (format == "mycustom") {
return std::make_unique<MyCustomHandler>();
}
// ... existing code
}
Migration Strategy
Phase 1: Parallel Implementation
- Implement new architecture alongside existing code
- Add feature flags to switch between implementations
- Maintain backward compatibility
Phase 2: Gradual Migration
- Update plugins to use new
SBOMGeneratorV2
- Migrate tools one by one
- Update tests to use new architecture
Phase 3: Cleanup
- Remove old
SBOMGenerator.cpp
- Remove feature flags
- Update documentation
Testing Strategy
Unit Tests
- Test each handler independently
- Mock dependencies for isolated testing
- Test format-specific edge cases
Integration Tests
- Test factory creation
- Test end-to-end SBOM generation
- Test format validation
Compatibility Tests
- Ensure new implementation produces identical output
- Test backward compatibility
- Validate against official schemas
Future Enhancements
1. Plugin System
- Allow external format handlers
- Dynamic loading of format plugins
- Third-party format support
2. Performance Optimizations
- Lazy loading of handlers
- Caching of generated content
- Parallel processing of components
3. Enhanced Validation
- Schema-based validation
- Custom validation rules
- Validation plugins
4. Format Conversion
- Convert between formats
- Lossless transformations
- Format comparison tools
Conclusion
The refactored SBOM architecture provides a clean, maintainable, and extensible foundation for handling multiple SBOM formats. The separation of concerns makes the code easier to understand, test, and modify, while the interface-based design allows for easy extension and customization.
This architecture positions Heimdall for future growth and ensures that adding new SBOM formats or versions will be straightforward and won't impact existing functionality.