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