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