Examples
Complex Serialize
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
18import sys
19from typing import TYPE_CHECKING
20
21from packageurl import PackageURL
22
23from cyclonedx.contrib.license.factories import LicenseFactory
24from cyclonedx.contrib.this.builders import this_component as cdx_lib_component
25from cyclonedx.exception import MissingOptionalDependencyException
26from cyclonedx.model import XsUri
27from cyclonedx.model.bom import Bom
28from cyclonedx.model.component import Component, ComponentType
29from cyclonedx.model.contact import OrganizationalEntity
30from cyclonedx.output import make_outputter
31from cyclonedx.output.json import JsonV1Dot5
32from cyclonedx.schema import OutputFormat, SchemaVersion
33from cyclonedx.validation import make_schemabased_validator
34from cyclonedx.validation.json import JsonStrictValidator
35
36if TYPE_CHECKING:
37 from cyclonedx.output.json import Json as JsonOutputter
38 from cyclonedx.output.xml import Xml as XmlOutputter
39 from cyclonedx.validation.xml import XmlValidator
40
41
42lc_factory = LicenseFactory()
43
44# region build the BOM
45
46bom = Bom()
47bom.metadata.tools.components.add(cdx_lib_component())
48bom.metadata.tools.components.add(Component(
49 name='my-own-SBOM-generator',
50 type=ComponentType.APPLICATION,
51))
52
53bom.metadata.component = root_component = Component(
54 name='myApp',
55 type=ComponentType.APPLICATION,
56 licenses=[lc_factory.make_from_string('MIT')],
57 bom_ref='myApp',
58)
59
60component1 = Component(
61 type=ComponentType.LIBRARY,
62 name='some-component',
63 group='acme',
64 version='1.33.7-beta.1',
65 licenses=[lc_factory.make_from_string('(c) 2021 Acme inc.')],
66 supplier=OrganizationalEntity(
67 name='Acme Inc',
68 urls=[XsUri('https://www.acme.org')]
69 ),
70 bom_ref='myComponent@1.33.7-beta.1',
71 purl=PackageURL('generic', 'acme', 'some-component', '1.33.7-beta.1')
72)
73bom.components.add(component1)
74bom.register_dependency(root_component, [component1])
75
76component2 = Component(
77 type=ComponentType.LIBRARY,
78 name='some-library',
79 licenses=[lc_factory.make_from_string('GPL-3.0-only WITH Classpath-exception-2.0')]
80)
81bom.components.add(component2)
82bom.register_dependency(component1, [component2])
83
84# endregion build the BOM
85
86# region JSON
87"""demo with explicit instructions for SchemaVersion, outputter and validator"""
88
89my_json_outputter: 'JsonOutputter' = JsonV1Dot5(bom)
90serialized_json = my_json_outputter.output_as_string(indent=2)
91print(serialized_json)
92my_json_validator = JsonStrictValidator(SchemaVersion.V1_7)
93try:
94 json_validation_errors = my_json_validator.validate_str(serialized_json)
95 if json_validation_errors:
96 print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr)
97 sys.exit(2)
98 print('JSON valid')
99except MissingOptionalDependencyException as error:
100 print('JSON-validation was skipped due to', error)
101
102# endregion JSON
103
104print('', '=' * 30, '', sep='\n')
105
106# region XML
107"""demo with implicit instructions for SchemaVersion, outputter and validator. TypeCheckers will catch errors."""
108
109my_xml_outputter: 'XmlOutputter' = make_outputter(bom, OutputFormat.XML, SchemaVersion.V1_7)
110serialized_xml = my_xml_outputter.output_as_string(indent=2)
111print(serialized_xml)
112my_xml_validator: 'XmlValidator' = make_schemabased_validator(
113 my_xml_outputter.output_format, my_xml_outputter.schema_version)
114try:
115 xml_validation_errors = my_xml_validator.validate_str(serialized_xml)
116 if xml_validation_errors:
117 print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr)
118 sys.exit(2)
119 print('XML valid')
120except MissingOptionalDependencyException as error:
121 print('XML-validation was skipped due to', error)
122
123# endregion XML
Complex Deserialize
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
18import sys
19import warnings
20from json import loads as json_loads
21from typing import TYPE_CHECKING
22
23from defusedxml import ElementTree as SafeElementTree # type:ignore[import-untyped]
24
25from cyclonedx.exception import MissingOptionalDependencyException
26from cyclonedx.model.bom import Bom
27from cyclonedx.schema import OutputFormat, SchemaVersion
28from cyclonedx.schema.deprecation import BaseSchemaDeprecationWarning
29from cyclonedx.validation import make_schemabased_validator
30from cyclonedx.validation.json import JsonStrictValidator
31
32if TYPE_CHECKING:
33 from cyclonedx.validation.xml import XmlValidator
34
35# region JSON
36
37json_data = """{
38 "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
39 "bomFormat": "CycloneDX",
40 "specVersion": "1.7",
41 "serialNumber": "urn:uuid:88fabcfa-7529-4ba2-8256-29bec0c03900",
42 "version": 1,
43 "metadata": {
44 "timestamp": "2024-02-10T21:38:53.313120+00:00",
45 "tools": [
46 {
47 "vendor": "CycloneDX",
48 "name": "cyclonedx-python-lib",
49 "version": "6.4.1",
50 "externalReferences": [
51 {
52 "type": "build-system",
53 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
54 },
55 {
56 "type": "distribution",
57 "url": "https://pypi.org/project/cyclonedx-python-lib/"
58 },
59 {
60 "type": "documentation",
61 "url": "https://cyclonedx-python-library.readthedocs.io/"
62 },
63 {
64 "type": "issue-tracker",
65 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
66 },
67 {
68 "type": "license",
69 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
70 },
71 {
72 "type": "release-notes",
73 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
74 },
75 {
76 "type": "vcs",
77 "url": "https://github.com/CycloneDX/cyclonedx-python-lib"
78 },
79 {
80 "type": "website",
81 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme"
82 }
83 ]
84 }
85 ],
86 "component": {
87 "bom-ref": "myApp",
88 "name": "myApp",
89 "type": "application",
90 "licenses": [
91 {
92 "license": {
93 "id": "MIT"
94 }
95 }
96 ]
97 }
98 },
99 "components": [
100 {
101 "bom-ref": "myComponent@1.33.7-beta.1",
102 "type": "library",
103 "group": "acme",
104 "name": "some-component",
105 "version": "1.33.7-beta.1",
106 "purl": "pkg:generic/acme/some-component@1.33.7-beta.1",
107 "licenses": [
108 {
109 "license": {
110 "name": "(c) 2021 Acme inc."
111 }
112 }
113 ],
114 "supplier": {
115 "name": "Acme Inc",
116 "url": [
117 "https://www.acme.org"
118 ]
119 }
120 },
121 {
122 "bom-ref": "some-lib",
123 "type": "library",
124 "name": "some-library",
125 "licenses": [
126 {
127 "expression": "GPL-3.0-only WITH Classpath-exception-2.0"
128 }
129 ]
130 }
131 ],
132 "dependencies": [
133 {
134 "ref": "some-lib"
135 },
136 {
137 "dependsOn": [
138 "myComponent@1.33.7-beta.1"
139 ],
140 "ref": "myApp"
141 },
142 {
143 "dependsOn": [
144 "some-lib"
145 ],
146 "ref": "myComponent@1.33.7-beta.1"
147 }
148 ]
149}"""
150my_json_validator = JsonStrictValidator(SchemaVersion.V1_7)
151try:
152 json_validation_errors = my_json_validator.validate_str(json_data)
153 if json_validation_errors:
154 print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr)
155 sys.exit(2)
156 print('JSON valid')
157except MissingOptionalDependencyException as error:
158 print('JSON-validation was skipped due to', error)
159with warnings.catch_warnings():
160 warnings.filterwarnings('ignore', category=BaseSchemaDeprecationWarning)
161 bom_from_json = Bom.from_json( # type: ignore[attr-defined]
162 json_loads(json_data))
163print('bom_from_json', repr(bom_from_json))
164
165# endregion JSON
166
167print('', '=' * 30, '', sep='\n')
168
169# endregion XML
170
171xml_data = """<?xml version="1.0" ?>
172<bom xmlns="http://cyclonedx.org/schema/bom/1.7"
173 serialNumber="urn:uuid:88fabcfa-7529-4ba2-8256-29bec0c03900"
174 version="1"
175>
176 <metadata>
177 <timestamp>2024-02-10T21:38:53.313120+00:00</timestamp>
178 <tools>
179 <tool>
180 <vendor>CycloneDX</vendor>
181 <name>cyclonedx-python-lib</name>
182 <version>6.4.1</version>
183 <externalReferences>
184 <reference type="build-system">
185 <url>https://github.com/CycloneDX/cyclonedx-python-lib/actions</url>
186 </reference>
187 <reference type="distribution">
188 <url>https://pypi.org/project/cyclonedx-python-lib/</url>
189 </reference>
190 <reference type="documentation">
191 <url>https://cyclonedx-python-library.readthedocs.io/</url>
192 </reference>
193 <reference type="issue-tracker">
194 <url>https://github.com/CycloneDX/cyclonedx-python-lib/issues</url>
195 </reference>
196 <reference type="license">
197 <url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE</url>
198 </reference>
199 <reference type="release-notes">
200 <url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md</url>
201 </reference>
202 <reference type="vcs">
203 <url>https://github.com/CycloneDX/cyclonedx-python-lib</url>
204 </reference>
205 <reference type="website">
206 <url>https://github.com/CycloneDX/cyclonedx-python-lib/#readme</url>
207 </reference>
208 </externalReferences>
209 </tool>
210 </tools>
211 <component type="application" bom-ref="myApp">
212 <name>myApp</name>
213 <licenses>
214 <license>
215 <id>MIT</id>
216 </license>
217 </licenses>
218 </component>
219 </metadata>
220 <components>
221 <component type="library" bom-ref="myComponent@1.33.7-beta.1">
222 <supplier>
223 <name>Acme Inc</name>
224 <url>https://www.acme.org</url>
225 </supplier>
226 <group>acme</group>
227 <name>some-component</name>
228 <version>1.33.7-beta.1</version>
229 <licenses>
230 <license>
231 <name>(c) 2021 Acme inc.</name>
232 </license>
233 </licenses>
234 <purl>pkg:generic/acme/some-component@1.33.7-beta.1</purl>
235 </component>
236 <component type="library" bom-ref="some-lib">
237 <name>some-library</name>
238 <licenses>
239 <expression>GPL-3.0-only WITH Classpath-exception-2.0</expression>
240 </licenses>
241 </component>
242 </components>
243 <dependencies>
244 <dependency ref="some-lib"/>
245 <dependency ref="myApp">
246 <dependency ref="myComponent@1.33.7-beta.1"/>
247 </dependency>
248 <dependency ref="myComponent@1.33.7-beta.1">
249 <dependency ref="some-lib"/>
250 </dependency>
251 </dependencies>
252</bom>"""
253my_xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_7)
254try:
255 xml_validation_errors = my_xml_validator.validate_str(xml_data)
256 if xml_validation_errors:
257 print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr)
258 sys.exit(2)
259 print('XML valid')
260except MissingOptionalDependencyException as error:
261 print('XML-validation was skipped due to', error)
262with warnings.catch_warnings():
263 warnings.filterwarnings('ignore', category=BaseSchemaDeprecationWarning)
264 bom_from_xml = Bom.from_xml( # type: ignore[attr-defined]
265 SafeElementTree.fromstring(xml_data))
266print('bom_from_xml', repr(bom_from_xml))
267
268# endregion XML
269
270print('', '=' * 30, '', sep='\n')
271
272print('assert bom_from_json equals bom_from_xml')
273assert bom_from_json == bom_from_xml, 'expected to have equal BOMs from JSON and XML'
Complex Validation
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
18import json
19import sys
20from typing import TYPE_CHECKING, Optional
21
22from cyclonedx.exception import MissingOptionalDependencyException
23from cyclonedx.schema import OutputFormat, SchemaVersion
24from cyclonedx.validation import make_schemabased_validator
25from cyclonedx.validation.json import JsonValidationError
26
27if TYPE_CHECKING:
28 from cyclonedx.validation.json import JsonValidator
29 from cyclonedx.validation.xml import XmlValidator
30
31"""
32This example demonstrates how to validate CycloneDX documents (both JSON and XML).
33Make sure to have the needed dependencies installed - install the library's extra 'validation' for that.
34"""
35
36# region Sample SBOMs
37
38JSON_SBOM = """
39{
40 "bomFormat": "CycloneDX",
41 "specVersion": "1.5",
42 "version": 1,
43 "metadata": {
44 "component": {
45 "type": "application",
46 "name": "my-app",
47 "version": "1.0.0"
48 }
49 },
50 "components": []
51}
52"""
53
54XML_SBOM = """<?xml version="1.0" encoding="UTF-8"?>
55<bom xmlns="http://cyclonedx.org/schema/bom/1.5" version="1">
56 <metadata>
57 <component type="application">
58 <name>my-app</name>
59 <version>1.0.0</version>
60 </component>
61 </metadata>
62</bom>
63"""
64
65INVALID_JSON_SBOM = """
66{
67 "bomFormat": "CycloneDX",
68 "specVersion": "1.5",
69 "metadata": {
70 "component": {
71 "type": "invalid-type",
72 "name": "my-app"
73 }
74 }
75}
76"""
77# endregion Sample SBOMs
78
79
80# region JSON Validation
81
82print('--- JSON Validation ---')
83
84# Create a JSON validator for a specific schema version
85json_validator: 'JsonValidator' = make_schemabased_validator(OutputFormat.JSON, SchemaVersion.V1_5)
86
87# 1. Validate valid SBOM
88try:
89 validation_error = json_validator.validate_str(JSON_SBOM)
90except MissingOptionalDependencyException as error:
91 print('JSON validation was skipped:', error)
92else:
93 if validation_error:
94 print('JSON SBOM is unexpectedly invalid!', file=sys.stderr)
95 else:
96 print('JSON SBOM is valid')
97
98 # 2. Validate invalid SBOM and inspect details
99 print('\nChecking invalid JSON SBOM...')
100 try:
101 validation_error = json_validator.validate_str(INVALID_JSON_SBOM)
102 except MissingOptionalDependencyException as error:
103 print('JSON validation was skipped:', error)
104 else:
105 if validation_error:
106 if not isinstance(validation_error, JsonValidationError):
107 raise TypeError('Expected a single JSON validation error')
108 print('Validation failed as expected.')
109 print(f'Error Message: {validation_error.data.message}')
110 print(f'JSON Path: {validation_error.data.json_path}')
111 print(f'Invalid Data: {validation_error.data.instance}')
112
113# endregion JSON Validation
114
115
116print('\n' + '=' * 30 + '\n')
117
118
119# region XML Validation
120
121print('--- XML Validation ---')
122
123xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5)
124
125try:
126 xml_validation_errors = xml_validator.validate_str(XML_SBOM)
127 if xml_validation_errors:
128 print('XML SBOM is invalid!', file=sys.stderr)
129 else:
130 print('XML SBOM is valid')
131except MissingOptionalDependencyException as error:
132 print('XML validation was skipped:', error)
133
134# endregion XML Validation
135
136
137print('\n' + '=' * 30 + '\n')
138
139
140# region Dynamic version detection
141
142print('--- Dynamic Validation ---')
143
144
145def _detect_json_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]:
146 """Detect JSON format and extract schema version."""
147 try:
148 data = json.loads(raw_data)
149 except json.JSONDecodeError:
150 return None
151
152 spec_version_str = data.get('specVersion')
153 try:
154 schema_version = SchemaVersion.from_version(spec_version_str)
155 except Exception:
156 print('failed to detect schema_version from', repr(spec_version_str), file=sys.stderr)
157 return None
158 return (OutputFormat.JSON, schema_version)
159
160
161def _detect_xml_format(raw_data: str) -> Optional[tuple[OutputFormat, SchemaVersion]]:
162 try:
163 from lxml import etree # type: ignore[import-untyped]
164 except ImportError:
165 return None
166
167 try:
168 xml_tree = etree.fromstring(raw_data.encode('utf-8'))
169 except etree.XMLSyntaxError:
170 return None
171
172 for ns in xml_tree.nsmap.values():
173 if ns and ns.startswith('http://cyclonedx.org/schema/bom/'):
174 version_str = ns.split('/')[-1]
175 try:
176 return (OutputFormat.XML, SchemaVersion.from_version(version_str))
177 except Exception:
178 print('failed to detect schema_version from namespace', repr(ns), file=sys.stderr)
179 return None
180
181 print('failed to detect CycloneDX namespace in XML document', file=sys.stderr)
182 return None
183
184
185def validate_sbom(raw_data: str) -> bool:
186 """Validate an SBOM by detecting its format and version."""
187 # Detect format and version
188 format_info = _detect_json_format(raw_data) or _detect_xml_format(raw_data)
189 if not format_info:
190 return False
191
192 input_format, schema_version = format_info
193 try:
194 validator = make_schemabased_validator(input_format, schema_version)
195 errors = validator.validate_str(raw_data)
196 if errors:
197 print(f'Validation failed ({input_format.name} {schema_version.to_version()}): {errors}',
198 file=sys.stderr)
199 return False
200 print(f'Valid {input_format.name} SBOM (schema {schema_version.to_version()})')
201 return True
202 except MissingOptionalDependencyException as e:
203 print(f'Validation skipped (missing dependencies): {e}')
204 return False
205
206
207# Execute dynamic validation
208validate_sbom(JSON_SBOM)
209validate_sbom(XML_SBOM)
210
211# endregion Dynamic version detection