Skip to content

Commit bf596c0

Browse files
docs: Add comprehensive SBOM validation guide (#933)
### Description Adds validation documentation with practical examples for validating CycloneDX SBOMs, addressing #708. - Include practical examples for JSON and XML validation - Document error handling patterns with ValidationError inspection Resolves or fixes issue: #708 ### Affirmation - [x] My code follows the [CONTRIBUTING.md](https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CONTRIBUTING.md) guidelines --------- Signed-off-by: Saquib Saifee <saquibsaifee@ibm.com> Signed-off-by: Saquib Saifee <saquibsaifee2@gmail.com> Co-authored-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent f311b61 commit bf596c0

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

docs/examples.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@ Complex Deserialize
2929
.. literalinclude:: ../examples/complex_deserialize.py
3030
:language: python
3131
:linenos:
32+
33+
34+
Complex Validation
35+
------------------
36+
37+
.. literalinclude:: ../examples/complex_validation.py
38+
:language: python
39+
:linenos:

examples/complex_validation.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
import json
19+
import sys
20+
from typing import TYPE_CHECKING, Optional
21+
22+
from cyclonedx.exception import MissingOptionalDependencyException
23+
from cyclonedx.schema import OutputFormat, SchemaVersion
24+
from cyclonedx.validation import make_schemabased_validator
25+
26+
if TYPE_CHECKING:
27+
from cyclonedx.validation.json import JsonValidator
28+
from cyclonedx.validation.xml import XmlValidator
29+
30+
"""
31+
This example demonstrates how to validate CycloneDX documents (both JSON and XML).
32+
Make sure to have the needed dependencies installed - install the library's extra 'validation' for that.
33+
"""
34+
35+
# region Sample SBOMs
36+
37+
JSON_SBOM = """
38+
{
39+
"bomFormat": "CycloneDX",
40+
"specVersion": "1.5",
41+
"version": 1,
42+
"metadata": {
43+
"component": {
44+
"type": "application",
45+
"name": "my-app",
46+
"version": "1.0.0"
47+
}
48+
},
49+
"components": []
50+
}
51+
"""
52+
53+
XML_SBOM = """<?xml version="1.0" encoding="UTF-8"?>
54+
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" version="1">
55+
<metadata>
56+
<component type="application">
57+
<name>my-app</name>
58+
<version>1.0.0</version>
59+
</component>
60+
</metadata>
61+
</bom>
62+
"""
63+
64+
INVALID_JSON_SBOM = """
65+
{
66+
"bomFormat": "CycloneDX",
67+
"specVersion": "1.5",
68+
"metadata": {
69+
"component": {
70+
"type": "invalid-type",
71+
"name": "my-app"
72+
}
73+
}
74+
}
75+
"""
76+
# endregion Sample SBOMs
77+
78+
79+
# region JSON Validation
80+
81+
print('--- JSON Validation ---')
82+
83+
# Create a JSON validator for a specific schema version
84+
json_validator: 'JsonValidator' = make_schemabased_validator(OutputFormat.JSON, SchemaVersion.V1_5)
85+
86+
# 1. Validate valid SBOM
87+
try:
88+
validation_errors = json_validator.validate_str(JSON_SBOM)
89+
except MissingOptionalDependencyException as error:
90+
print('JSON validation was skipped:', error)
91+
else:
92+
if validation_errors:
93+
print('JSON SBOM is unexpectedly invalid!', file=sys.stderr)
94+
else:
95+
print('JSON SBOM is valid')
96+
97+
# 2. Validate invalid SBOM and inspect details
98+
print('\nChecking invalid JSON SBOM...')
99+
try:
100+
validation_errors = json_validator.validate_str(INVALID_JSON_SBOM)
101+
except MissingOptionalDependencyException as error:
102+
print('JSON validation was skipped:', error)
103+
else:
104+
if validation_errors:
105+
print('Validation failed as expected.')
106+
print(f'Error Message: {validation_errors.data.message}')
107+
print(f'JSON Path: {validation_errors.data.json_path}')
108+
print(f'Invalid Data: {validation_errors.data.instance}')
109+
110+
# endregion JSON Validation
111+
112+
113+
print('\n' + '=' * 30 + '\n')
114+
115+
116+
# region XML Validation
117+
118+
print('--- XML Validation ---')
119+
120+
xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5)
121+
122+
try:
123+
xml_validation_errors = xml_validator.validate_str(XML_SBOM)
124+
if xml_validation_errors:
125+
print('XML SBOM is invalid!', file=sys.stderr)
126+
else:
127+
print('XML SBOM is valid')
128+
except MissingOptionalDependencyException as error:
129+
print('XML validation was skipped:', error)
130+
131+
# endregion XML Validation
132+
133+
134+
print('\n' + '=' * 30 + '\n')
135+
136+
137+
# region Dynamic version detection
138+
139+
print('--- Dynamic Validation ---')
140+
141+
142+
def _detect_json_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]:
143+
"""Detect JSON format and extract schema version."""
144+
try:
145+
data = json.loads(raw_data)
146+
except json.JSONDecodeError:
147+
return None
148+
149+
spec_version_str = data.get('specVersion')
150+
try:
151+
schema_version = SchemaVersion.from_version(spec_version_str)
152+
except Exception:
153+
print('failed to detect schema_version from', repr(spec_version_str), file=sys.stderr)
154+
return None
155+
return (OutputFormat.JSON, schema_version)
156+
157+
158+
def _detect_xml_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]:
159+
try:
160+
from lxml import etree # type: ignore[import-untyped]
161+
except ImportError:
162+
return None
163+
164+
try:
165+
xml_tree = etree.fromstring(raw_data.encode('utf-8'))
166+
except etree.XMLSyntaxError:
167+
return None
168+
169+
for ns in xml_tree.nsmap.values():
170+
if ns and ns.startswith('http://cyclonedx.org/schema/bom/'):
171+
version_str = ns.split('/')[-1]
172+
try:
173+
return (OutputFormat.XML, SchemaVersion.from_version(version_str))
174+
except Exception:
175+
print('failed to detect schema_version from namespace', repr(ns), file=sys.stderr)
176+
return None
177+
178+
print('failed to detect CycloneDX namespace in XML document', file=sys.stderr)
179+
return None
180+
181+
182+
def validate_sbom(raw_data: str) -> bool:
183+
"""Validate an SBOM by detecting its format and version."""
184+
# Detect format and version
185+
format_info = _detect_json_format(raw_data) or _detect_xml_format(raw_data)
186+
if not format_info:
187+
return False
188+
189+
input_format, schema_version = format_info
190+
try:
191+
validator = make_schemabased_validator(input_format, schema_version)
192+
errors = validator.validate_str(raw_data)
193+
if errors:
194+
print(f'Validation failed ({input_format.name} {schema_version.to_version()}): {errors}',
195+
file=sys.stderr)
196+
return False
197+
print(f'Valid {input_format.name} SBOM (schema {schema_version.to_version()})')
198+
return True
199+
except MissingOptionalDependencyException as e:
200+
print(f'Validation skipped (missing dependencies): {e}')
201+
return False
202+
203+
204+
# Execute dynamic validation
205+
validate_sbom(JSON_SBOM)
206+
validate_sbom(XML_SBOM)
207+
208+
# endregion Dynamic version detection

0 commit comments

Comments
 (0)