Validating CycloneDX SBOMs
Overview
The CycloneDX Python library provides robust validation capabilities to ensure your Software Bill of Materials (SBOM) documents conform to the CycloneDX specification. This guide demonstrates how to validate SBOMs effectively in various scenarios, from simple validation checks to production API integrations.
Why Validate SBOMs?
Validation ensures that:
Your SBOM conforms to the CycloneDX schema specification
All required fields are present and correctly formatted
Data types and structures match the specification
The SBOM can be reliably consumed by other tools and systems
Basic Validation
Validating JSON SBOMs
The most common use case is validating a JSON-formatted SBOM:
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
import json
# Create a validator for CycloneDX 1.5
validator = JsonValidator(SchemaVersion.V1_5)
# Load your SBOM
with open('sbom.json', 'r') as f:
sbom_data = f.read()
# Validate the SBOM
validation_error = validator.validate_str(sbom_data)
if validation_error:
print(f"❌ Validation failed!")
print(f"Error: {validation_error}")
else:
print("✅ SBOM is valid!")
Validating from Dictionary
If you already have your SBOM as a Python dictionary:
import json
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
sbom_dict = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"component": {
"type": "application",
"name": "my-app",
"version": "1.0.0"
}
},
"components": []
}
validator = JsonValidator(SchemaVersion.V1_5)
validation_error = validator.validate_str(json.dumps(sbom_dict))
if not validation_error:
print("✅ SBOM is valid!")
Understanding Validation Errors
When validation fails, the library provides detailed error information to help you identify and fix issues.
Accessing Error Details
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
import json
validator = JsonValidator(SchemaVersion.V1_5)
# Invalid SBOM (missing required fields)
invalid_sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
# Missing 'version' field (required)
}
validation_error = validator.validate_str(json.dumps(invalid_sbom))
if validation_error:
# Access the validation error details
print(f"Error message: {validation_error.message}")
print(f"Invalid data: {validation_error.data.instance}")
print(f"JSON path: {validation_error.data.json_path}")
Error Object Structure
The ValidationError object provides:
message: Human-readable error description
data.instance: The actual invalid data that caused the error
data.json_path: JSONPath to the location of the error in the document
Detailed Error Logging Example
import logging
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
import json
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def validate_with_logging(sbom_dict: dict, schema_version: SchemaVersion) -> bool:
"""Validate SBOM with detailed error logging."""
validator = JsonValidator(schema_version)
validation_error = validator.validate_str(json.dumps(sbom_dict))
if validation_error:
logger.error("SBOM validation failed")
logger.error(f"Location: {validation_error.data.json_path}")
logger.error(f"Invalid data: {validation_error.data.instance}")
logger.error(f"Message: {validation_error.message}")
return False
logger.info("SBOM validation successful")
return True
# Usage
sbom = {"bomFormat": "CycloneDX", "specVersion": "1.5", "version": 1}
is_valid = validate_with_logging(sbom, SchemaVersion.V1_5)
Multi-Version Support
The CycloneDX specification has multiple versions. Your application should handle different versions gracefully.
Dynamic Version Detection
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
import json
def validate_sbom_any_version(sbom_dict: dict) -> tuple[bool, str | None]:
"""
Validate SBOM with automatic version detection.
Args:
sbom_dict: SBOM as a dictionary
Returns:
Tuple of (is_valid, error_message)
"""
# Map spec versions to SchemaVersion enums
version_map = {
"1.2": SchemaVersion.V1_2,
"1.3": SchemaVersion.V1_3,
"1.4": SchemaVersion.V1_4,
"1.5": SchemaVersion.V1_5,
"1.6": SchemaVersion.V1_6,
}
# Get the spec version from SBOM
spec_version = sbom_dict.get("specVersion")
if not spec_version:
return False, "Missing 'specVersion' field"
if spec_version not in version_map:
return False, f"Unsupported CycloneDX version: {spec_version}"
# Validate with the appropriate schema version
validator = JsonValidator(version_map[spec_version])
validation_error = validator.validate_str(json.dumps(sbom_dict))
if validation_error:
error_msg = f"Validation failed at {validation_error.data.json_path}: {validation_error.message}"
return False, error_msg
return True, None
# Usage
sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.4", # Will automatically use V1_4 validator
"version": 1
}
is_valid, error = validate_sbom_any_version(sbom)
if is_valid:
print("✅ SBOM is valid!")
else:
print(f"❌ Validation failed: {error}")
Validating XML SBOMs
CycloneDX also supports XML format. The validation process is similar to JSON:
from cyclonedx.validation.xml import XmlValidator
from cyclonedx.schema import SchemaVersion
# Create XML validator
validator = XmlValidator(SchemaVersion.V1_5)
# Load XML SBOM
with open('sbom.xml', 'r') as f:
xml_data = f.read()
# Validate
validation_error = validator.validate_str(xml_data)
if validation_error:
print(f"❌ XML validation failed: {validation_error}")
else:
print("✅ XML SBOM is valid!")
Auto-detecting Format
from cyclonedx.validation.json import JsonValidator
from cyclonedx.validation.xml import XmlValidator
from cyclonedx.schema import SchemaVersion
def validate_sbom_file(file_path: str, schema_version: SchemaVersion = SchemaVersion.V1_5) -> bool:
"""Validate SBOM file (auto-detect JSON or XML)."""
with open(file_path, 'r') as f:
content = f.read()
# Determine format by file extension or content
if file_path.endswith('.xml'):
validator = XmlValidator(schema_version)
else: # Assume JSON
validator = JsonValidator(schema_version)
validation_error = validator.validate_str(content)
return validation_error is None
# Usage
is_valid_json = validate_sbom_file('sbom.json')
is_valid_xml = validate_sbom_file('sbom.xml')
Production Integration Examples
FastAPI Integration
Here’s how to integrate validation into a FastAPI application:
from fastapi import FastAPI, HTTPException, UploadFile, File, status
from cyclonedx.validation.json import JsonValidator
from cyclonedx.schema import SchemaVersion
import json
from typing import Any
app = FastAPI()
def validate_sbom_schema(sbom_data: dict[str, Any]) -> None:
"""
Validate CycloneDX SBOM schema.
Args:
sbom_data: SBOM as dictionary
Raises:
HTTPException: If validation fails
"""
# Map of supported versions
schema_version_map = {
"1.4": SchemaVersion.V1_4,
"1.5": SchemaVersion.V1_5,
"1.6": SchemaVersion.V1_6,
}
# Get spec version
spec_version = sbom_data.get("specVersion")
# Check if version is supported
if spec_version not in schema_version_map:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported CycloneDX schema version: {spec_version}"
)
# Validate against schema
try:
validator = JsonValidator(schema_version_map[spec_version])
validation_error = validator.validate_str(json.dumps(sbom_data))
if validation_error:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"message": "Invalid CycloneDX SBOM",
"location": validation_error.data.json_path,
"invalid_data": str(validation_error.data.instance)[:200]
}
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Validation error: {str(e)}"
)
@app.post("/validate-sbom")
async def validate_sbom_endpoint(file: UploadFile = File(...)):
"""Endpoint to validate uploaded SBOM."""
try:
# Read and parse JSON
content = await file.read()
sbom_data = json.loads(content)
# Validate schema
validate_sbom_schema(sbom_data)
return {
"status": "valid",
"message": "SBOM is valid",
"version": sbom_data.get("specVersion")
}
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid JSON format"
)
Best Practices
1. Always Validate Before Processing
def process_sbom(sbom_data: dict) -> None:
"""Process SBOM only after validation."""
# Validate first
validator = JsonValidator(SchemaVersion.V1_5)
validation_error = validator.validate_str(json.dumps(sbom_data))
if validation_error:
raise ValueError(f"Invalid SBOM: {validation_error.message}")
# Now safe to process
components = sbom_data.get("components", [])
# ... process components
2. Use Appropriate Error Codes
In APIs, use standard HTTP status codes:
400 Bad Request: Invalid JSON format, unsupported version
422 Unprocessable Entity: Valid JSON but invalid CycloneDX schema
500 Internal Server Error: Unexpected validation errors
3. Log Validation Errors with Context
import logging
logger = logging.getLogger(__name__)
def validate_with_logging(sbom_data: dict, context: dict) -> bool:
"""Validate with contextual logging."""
validator = JsonValidator(SchemaVersion.V1_5)
validation_error = validator.validate_str(json.dumps(sbom_data))
if validation_error:
logger.error(
"SBOM validation failed",
extra={
"context": context,
"error_location": validation_error.data.json_path,
"error_message": validation_error.message,
"spec_version": sbom_data.get("specVersion")
}
)
return False
logger.info("SBOM validation successful", extra={"context": context})
return True
Summary
Key takeaways for SBOM validation:
✅ Always validate SBOMs before processing
✅ Handle errors gracefully with detailed logging
✅ Support multiple versions with dynamic detection
✅ Use appropriate error codes in APIs (400, 422, 500)
✅ Provide helpful error messages to users
✅ Test validation logic thoroughly
✅ Cache validators for better performance
The CycloneDX Python library provides robust validation capabilities that can be integrated into various applications, from simple scripts to production APIs.