.. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 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: .. code-block:: python 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: .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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: .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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: .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python 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. Additional Resources -------------------- * `CycloneDX Specification `_ * `SPDX License List `_ * `JSON Schema Validation `_