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
19from json import loads as json_loads
20from typing import TYPE_CHECKING
21
22from defusedxml import ElementTree as SafeElementTree # type:ignore[import-untyped]
23
24from cyclonedx.exception import MissingOptionalDependencyException
25from cyclonedx.model.bom import Bom
26from cyclonedx.schema import OutputFormat, SchemaVersion
27from cyclonedx.validation import make_schemabased_validator
28from cyclonedx.validation.json import JsonStrictValidator
29
30if TYPE_CHECKING:
31 from cyclonedx.validation.xml import XmlValidator
32
33# region JSON
34
35json_data = """{
36 "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
37 "bomFormat": "CycloneDX",
38 "specVersion": "1.7",
39 "serialNumber": "urn:uuid:88fabcfa-7529-4ba2-8256-29bec0c03900",
40 "version": 1,
41 "metadata": {
42 "timestamp": "2024-02-10T21:38:53.313120+00:00",
43 "tools": [
44 {
45 "vendor": "CycloneDX",
46 "name": "cyclonedx-python-lib",
47 "version": "6.4.1",
48 "externalReferences": [
49 {
50 "type": "build-system",
51 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
52 },
53 {
54 "type": "distribution",
55 "url": "https://pypi.org/project/cyclonedx-python-lib/"
56 },
57 {
58 "type": "documentation",
59 "url": "https://cyclonedx-python-library.readthedocs.io/"
60 },
61 {
62 "type": "issue-tracker",
63 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
64 },
65 {
66 "type": "license",
67 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
68 },
69 {
70 "type": "release-notes",
71 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
72 },
73 {
74 "type": "vcs",
75 "url": "https://github.com/CycloneDX/cyclonedx-python-lib"
76 },
77 {
78 "type": "website",
79 "url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme"
80 }
81 ]
82 }
83 ],
84 "component": {
85 "bom-ref": "myApp",
86 "name": "myApp",
87 "type": "application",
88 "licenses": [
89 {
90 "license": {
91 "id": "MIT"
92 }
93 }
94 ]
95 }
96 },
97 "components": [
98 {
99 "bom-ref": "myComponent@1.33.7-beta.1",
100 "type": "library",
101 "group": "acme",
102 "name": "some-component",
103 "version": "1.33.7-beta.1",
104 "purl": "pkg:generic/acme/some-component@1.33.7-beta.1",
105 "licenses": [
106 {
107 "license": {
108 "name": "(c) 2021 Acme inc."
109 }
110 }
111 ],
112 "supplier": {
113 "name": "Acme Inc",
114 "url": [
115 "https://www.acme.org"
116 ]
117 }
118 },
119 {
120 "bom-ref": "some-lib",
121 "type": "library",
122 "name": "some-library",
123 "licenses": [
124 {
125 "expression": "GPL-3.0-only WITH Classpath-exception-2.0"
126 }
127 ]
128 }
129 ],
130 "dependencies": [
131 {
132 "ref": "some-lib"
133 },
134 {
135 "dependsOn": [
136 "myComponent@1.33.7-beta.1"
137 ],
138 "ref": "myApp"
139 },
140 {
141 "dependsOn": [
142 "some-lib"
143 ],
144 "ref": "myComponent@1.33.7-beta.1"
145 }
146 ]
147}"""
148my_json_validator = JsonStrictValidator(SchemaVersion.V1_7)
149try:
150 json_validation_errors = my_json_validator.validate_str(json_data)
151 if json_validation_errors:
152 print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr)
153 sys.exit(2)
154 print('JSON valid')
155except MissingOptionalDependencyException as error:
156 print('JSON-validation was skipped due to', error)
157bom_from_json = Bom.from_json( # type: ignore[attr-defined]
158 json_loads(json_data))
159print('bom_from_json', repr(bom_from_json))
160
161# endregion JSON
162
163print('', '=' * 30, '', sep='\n')
164
165# endregion XML
166
167xml_data = """<?xml version="1.0" ?>
168<bom xmlns="http://cyclonedx.org/schema/bom/1.7"
169 serialNumber="urn:uuid:88fabcfa-7529-4ba2-8256-29bec0c03900"
170 version="1"
171>
172 <metadata>
173 <timestamp>2024-02-10T21:38:53.313120+00:00</timestamp>
174 <tools>
175 <tool>
176 <vendor>CycloneDX</vendor>
177 <name>cyclonedx-python-lib</name>
178 <version>6.4.1</version>
179 <externalReferences>
180 <reference type="build-system">
181 <url>https://github.com/CycloneDX/cyclonedx-python-lib/actions</url>
182 </reference>
183 <reference type="distribution">
184 <url>https://pypi.org/project/cyclonedx-python-lib/</url>
185 </reference>
186 <reference type="documentation">
187 <url>https://cyclonedx-python-library.readthedocs.io/</url>
188 </reference>
189 <reference type="issue-tracker">
190 <url>https://github.com/CycloneDX/cyclonedx-python-lib/issues</url>
191 </reference>
192 <reference type="license">
193 <url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE</url>
194 </reference>
195 <reference type="release-notes">
196 <url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md</url>
197 </reference>
198 <reference type="vcs">
199 <url>https://github.com/CycloneDX/cyclonedx-python-lib</url>
200 </reference>
201 <reference type="website">
202 <url>https://github.com/CycloneDX/cyclonedx-python-lib/#readme</url>
203 </reference>
204 </externalReferences>
205 </tool>
206 </tools>
207 <component type="application" bom-ref="myApp">
208 <name>myApp</name>
209 <licenses>
210 <license>
211 <id>MIT</id>
212 </license>
213 </licenses>
214 </component>
215 </metadata>
216 <components>
217 <component type="library" bom-ref="myComponent@1.33.7-beta.1">
218 <supplier>
219 <name>Acme Inc</name>
220 <url>https://www.acme.org</url>
221 </supplier>
222 <group>acme</group>
223 <name>some-component</name>
224 <version>1.33.7-beta.1</version>
225 <licenses>
226 <license>
227 <name>(c) 2021 Acme inc.</name>
228 </license>
229 </licenses>
230 <purl>pkg:generic/acme/some-component@1.33.7-beta.1</purl>
231 </component>
232 <component type="library" bom-ref="some-lib">
233 <name>some-library</name>
234 <licenses>
235 <expression>GPL-3.0-only WITH Classpath-exception-2.0</expression>
236 </licenses>
237 </component>
238 </components>
239 <dependencies>
240 <dependency ref="some-lib"/>
241 <dependency ref="myApp">
242 <dependency ref="myComponent@1.33.7-beta.1"/>
243 </dependency>
244 <dependency ref="myComponent@1.33.7-beta.1">
245 <dependency ref="some-lib"/>
246 </dependency>
247 </dependencies>
248</bom>"""
249my_xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_7)
250try:
251 xml_validation_errors = my_xml_validator.validate_str(xml_data)
252 if xml_validation_errors:
253 print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr)
254 sys.exit(2)
255 print('XML valid')
256except MissingOptionalDependencyException as error:
257 print('XML-validation was skipped due to', error)
258bom_from_xml = Bom.from_xml( # type: ignore[attr-defined]
259 SafeElementTree.fromstring(xml_data))
260print('bom_from_xml', repr(bom_from_xml))
261
262# endregion XML
263
264print('', '=' * 30, '', sep='\n')
265
266print('assert bom_from_json equals bom_from_xml')
267assert 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