From b55d436078314c3827374ee770b104623726b332 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Thu, 29 Jan 2026 18:16:07 -0500 Subject: [PATCH 01/13] docs: Add comprehensive SBOM validation guide - Add new validation.rst documentation covering validation workflows - Include practical examples for JSON and XML validation - Document error handling patterns with ValidationError inspection - Add multi-version support examples (CycloneDX 1.2-1.6) - Provide production integration examples (FastAPI) - Include best practices for validation in production systems - Add validation.rst to documentation index Addresses #708 Signed-off-by: Saquib Saifee Signed-off-by: Saquib Saifee --- docs/index.rst | 1 + docs/validation.rst | 453 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 docs/validation.rst diff --git a/docs/index.rst b/docs/index.rst index 729103101..c4236bcc2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,6 +45,7 @@ If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-o install architecture examples + validation contributing support changelog diff --git a/docs/validation.rst b/docs/validation.rst new file mode 100644 index 000000000..1cc31c8d7 --- /dev/null +++ b/docs/validation.rst @@ -0,0 +1,453 @@ +.. # 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 `_ \ No newline at end of file From c9081d4ea3ee83a7da86fb8a3a6ad66c5b583f7a Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 8 Feb 2026 16:48:22 -0500 Subject: [PATCH 02/13] Update docs/validation.rst Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- docs/validation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/validation.rst b/docs/validation.rst index 1cc31c8d7..75853dcf8 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -12,7 +12,7 @@ # # SPDX-License-Identifier: Apache-2.0 -Validating CycloneDX SBOMs +Validating =========================== Overview From e52401f2e9e7ef2f7ef4745a84745aa07dacb0f2 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 8 Feb 2026 16:48:43 -0500 Subject: [PATCH 03/13] Update docs/validation.rst Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- docs/validation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/validation.rst b/docs/validation.rst index 75853dcf8..5ecd2203a 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -25,7 +25,7 @@ Why Validate SBOMs? Validation ensures that: -* Your SBOM conforms to the CycloneDX schema specification +* Your JSON/XML 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 From 1061440bdbecc30f7862a0040b37448da13be060 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 8 Feb 2026 16:49:05 -0500 Subject: [PATCH 04/13] Update docs/validation.rst Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- docs/validation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/validation.rst b/docs/validation.rst index 5ecd2203a..de380824d 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -28,7 +28,7 @@ Validation ensures that: * Your JSON/XML 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 +* The JSON/XML can be reliably consumed by other tools and systems Basic Validation ---------------- From 87749c23d84bde0efa053c8679e64b132a1fff31 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 8 Feb 2026 17:41:28 -0500 Subject: [PATCH 05/13] docs: use literalinclude for validation examples and improve format detection Signed-off-by: Saquib Saifee --- docs/examples.rst | 8 + docs/validation.rst | 453 --------------------------------- examples/complex_validation.py | 197 ++++++++++++++ 3 files changed, 205 insertions(+), 453 deletions(-) delete mode 100644 docs/validation.rst create mode 100644 examples/complex_validation.py diff --git a/docs/examples.rst b/docs/examples.rst index 2452e7d44..83bc82acd 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -29,3 +29,11 @@ Complex Deserialize .. literalinclude:: ../examples/complex_deserialize.py :language: python :linenos: + + +Complex Validation +------------------ + +.. literalinclude:: ../examples/complex_validation.py + :language: python + :linenos: diff --git a/docs/validation.rst b/docs/validation.rst deleted file mode 100644 index de380824d..000000000 --- a/docs/validation.rst +++ /dev/null @@ -1,453 +0,0 @@ -.. # 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 -=========================== - -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 JSON/XML conforms to the CycloneDX schema specification -* All required fields are present and correctly formatted -* Data types and structures match the specification -* The JSON/XML 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 `_ \ No newline at end of file diff --git a/examples/complex_validation.py b/examples/complex_validation.py new file mode 100644 index 000000000..7444de551 --- /dev/null +++ b/examples/complex_validation.py @@ -0,0 +1,197 @@ +# This file is part of CycloneDX Python Library +# +# 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 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import json +import sys +from typing import TYPE_CHECKING + +from cyclonedx.exception import MissingOptionalDependencyException +from cyclonedx.schema import OutputFormat, SchemaVersion +from cyclonedx.validation import make_schemabased_validator + +if TYPE_CHECKING: + from cyclonedx.validation.json import JsonValidator + from cyclonedx.validation.xml import XmlValidator + +""" +This example demonstrates how to validate CycloneDX SBOMs (both JSON and XML). +Validation is built upon `jsonschema` for JSON and `lxml` for XML. +To use validation, ensure you have installed the library with the validation extra: + pip install cyclonedx-python-lib[validation] + or + pip install cyclonedx-python-lib[json-validation,xml-validation] +""" + +# region Sample SBOMs + +JSON_SBOM = """ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "metadata": { + "component": { + "type": "application", + "name": "my-app", + "version": "1.0.0" + } + }, + "components": [] +} +""" + +XML_SBOM = """ + + + + my-app + 1.0.0 + + + +""" + +INVALID_JSON_SBOM = """ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": { + "component": { + "type": "invalid-type", + "name": "my-app" + } + } +} +""" +# endregion Sample SBOMs + + +# region JSON Validation + +print('--- JSON Validation ---') + +# Create a JSON validator for a specific schema version +json_validator: 'JsonValidator' = make_schemabased_validator(OutputFormat.JSON, SchemaVersion.V1_5) + +try: + # 1. Validate valid SBOM + validation_errors = json_validator.validate_str(JSON_SBOM) + if validation_errors: + print('JSON SBOM is unexpectedly invalid!', file=sys.stderr) + else: + print('JSON SBOM is valid') + + # 2. Validate invalid SBOM and inspect details + print('\nChecking invalid JSON SBOM...') + validation_errors = json_validator.validate_str(INVALID_JSON_SBOM) + if validation_errors: + print('Validation failed as expected.') + print(f'Error Message: {validation_errors.message}') + print(f'JSON Path: {validation_errors.data.json_path}') + print(f'Invalid Data: {validation_errors.data.instance}') +except MissingOptionalDependencyException as error: + print('JSON validation was skipped:', error) + +# endregion JSON Validation + + +print('\n' + '=' * 30 + '\n') + + +# region XML Validation + +print('--- XML Validation ---') + +xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5) + +try: + validation_errors = xml_validator.validate_str(XML_SBOM) + if validation_errors: + print('XML SBOM is invalid!', file=sys.stderr) + else: + print('XML SBOM is valid') +except MissingOptionalDependencyException as error: + print('XML validation was skipped:', error) + +# endregion XML Validation + + +print('\n' + '=' * 30 + '\n') + + +# region Dynamic version detection + +print('--- Dynamic Validation ---') + + +def validate_sbom(raw_data: str) -> bool: + """Validate an SBOM by detecting its format and version.""" + + # 1. Attempt to detect JSON and its version + try: + data = json.loads(raw_data) + output_format = OutputFormat.JSON + spec_version_str = data.get('specVersion') + if not spec_version_str: + print('Error: Missing specVersion in JSON SBOM', file=sys.stderr) + return False + schema_version = SchemaVersion.from_version(spec_version_str) + except (json.JSONDecodeError, ValueError): + # 2. Attempt to detect XML and its version + try: + from lxml import etree + xml_tree = etree.fromstring(raw_data.encode('utf-8')) + output_format = OutputFormat.XML + # Extract version from CycloneDX namespace + schema_version = SchemaVersion.V1_5 # Default + for ns in xml_tree.nsmap.values(): + if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): + try: + schema_version = SchemaVersion.from_version(ns.split('/')[-1]) + break + except ValueError: + pass + except (ImportError, etree.XMLSyntaxError): + print('Error: Unknown or malformed SBOM format', file=sys.stderr) + return False + except Exception as e: + print(f'Error: Format detection failed: {e}', file=sys.stderr) + return False + + # 3. Perform validation using the detected format and version + try: + validator = make_schemabased_validator(output_format, schema_version) + errors = validator.validate_str(raw_data) + + if errors: + print(f'Validation failed for {output_format.name} version {schema_version.to_version()}', file=sys.stderr) + print(f'Reason: {errors}', file=sys.stderr) + return False + + print(f'Successfully validated {output_format.name} SBOM (Version {schema_version.to_version()})') + return True + + except MissingOptionalDependencyException as error: + print(f'Validation skipped due to missing dependencies: {error}') + return False + + +# Execute dynamic validation +validate_sbom(JSON_SBOM) +validate_sbom(XML_SBOM) + +# endregion Dynamic version detection From 0295ccc81b6300bfe96f2048adbd0c589290a285 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 8 Feb 2026 17:49:29 -0500 Subject: [PATCH 06/13] docs: remove reference to validation.rst in index.rst Signed-off-by: Saquib Saifee --- docs/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c4236bcc2..729103101 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,7 +45,6 @@ If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-o install architecture examples - validation contributing support changelog From d3b3f54eec4c5b05aa256284ff42dafebb251783 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Thu, 12 Feb 2026 06:52:49 -0500 Subject: [PATCH 07/13] Update examples/complex_validation.py Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 7444de551..434ad702d 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -144,7 +144,7 @@ def validate_sbom(raw_data: str) -> bool: # 1. Attempt to detect JSON and its version try: data = json.loads(raw_data) - output_format = OutputFormat.JSON + input_format = OutputFormat.JSON spec_version_str = data.get('specVersion') if not spec_version_str: print('Error: Missing specVersion in JSON SBOM', file=sys.stderr) From 5a842e13bba8bc58e63aa116b0076a84490c191f Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 15 Feb 2026 11:48:28 -0500 Subject: [PATCH 08/13] fix: correct validation errors and refactor for code quality - Fix validation_errors.message -> validation_errors.data.message (ValidationError wraps underlying error in data attribute) - Use consistent input_format variable naming throughout (semantic clarity: detecting input format, not output) - Fix XML validator variable name to avoid type conflicts - Refactor validate_sbom to reduce complexity (C901) - Extract _detect_json_format helper function - Extract _detect_xml_format helper function - Improves maintainability and testability - Add type ignore for lxml import (missing stubs) - Simplify docstring per maintainer feedback - Change 'SBOMs' to 'documents' (CycloneDX not SBOM-only) Addresses all maintainer feedback from PR review. Fixes test failures and passes all pre-commit checks. Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 86 +++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 434ad702d..985a0a944 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -28,12 +28,8 @@ from cyclonedx.validation.xml import XmlValidator """ -This example demonstrates how to validate CycloneDX SBOMs (both JSON and XML). -Validation is built upon `jsonschema` for JSON and `lxml` for XML. -To use validation, ensure you have installed the library with the validation extra: - pip install cyclonedx-python-lib[validation] - or - pip install cyclonedx-python-lib[json-validation,xml-validation] +This example demonstrates how to validate CycloneDX documents (both JSON and XML). +Make sure to have the needed dependencies installed - install the library's extra 'validation' for that. """ # region Sample SBOMs @@ -100,7 +96,7 @@ validation_errors = json_validator.validate_str(INVALID_JSON_SBOM) if validation_errors: print('Validation failed as expected.') - print(f'Error Message: {validation_errors.message}') + print(f'Error Message: {validation_errors.data.message}') print(f'JSON Path: {validation_errors.data.json_path}') print(f'Invalid Data: {validation_errors.data.instance}') except MissingOptionalDependencyException as error: @@ -119,8 +115,8 @@ xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5) try: - validation_errors = xml_validator.validate_str(XML_SBOM) - if validation_errors: + xml_validation_errors = xml_validator.validate_str(XML_SBOM) + if xml_validation_errors: print('XML SBOM is invalid!', file=sys.stderr) else: print('XML SBOM is valid') @@ -138,51 +134,63 @@ print('--- Dynamic Validation ---') -def validate_sbom(raw_data: str) -> bool: - """Validate an SBOM by detecting its format and version.""" - - # 1. Attempt to detect JSON and its version +def _detect_json_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: + """Detect JSON format and extract schema version.""" try: data = json.loads(raw_data) - input_format = OutputFormat.JSON spec_version_str = data.get('specVersion') if not spec_version_str: print('Error: Missing specVersion in JSON SBOM', file=sys.stderr) - return False + return None schema_version = SchemaVersion.from_version(spec_version_str) + return (OutputFormat.JSON, schema_version) except (json.JSONDecodeError, ValueError): - # 2. Attempt to detect XML and its version - try: - from lxml import etree - xml_tree = etree.fromstring(raw_data.encode('utf-8')) - output_format = OutputFormat.XML - # Extract version from CycloneDX namespace - schema_version = SchemaVersion.V1_5 # Default - for ns in xml_tree.nsmap.values(): - if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): - try: - schema_version = SchemaVersion.from_version(ns.split('/')[-1]) - break - except ValueError: - pass - except (ImportError, etree.XMLSyntaxError): - print('Error: Unknown or malformed SBOM format', file=sys.stderr) - return False - except Exception as e: - print(f'Error: Format detection failed: {e}', file=sys.stderr) - return False + return None + + +def _detect_xml_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: + """Detect XML format and extract schema version.""" + try: + from lxml import etree # type: ignore[import-untyped] + xml_tree = etree.fromstring(raw_data.encode('utf-8')) + # Extract version from CycloneDX namespace + schema_version = SchemaVersion.V1_5 # Default + for ns in xml_tree.nsmap.values(): + if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): + try: + schema_version = SchemaVersion.from_version(ns.split('/')[-1]) + break + except ValueError: + pass + return (OutputFormat.XML, schema_version) + except (ImportError, etree.XMLSyntaxError): + print('Error: Unknown or malformed SBOM format', file=sys.stderr) + return None + except Exception as e: + print(f'Error: Format detection failed: {e}', file=sys.stderr) + return None + + +def validate_sbom(raw_data: str) -> bool: + """Validate an SBOM by detecting its format and version.""" + # Detect format and version + format_info = _detect_json_format(raw_data) or _detect_xml_format(raw_data) + if not format_info: + return False + + input_format, schema_version = format_info - # 3. Perform validation using the detected format and version + # Perform validation try: - validator = make_schemabased_validator(output_format, schema_version) + validator = make_schemabased_validator(input_format, schema_version) errors = validator.validate_str(raw_data) if errors: - print(f'Validation failed for {output_format.name} version {schema_version.to_version()}', file=sys.stderr) + print(f'Validation failed for {input_format.name} version {schema_version.to_version()}', file=sys.stderr) print(f'Reason: {errors}', file=sys.stderr) return False - print(f'Successfully validated {output_format.name} SBOM (Version {schema_version.to_version()})') + print(f'Successfully validated {input_format.name} SBOM (Version {schema_version.to_version()})') return True except MissingOptionalDependencyException as error: From a617abebc9ea5486df3b19a0a4273200200b377c Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 15 Feb 2026 12:22:55 -0500 Subject: [PATCH 09/13] fix: add type ignore comment for lxml import - Add type: ignore[import-untyped] comment to suppress mypy error - lxml-stubs are not required for examples - All pre-commit checks now pass Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 985a0a944..9924ebb52 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -19,6 +19,8 @@ import sys from typing import TYPE_CHECKING +from lxml import etree # type: ignore[import-untyped] + from cyclonedx.exception import MissingOptionalDependencyException from cyclonedx.schema import OutputFormat, SchemaVersion from cyclonedx.validation import make_schemabased_validator @@ -151,7 +153,6 @@ def _detect_json_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | N def _detect_xml_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: """Detect XML format and extract schema version.""" try: - from lxml import etree # type: ignore[import-untyped] xml_tree = etree.fromstring(raw_data.encode('utf-8')) # Extract version from CycloneDX namespace schema_version = SchemaVersion.V1_5 # Default @@ -163,7 +164,7 @@ def _detect_xml_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | No except ValueError: pass return (OutputFormat.XML, schema_version) - except (ImportError, etree.XMLSyntaxError): + except etree.XMLSyntaxError: print('Error: Unknown or malformed SBOM format', file=sys.stderr) return None except Exception as e: From 0af98fdb615ac681987d8248c91e679e00ad7bc4 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Fri, 20 Feb 2026 07:05:30 -0500 Subject: [PATCH 10/13] fix: lazy-import lxml and fix Python 3.9 union syntax in complex_validation - Move 'from lxml import etree' inside _detect_xml_format to make it a lazy import; lxml is an optional dependency and a top-level import crashes when the xml-validation extra is not installed - Replace 'X | Y' union syntax with Optional[Tuple[...]] for Python 3.9 compatibility (X | Y requires Python 3.10+) - Apply maintainer review: replace if-not-spec_version_str guard with try/except around SchemaVersion.from_version to handle all malformed inputs (None, empty string, unknown version string) - Remove hardcoded SchemaVersion.V1_5 fallback default in XML detection - Clean up unnecessary inline comments Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 69 ++++++++++++++++------------------ 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 9924ebb52..8a5ae874a 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -17,9 +17,7 @@ import json import sys -from typing import TYPE_CHECKING - -from lxml import etree # type: ignore[import-untyped] +from typing import TYPE_CHECKING, Optional, Tuple from cyclonedx.exception import MissingOptionalDependencyException from cyclonedx.schema import OutputFormat, SchemaVersion @@ -136,41 +134,45 @@ print('--- Dynamic Validation ---') -def _detect_json_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: +def _detect_json_format(raw_data: str) -> 'Optional[Tuple[OutputFormat, SchemaVersion]]': """Detect JSON format and extract schema version.""" try: data = json.loads(raw_data) - spec_version_str = data.get('specVersion') - if not spec_version_str: - print('Error: Missing specVersion in JSON SBOM', file=sys.stderr) - return None + except json.JSONDecodeError: + return None + + spec_version_str = data.get('specVersion') + try: schema_version = SchemaVersion.from_version(spec_version_str) - return (OutputFormat.JSON, schema_version) - except (json.JSONDecodeError, ValueError): + except Exception: + print('failed to detect schema_version from', repr(spec_version_str), file=sys.stderr) return None + return (OutputFormat.JSON, schema_version) -def _detect_xml_format(raw_data: str) -> tuple[OutputFormat, SchemaVersion] | None: - """Detect XML format and extract schema version.""" +def _detect_xml_format(raw_data: str) -> 'Optional[Tuple[OutputFormat, SchemaVersion]]': + try: + from lxml import etree # type: ignore[import-untyped] + except ImportError: + return None + try: xml_tree = etree.fromstring(raw_data.encode('utf-8')) - # Extract version from CycloneDX namespace - schema_version = SchemaVersion.V1_5 # Default - for ns in xml_tree.nsmap.values(): - if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): - try: - schema_version = SchemaVersion.from_version(ns.split('/')[-1]) - break - except ValueError: - pass - return (OutputFormat.XML, schema_version) except etree.XMLSyntaxError: - print('Error: Unknown or malformed SBOM format', file=sys.stderr) - return None - except Exception as e: - print(f'Error: Format detection failed: {e}', file=sys.stderr) return None + for ns in xml_tree.nsmap.values(): + if ns and ns.startswith('http://cyclonedx.org/schema/bom/'): + version_str = ns.split('/')[-1] + try: + return (OutputFormat.XML, SchemaVersion.from_version(version_str)) + except Exception: + print('failed to detect schema_version from namespace', repr(ns), file=sys.stderr) + return None + + print('failed to detect CycloneDX namespace in XML document', file=sys.stderr) + return None + def validate_sbom(raw_data: str) -> bool: """Validate an SBOM by detecting its format and version.""" @@ -180,22 +182,17 @@ def validate_sbom(raw_data: str) -> bool: return False input_format, schema_version = format_info - - # Perform validation try: validator = make_schemabased_validator(input_format, schema_version) errors = validator.validate_str(raw_data) - if errors: - print(f'Validation failed for {input_format.name} version {schema_version.to_version()}', file=sys.stderr) - print(f'Reason: {errors}', file=sys.stderr) + print(f'Validation failed ({input_format.name} {schema_version.to_version()}): {errors}', + file=sys.stderr) return False - - print(f'Successfully validated {input_format.name} SBOM (Version {schema_version.to_version()})') + print(f'Valid {input_format.name} SBOM (schema {schema_version.to_version()})') return True - - except MissingOptionalDependencyException as error: - print(f'Validation skipped due to missing dependencies: {error}') + except MissingOptionalDependencyException as e: + print(f'Validation skipped (missing dependencies): {e}') return False From 4fb1f4c843ae35572f8df6c0d1b6643dc75dd283 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Wed, 25 Feb 2026 13:27:37 -0500 Subject: [PATCH 11/13] Update examples/complex_validation.py Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 8a5ae874a..b56470e65 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -150,7 +150,7 @@ def _detect_json_format(raw_data: str) -> 'Optional[Tuple[OutputFormat, SchemaVe return (OutputFormat.JSON, schema_version) -def _detect_xml_format(raw_data: str) -> 'Optional[Tuple[OutputFormat, SchemaVersion]]': +def _detect_xml_format(raw_data: str) -> Optional[Tuple[OutputFormat, SchemaVersion]]: try: from lxml import etree # type: ignore[import-untyped] except ImportError: From 7030e3417963e67186cce1af5554c8063f2b13ed Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Wed, 25 Feb 2026 13:27:46 -0500 Subject: [PATCH 12/13] Update examples/complex_validation.py Co-authored-by: Jan Kowalleck Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index b56470e65..2b92f8557 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -134,7 +134,7 @@ print('--- Dynamic Validation ---') -def _detect_json_format(raw_data: str) -> 'Optional[Tuple[OutputFormat, SchemaVersion]]': +def _detect_json_format(raw_data: str) -> Optional[Tuple[OutputFormat, SchemaVersion]]: """Detect JSON format and extract schema version.""" try: data = json.loads(raw_data) From 7c6175968f4881db3f717a10cd429ef223fd99f4 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Wed, 25 Feb 2026 16:50:31 -0500 Subject: [PATCH 13/13] refactor: narrow try/except blocks in JSON validation example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the try/except in the JSON validation section to use try/except/else, so that only the validate_str() calls — which can actually raise MissingOptionalDependencyException — are inside the try blocks. Signed-off-by: Saquib Saifee --- examples/complex_validation.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/examples/complex_validation.py b/examples/complex_validation.py index 2b92f8557..5b094cc53 100644 --- a/examples/complex_validation.py +++ b/examples/complex_validation.py @@ -17,7 +17,7 @@ import json import sys -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional from cyclonedx.exception import MissingOptionalDependencyException from cyclonedx.schema import OutputFormat, SchemaVersion @@ -83,9 +83,12 @@ # Create a JSON validator for a specific schema version json_validator: 'JsonValidator' = make_schemabased_validator(OutputFormat.JSON, SchemaVersion.V1_5) +# 1. Validate valid SBOM try: - # 1. Validate valid SBOM validation_errors = json_validator.validate_str(JSON_SBOM) +except MissingOptionalDependencyException as error: + print('JSON validation was skipped:', error) +else: if validation_errors: print('JSON SBOM is unexpectedly invalid!', file=sys.stderr) else: @@ -93,14 +96,16 @@ # 2. Validate invalid SBOM and inspect details print('\nChecking invalid JSON SBOM...') - validation_errors = json_validator.validate_str(INVALID_JSON_SBOM) - if validation_errors: - print('Validation failed as expected.') - print(f'Error Message: {validation_errors.data.message}') - print(f'JSON Path: {validation_errors.data.json_path}') - print(f'Invalid Data: {validation_errors.data.instance}') -except MissingOptionalDependencyException as error: - print('JSON validation was skipped:', error) + try: + validation_errors = json_validator.validate_str(INVALID_JSON_SBOM) + except MissingOptionalDependencyException as error: + print('JSON validation was skipped:', error) + else: + if validation_errors: + print('Validation failed as expected.') + print(f'Error Message: {validation_errors.data.message}') + print(f'JSON Path: {validation_errors.data.json_path}') + print(f'Invalid Data: {validation_errors.data.instance}') # endregion JSON Validation @@ -134,7 +139,7 @@ print('--- Dynamic Validation ---') -def _detect_json_format(raw_data: str) -> Optional[Tuple[OutputFormat, SchemaVersion]]: +def _detect_json_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]: """Detect JSON format and extract schema version.""" try: data = json.loads(raw_data) @@ -150,7 +155,7 @@ def _detect_json_format(raw_data: str) -> Optional[Tuple[OutputFormat, SchemaVer return (OutputFormat.JSON, schema_version) -def _detect_xml_format(raw_data: str) -> Optional[Tuple[OutputFormat, SchemaVersion]]: +def _detect_xml_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]: try: from lxml import etree # type: ignore[import-untyped] except ImportError: