# 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