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
25
26if TYPE_CHECKING:
27 from cyclonedx.validation.json import JsonValidator
28 from cyclonedx.validation.xml import XmlValidator
29
30"""
31This example demonstrates how to validate CycloneDX documents (both JSON and XML).
32Make sure to have the needed dependencies installed - install the library's extra 'validation' for that.
33"""
34
35# region Sample SBOMs
36
37JSON_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
53XML_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
64INVALID_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
81print('--- JSON Validation ---')
82
83# Create a JSON validator for a specific schema version
84json_validator: 'JsonValidator' = make_schemabased_validator(OutputFormat.JSON, SchemaVersion.V1_5)
85
86# 1. Validate valid SBOM
87try:
88 validation_errors = json_validator.validate_str(JSON_SBOM)
89except MissingOptionalDependencyException as error:
90 print('JSON validation was skipped:', error)
91else:
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
113print('\n' + '=' * 30 + '\n')
114
115
116# region XML Validation
117
118print('--- XML Validation ---')
119
120xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_5)
121
122try:
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')
128except MissingOptionalDependencyException as error:
129 print('XML validation was skipped:', error)
130
131# endregion XML Validation
132
133
134print('\n' + '=' * 30 + '\n')
135
136
137# region Dynamic version detection
138
139print('--- Dynamic Validation ---')
140
141
142def _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
158def _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
182def 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
205validate_sbom(JSON_SBOM)
206validate_sbom(XML_SBOM)
207
208# endregion Dynamic version detection