├── .python-version ├── .gitattributes ├── cdxev ├── auxiliary │ ├── schema │ │ └── __init__.py │ ├── __init__.py │ └── filename_gen.py ├── amend │ ├── __init__.py │ ├── license.py │ └── command.py ├── __init__.py ├── validator │ ├── __init__.py │ └── helper.py ├── pkg.py ├── error.py ├── log.py └── initialize_sbom.py ├── tests ├── auxiliary │ ├── licenses │ │ ├── Apache-1.0.txt │ │ ├── UPPERCASE.license │ │ ├── license_name │ │ └── another license.txt │ ├── test_validate_sboms │ │ ├── test_schema.json │ │ ├── modified_sbom │ │ │ └── Acme_Application_9.1.1_20220217T101458_bom.json │ │ └── Acme_Application_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json │ ├── __init__.py │ ├── test_build_public_bom_sboms │ │ ├── schema │ │ │ ├── documentation_schema_1.json │ │ │ ├── documentation_schema_3.json │ │ │ ├── example_schema_1.json │ │ │ ├── example_schema_2.json │ │ │ ├── documentation_schema_2.json │ │ │ └── documentation_schema_4.json │ │ └── internal_removed_sbom.json │ ├── test_merge_sboms │ │ ├── governing_program.json │ │ └── ratings_lists_for_tests.json │ ├── test_amend_sboms │ │ ├── bom.json │ │ ├── bom_licenses_changed.json │ │ └── bom_licenses_changed_with_id.json │ ├── test_vex │ │ └── vex.json │ ├── test_set_sboms │ │ └── Acme_Application_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json │ └── helper.py ├── integration │ ├── data │ │ ├── set.input_empty.json │ │ ├── license-texts │ │ │ └── other license │ │ ├── validate │ │ │ └── invalid │ │ │ │ ├── default │ │ │ │ ├── laravel_1.6.cdx.json.errors │ │ │ │ ├── laravel_1.2.cdx.json.errors │ │ │ │ ├── laravel_1.3.cdx.json.errors │ │ │ │ ├── laravel_1.4.cdx.json.errors │ │ │ │ └── laravel_1.5.cdx.json.errors │ │ │ │ ├── strict │ │ │ │ ├── laravel_1.2.cdx.json.errors │ │ │ │ └── laravel_1.3.cdx.json.errors │ │ │ │ └── custom │ │ │ │ ├── Acme_Application_1.3_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors │ │ │ │ ├── Acme_Application_1.4_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors │ │ │ │ └── Acme_Application_1.5_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors │ │ ├── set.input_invalid.json │ │ ├── validate.invalid_schema.json │ │ ├── validate.custom_schema.json │ │ ├── build-public.input.schema.json │ │ ├── set.input_remap.json │ │ ├── list_command │ │ │ ├── list_components.csv │ │ │ ├── list_components.txt │ │ │ ├── list_licenses.csv │ │ │ └── list_licenses.txt │ │ ├── set.expected_remap.cdx.json │ │ ├── set.input_remap.cdx.json │ │ ├── validate.invalid_filename.json │ │ ├── vex.expected_search.json │ │ ├── set.input_version-ranges.cdx.json │ │ ├── init-sbom.initial_expected.json │ │ ├── set.input.json │ │ ├── set.expected_version-ranges.cdx.json │ │ ├── init-sbom.initial_default.json │ │ ├── vex.expected_list_default.csv │ │ ├── list-command.expected.json │ │ ├── merge-from-folder │ │ │ └── merge.input_1.cdx.json │ │ ├── merge.input_1.cdx.json │ │ ├── merge.vex.input_1.cdx.json │ │ └── amend.expected_delete-ambiguous-licenses.cdx.json │ ├── __init__.py │ ├── conftest.py │ └── helper.py ├── __init__.py ├── test_spec_version.py ├── test_main.py ├── test_error.py ├── test_log.py ├── test_initialize_sbom.py └── test_list_command.py ├── .github ├── workflows │ ├── pr-labeler.yml │ ├── main.yml │ ├── python-test-publish.yml │ ├── dependabot-pre-commit.yaml │ ├── bump_spdx_schema_version.yaml │ ├── python-publish.yml │ ├── scorecard.yml │ ├── tests.yml │ └── codeql.yml ├── labeler.yml └── dependabot.yml ├── docs ├── source │ ├── maintainers.rst │ ├── known_limitations.rst │ ├── first_steps.rst │ ├── index.rst │ ├── usage │ │ ├── index.rst │ │ ├── list.rst │ │ ├── amend.rst │ │ ├── vex.rst │ │ ├── init-sbom.rst │ │ ├── merge.rst │ │ ├── validate.rst │ │ └── build-public.rst │ └── conf.py ├── Makefile └── make.bat ├── NOTICE ├── .gitignore ├── .flake8 ├── mkdocs.yml ├── .pre-commit-config.yaml ├── SECURITY.md ├── pyproject.toml └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /cdxev/auxiliary/schema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/auxiliary/licenses/Apache-1.0.txt: -------------------------------------------------------------------------------- 1 | Dummy text -------------------------------------------------------------------------------- /tests/integration/data/set.input_empty.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /tests/integration/data/license-texts/other license: -------------------------------------------------------------------------------- 1 | Foo 2 | -------------------------------------------------------------------------------- /tests/auxiliary/licenses/UPPERCASE.license: -------------------------------------------------------------------------------- 1 | UPPERCASE LICENSE -------------------------------------------------------------------------------- /tests/auxiliary/test_validate_sboms/test_schema.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/auxiliary/licenses/license_name: -------------------------------------------------------------------------------- 1 | The text describing a license. -------------------------------------------------------------------------------- /cdxev/amend/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | -------------------------------------------------------------------------------- /cdxev/auxiliary/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | -------------------------------------------------------------------------------- /tests/auxiliary/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | -------------------------------------------------------------------------------- /tests/auxiliary/licenses/another license.txt: -------------------------------------------------------------------------------- 1 | The text describing another license. -------------------------------------------------------------------------------- /cdxev/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """ 4 | Main package of the tool. 5 | """ 6 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/default/laravel_1.6.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "asm89/stack-cors-1.3.0.0" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/default/laravel_1.2.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "'type' is a required property" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/default/laravel_1.3.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "'type' is a required property" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/default/laravel_1.4.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "'type' is a required property" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/default/laravel_1.5.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "'type' or 'components' is a required property" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/set.input_invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": { 4 | "name": "delete nested components" 5 | } 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /cdxev/validator/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from .validate import validate_sbom 4 | 5 | __all__ = ["validate_sbom"] 6 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/strict/laravel_1.2.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "Additional properties are not allowed ('properties' was unexpected)" 3 | ] 4 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/strict/laravel_1.3.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "Additional properties are not allowed ('externalReferences' was unexpected)" 3 | ] 4 | -------------------------------------------------------------------------------- /cdxev/pkg.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import importlib.metadata 4 | 5 | NAME = "cyclonedx-editor-validator" 6 | VENDOR = "Festo SE & Co. KG" 7 | VERSION = importlib.metadata.version(NAME) 8 | -------------------------------------------------------------------------------- /tests/integration/data/validate.invalid_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://github.com/festo-se/cyclonedx-editor-validator/my-schema", 4 | "type": "foo" 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/data/validate.custom_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://github.com/festo-se/cyclonedx-editor-validator/my-schema", 4 | "type": "array" 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/custom/Acme_Application_1.3_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "filename doesn't match regular expression", 3 | "True is not of type 'string'" 4 | ] 5 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/custom/Acme_Application_1.4_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "filename doesn't match regular expression", 3 | "'type' is a required property" 4 | ] 5 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/documentation_schema_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "group": { 4 | "const": "com.acme.internal" 5 | } 6 | }, 7 | "required": [ 8 | "group" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/data/validate/invalid/custom/Acme_Application_1.5_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json.errors: -------------------------------------------------------------------------------- 1 | [ 2 | "filename doesn't match regular expression", 3 | "'type' or 'components' is a required property" 4 | ] 5 | -------------------------------------------------------------------------------- /tests/integration/data/build-public.input.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "properties": { 4 | "group": { 5 | "const": "com.acme.internal" 6 | } 7 | }, 8 | "required": [ 9 | "group" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/documentation_schema_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": { 4 | "enum": [ 5 | "AcmeSecret", 6 | "AcmeNotPublic", 7 | "AcmeSensitive" 8 | ] 9 | } 10 | }, 11 | "required": [ 12 | "name" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """ 4 | Required for Visual Studio as otherwise it does not recognize the tests in the test folder, 5 | for reference see 6 | https://stackoverflow.com/questions/66504420/visual-studio-2019-cannot-run-python-unittests-failed-to-import-test-module 7 | """ 8 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | """ 4 | Required for Visual Studio as otherwise it does not recognize the tests in the test folder, 5 | for reference see 6 | https://stackoverflow.com/questions/66504420/visual-studio-2019-cannot-run-python-unittests-failed-to-import-test-module 7 | """ 8 | -------------------------------------------------------------------------------- /tests/integration/data/set.input_remap.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": { 4 | "name": "delete nested components" 5 | }, 6 | "set": { 7 | "components": null 8 | } 9 | }, 10 | { 11 | "id": { 12 | "name": "nested" 13 | }, 14 | "set": { 15 | "copyright": "foo" 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/example_schema_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "Example schema to describe when a component is considered internal", 4 | "properties": { 5 | "group": { 6 | "type": "string", 7 | "enum": [ 8 | "com.company.internal" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | triage: 10 | permissions: 11 | pull-requests: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 15 | -------------------------------------------------------------------------------- /docs/source/maintainers.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Maintainers 3 | =========== 4 | 5 | The following people are currently principally responsible for development of this tool: 6 | 7 | - Aleg Vilinski (aleg.vilinski@festo.com) 8 | 9 | Contributions were made by: 10 | 11 | - Moritz Marseu (moritz.marseu@festo.com) 12 | - Christian Beck (christian.beck@festo.com) 13 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | CycloneDX Editor Validator 2 | Copyright (c) 2023-2024 Festo SE & Co. KG 3 | 4 | This product includes material developed by third parties: 5 | License name to SPDX id mapping - Copyright (c) OWASP Foundation - - Apache-2.0 6 | License name to SPDX id mapping - Copyright (c) The Linux Foundation - - CC-BY-3.0 7 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | "documentation": 2 | - changed-files: 3 | - any-glob-to-any-file: "docs/**/*" 4 | 5 | "unittests": 6 | - changed-files: 7 | - any-glob-to-any-file: "tests/**/*" 8 | 9 | "settings_changes": 10 | - changed-files: 11 | - any-glob-to-any-file: ["*", "!LICENSE", "!README.md"] 12 | 13 | "enhancement": 14 | - changed-files: 15 | - any-glob-to-any-file: "cdxev/**/*" 16 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/example_schema_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "Example schema to describe when a component is considered internal", 4 | "properties": { 5 | "group": { 6 | "type": "string", 7 | "enum": [ 8 | "com.company.internal" 9 | ] 10 | } 11 | }, 12 | "not": { 13 | "required": [ 14 | "name" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python artifacts 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .coverage 6 | .coverage.* 7 | .pytest_cache/ 8 | 9 | # Build directories 10 | dist/ 11 | docs/build/ 12 | 13 | # Virtualenvs 14 | .venv/ 15 | venv/ 16 | ENV/ 17 | 18 | # IDE config 19 | .vscode/ 20 | /*.pyproj 21 | /*.sln 22 | .idea 23 | 24 | # Backup files 25 | *.bak 26 | 27 | # CycloneDX json files except for those in the tests directory 28 | *.cdx.json 29 | !tests/**/*.cdx.json 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "uv" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | target-branch: main 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every week 13 | interval: "weekly" 14 | open-pull-requests-limit: 10 15 | target-branch: main 16 | -------------------------------------------------------------------------------- /tests/integration/data/list_command/list_components.csv: -------------------------------------------------------------------------------- 1 | Name,Version,Supplier 2 | "Acme_Application","9.1.1","Company Legal" 3 | "some_name","T4.0.1.30","somecompany SE & Co.KG" 4 | "card-verifier","1.0.2","Acme, Inc." 5 | "tomcat-catalina","9.0.14","Acme, Inc." 6 | "common-util","3.0.0","Acme, Inc." 7 | "util","2.0.0","Example, Inc." 8 | "persistence","3.1.0","Acme, Inc." 9 | "Component index 1","1.0.0","Acme, Inc." 10 | "web-framework","1.0.0","Acme, Inc." 11 | "sub_web-framework","1.0.0","Acme, Inc." 12 | "license and copyright less component","","" -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max_line_length = 99 3 | 4 | ignore = 5 | # Whitespace before : (conflicting with black, see https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html) 6 | E203 7 | # Multiple statements on one line (def) : (conflicting with black, see https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html) 8 | E704 9 | # Bare except 10 | E722 11 | # Line break occurred before a binary operator (conflicting with black) 12 | W503 13 | # line break after binary operator (conflicting with black) 14 | W504 15 | -------------------------------------------------------------------------------- /docs/source/known_limitations.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Known Limitations 3 | ================= 4 | 5 | - Only JSON-formatted SBOMs are supported. XML support is not planned. 6 | - Though validation is supported it's not used before execute an operation. Unforeseen errors might occur if an invalid CycloneDX is fed as input. Users are encouraged to use either a stock CycloneDX validator or the validation of this tool beforehand, depending on whether the input/output is meant to conform to a specific specification or not. 7 | - *git bash* is known to cause problems with interactive user input. The *set* command won't prompt the user whether to overwrite conflicting information in *git bash*. 8 | -------------------------------------------------------------------------------- /tests/integration/data/list_command/list_components.txt: -------------------------------------------------------------------------------- 1 | Acme_Application 2 | 9.1.1 3 | Company Legal 4 | 5 | This product includes material developed by third parties: 6 | 7 | some_name 8 | T4.0.1.30 9 | somecompany SE & Co.KG 10 | 11 | card-verifier 12 | 1.0.2 13 | Acme, Inc. 14 | 15 | tomcat-catalina 16 | 9.0.14 17 | Acme, Inc. 18 | 19 | common-util 20 | 3.0.0 21 | Acme, Inc. 22 | 23 | util 24 | 2.0.0 25 | Example, Inc. 26 | 27 | persistence 28 | 3.1.0 29 | Acme, Inc. 30 | 31 | Component index 1 32 | 1.0.0 33 | Acme, Inc. 34 | 35 | web-framework 36 | 1.0.0 37 | Acme, Inc. 38 | 39 | sub_web-framework 40 | 1.0.0 41 | Acme, Inc. 42 | 43 | license and copyright less component -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/integration/data/list_command/list_licenses.csv: -------------------------------------------------------------------------------- 1 | Name,Copyright,Licenses 2 | "Acme_Application","Company Legal 2022, all rights reserved","" 3 | "some_name","Company Legal 2022, all rights reserved","Apache-2.0" 4 | "card-verifier","","EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0,A licenses name of card verifier" 5 | "tomcat-catalina","Copyright tomcat-catalina","Apache-1.0,Apache-2.0" 6 | "common-util","","BSD-3-Clause" 7 | "util","","Example, Inc. Commercial License,Apache-1.0" 8 | "persistence","","Apache-2.0" 9 | "Component index 1","","(CDDL-1.0 OR GPL-2.0-with-classpath-exception)" 10 | "web-framework","","Apache-1.0" 11 | "sub_web-framework","","Apache-1.0,Apache-2.0,Apache-3.0" 12 | "license and copyright less component","","" -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/documentation_schema_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "group": { 4 | "const": "com.acme.internal" 5 | } 6 | }, 7 | "required": [ 8 | "group" 9 | ], 10 | "not": { 11 | "properties": { 12 | "properties": { 13 | "contains": { 14 | "properties": { 15 | "name": { 16 | "const": "internal:public" 17 | }, 18 | "value": { 19 | "const": "true" 20 | } 21 | }, 22 | "required": [ 23 | "name", 24 | "value" 25 | ] 26 | } 27 | } 28 | }, 29 | "required": [ 30 | "properties" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/schema/documentation_schema_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "licenses": { 4 | "contains": { 5 | "properties": { 6 | "license": { 7 | "properties": { 8 | "text": { 9 | "properties": { 10 | "content": { 11 | "pattern": "This must not be made public" 12 | } 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "text" 18 | ] 19 | } 20 | }, 21 | "required": [ 22 | "license" 23 | ] 24 | } 25 | } 26 | }, 27 | "required": [ 28 | "licenses" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: CycloneDX Editor Validator Tool 2 | site_url: https://festo-se.github.io/cyclonedx-editor-validator/ 3 | repo_url: https://github.com/Festo-se/cyclonedx-editor-validator/ 4 | edit_uri: blob/main/docs/ 5 | copyright: Copyright 2024, Copyright (c) Festo SE & Co. KG 6 | 7 | theme: 8 | name: readthedocs 9 | 10 | markdown_extensions: 11 | - mdx_truly_sane_lists 12 | 13 | extra_css: 14 | - css/festo-web-essentials.css 15 | - https://www.festo.com/fonts/fonts.css 16 | 17 | nav: 18 | - 'Documentation': 19 | - 'First Steps': 'index.md' 20 | - 'Available commands': 'available_commands.md' 21 | - 'Known Limitations': 'known_limitations.md' 22 | - 'Further Information': 23 | - 'Maintainers': 'maintainers.md' 24 | - 'Contributing': 'CONTRIBUTING.md' 25 | -------------------------------------------------------------------------------- /docs/source/first_steps.rst: -------------------------------------------------------------------------------- 1 | First Steps 2 | =========== 3 | 4 | ============= 5 | Installation 6 | ============= 7 | 8 | This tool is published on `PyPi `_ and can be installed via pip: 9 | 10 | .. code-block:: bash 11 | 12 | python -m pip install cyclonedx-editor-validator 13 | 14 | ====== 15 | Usage 16 | ====== 17 | 18 | The tool comes with built-in command-line help on its usage. To display these hints, run it with: 19 | 20 | .. code-block:: bash 21 | 22 | cdx-ev --help # Lists commands and options 23 | cdx-ev --help # Help for a specific command and its options 24 | 25 | Or clone the repository and run it as module with: 26 | 27 | .. code-block:: bash 28 | 29 | git clone https://github.com/Festo-se/cyclonedx-editor-validator.git # Clone repository 30 | python -m cdxev --help # Lists commands and options 31 | -------------------------------------------------------------------------------- /tests/integration/data/list_command/list_licenses.txt: -------------------------------------------------------------------------------- 1 | Acme_Application: 2 | Company Legal 2022, all rights reserved 3 | 4 | This product includes material developed by third parties: 5 | 6 | some_name: 7 | Company Legal 2022, all rights reserved 8 | Apache-2.0 9 | 10 | card-verifier: 11 | EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 12 | A licenses name of card verifier 13 | 14 | tomcat-catalina: 15 | Copyright tomcat-catalina 16 | Apache-1.0 17 | Apache-2.0 18 | 19 | common-util: 20 | BSD-3-Clause 21 | 22 | util: 23 | Example, Inc. Commercial License 24 | Apache-1.0 25 | 26 | persistence: 27 | Apache-2.0 28 | 29 | Component index 1: 30 | (CDDL-1.0 OR GPL-2.0-with-classpath-exception) 31 | 32 | web-framework: 33 | Apache-1.0 34 | 35 | sub_web-framework: 36 | Apache-1.0 37 | Apache-2.0 38 | Apache-3.0 39 | 40 | license and copyright less component: 41 | No license or copyright information available. 42 | 43 | -------------------------------------------------------------------------------- /tests/integration/data/set.expected_remap.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", 3 | "bomFormat": "CycloneDX", 4 | "specVersion": "1.4", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-12-21T09:41:59.744Z", 8 | "tools": [ 9 | { 10 | "vendor": "Company Legal", 11 | "name": "some-tool", 12 | "version": "3.0.0" 13 | } 14 | ], 15 | "component": { 16 | "type": "application", 17 | "name": "test-app", 18 | "version": "1.0.0", 19 | "bom-ref": "pkg:npm/test-app@1.0.0", 20 | "author": "Company Legal", 21 | "purl": "pkg:npm/test-app@1.0.0", 22 | "description": "Will be deleted" 23 | } 24 | }, 25 | "components": [ 26 | { 27 | "type": "library", 28 | "name": "delete nested components", 29 | "bom-ref": "delete_nested_components" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: build and test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 24 | - name: Install the latest version of uv 25 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 26 | with: 27 | enable-cache: true 28 | - name: Install dependencies 29 | run: uv sync --frozen 30 | - name: Build package 31 | run: uv build 32 | 33 | tests: 34 | needs: build 35 | uses: ./.github/workflows/tests.yml 36 | -------------------------------------------------------------------------------- /tests/integration/data/set.input_remap.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", 3 | "bomFormat": "CycloneDX", 4 | "specVersion": "1.4", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-12-21T09:41:59.744Z", 8 | "tools": [ 9 | { 10 | "vendor": "Company Legal", 11 | "name": "some-tool", 12 | "version": "3.0.0" 13 | } 14 | ], 15 | "component": { 16 | "type": "application", 17 | "name": "test-app", 18 | "version": "1.0.0", 19 | "bom-ref": "pkg:npm/test-app@1.0.0", 20 | "author": "Company Legal", 21 | "purl": "pkg:npm/test-app@1.0.0", 22 | "description": "Will be deleted" 23 | } 24 | }, 25 | "components": [ 26 | { 27 | "type": "library", 28 | "name": "delete nested components", 29 | "bom-ref": "delete_nested_components", 30 | "components": [ 31 | { 32 | "type": "application", 33 | "name": "nested" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tests/integration/data/validate.invalid_filename.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", 3 | "bomFormat": "CycloneDX", 4 | "specVersion": "1.4", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-12-21T09:41:59.744Z", 8 | "tools": [ 9 | { 10 | "vendor": "Company Legal", 11 | "name": "some-tool", 12 | "version": "3.0.0" 13 | } 14 | ], 15 | "component": { 16 | "type": "application", 17 | "name": "test-app", 18 | "version": "1.0.0", 19 | "bom-ref": "pkg:npm/test-app@1.0.0", 20 | "author": "Company Legal", 21 | "purl": "pkg:npm/test-app@1.0.0", 22 | "description": "Will be deleted" 23 | } 24 | }, 25 | "components": [ 26 | { 27 | "type": "library", 28 | "name": "delete nested components", 29 | "bom-ref": "delete_nested_components", 30 | "components": [ 31 | { 32 | "type": "application", 33 | "name": "nested" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | CycloneDX Editor Validator Tool 2 | =============================== 3 | 4 | This command-line tool performs various actions on `CycloneDX `_ SBOMs. It allows you to modify, merge and validate your Software Bill of Materials (SBOM). 5 | Originally, it was created to validate CycloneDX SBOMs against not only official schema, which is already supported by `cyclonedx-cli `_, but also custom schemas. 6 | Please note that even though we are speaking of "SBOMs", the tool does not have any limitation regarding the variant of CycloneDX BOMs, e.g., it also works with BOMs having only "hardware" included. 7 | 8 | This tool also offers command to amend and set, respectively editing information within a BOM. In addition the tool supports merging two or more BOMs. 9 | 10 | .. toctree:: 11 | :caption: Documentation 12 | :maxdepth: 1 13 | 14 | self 15 | first_steps 16 | usage/index 17 | known_limitations 18 | 19 | .. toctree:: 20 | :caption: Further information 21 | :maxdepth: 1 22 | 23 | maintainers 24 | CONTRIBUTING 25 | -------------------------------------------------------------------------------- /tests/integration/data/vex.expected_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "version": 1, 5 | "vulnerabilities": [ 6 | { 7 | "description": "some description of a vulnerability 2", 8 | "id": "CVE-1013-0002", 9 | "ratings": [ 10 | { 11 | "score": 7.2, 12 | "severity": "high", 13 | "method": "CVSSv31", 14 | "vector": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H" 15 | } 16 | ], 17 | "published": "1013-01-01T01:02Z", 18 | "updated": "1013-03-02T12:24Z", 19 | "affects": [{"ref": "11231231"}], 20 | "analysis": { 21 | "state": "not_affected", 22 | "justification": "add justification here", 23 | "response": [ 24 | "add response here", 25 | "more than one response is possible" 26 | ], 27 | "detail": "the fields state, justification and response are enums, please see CycloneDX specification" 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /tests/integration/data/set.input_version-ranges.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-02-17T10:14:58Z", 8 | "component": { 9 | "type": "application", 10 | "bom-ref": "acme-app", 11 | "name": "Acme_Application", 12 | "version": "9.1.1" 13 | } 14 | }, 15 | "components": [ 16 | { 17 | "bom-ref": "web-framework@1.0.0", 18 | "type": "library", 19 | "name": "web-framework", 20 | "version": "1.0.0" 21 | }, 22 | { 23 | "bom-ref": "web-framework@2.0.0", 24 | "type": "library", 25 | "name": "web-framework", 26 | "version": "2.0.0" 27 | }, 28 | { 29 | "bom-ref": "web-framework@2.0.1", 30 | "type": "library", 31 | "name": "web-framework", 32 | "version": "3.0.0" 33 | }, 34 | { 35 | "bom-ref": "container@2.0.1", 36 | "type": "library", 37 | "name": "container", 38 | "version": "2.0.1", 39 | "components": [ 40 | { 41 | "bom-ref": "container@2.0.1|web-framework@2.0.1", 42 | "type": "library", 43 | "name": "web-framework", 44 | "version": "2.0.1" 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/data/init-sbom.initial_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM." 5 | } 6 | ], 7 | "metadata": { 8 | "authors": [ 9 | { 10 | "name": "authors name", 11 | "email": "test@test.com" 12 | } 13 | ], 14 | "component": { 15 | "bom-ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM.", 16 | "copyright": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.", 17 | "name": "software name", 18 | "supplier": { 19 | "name": "supplier" 20 | }, 21 | "type": "application", 22 | "version": "1.1.1" 23 | }, 24 | "timestamp": "2024-10-15T22:15:37.204103+02:00", 25 | "tools": [ 26 | { 27 | "externalReferences": [ 28 | { 29 | "type": "website", 30 | "url": "https://github.com/Festo-se/cyclonedx-editor-validator" 31 | } 32 | ], 33 | "name": "cyclonedx-editor-validator", 34 | "vendor": "Festo SE & Co. KG", 35 | "version": "0.0.0" 36 | } 37 | ] 38 | }, 39 | "serialNumber": "urn:uuid:1859cebc-67fb-448a-ad66-d00aa5fab3bf", 40 | "version": 1, 41 | "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", 42 | "bomFormat": "CycloneDX", 43 | "specVersion": "1.6", 44 | "compositions": [ 45 | { 46 | "aggregate": "unknown", 47 | "assemblies": [] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /tests/integration/data/set.input.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": { 4 | "purl": "pkg:npm/test-app@1.0.0" 5 | }, 6 | "set": { 7 | "author": "Another author" 8 | } 9 | }, 10 | { 11 | "id": { 12 | "purl": "pkg:npm/test-app@1.0.0" 13 | }, 14 | "set": { 15 | "copyright": "2022 Acme Inc" 16 | } 17 | }, 18 | { 19 | "id": { 20 | "name": "depA", 21 | "group": "com.company.unit", 22 | "version": "4.0.2" 23 | }, 24 | "set": { 25 | "licenses": [ 26 | { 27 | "license": { 28 | "id": "MIT" 29 | } 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "id": { 36 | "name": "depC", 37 | "version": "3.2.1" 38 | }, 39 | "set": { 40 | "licenses": { 41 | "license": { 42 | "id": "Apache-2.0" 43 | } 44 | } 45 | } 46 | }, 47 | { 48 | "id": { 49 | "purl": "pkg:npm/test-app@1.0.0" 50 | }, 51 | "set": { 52 | "description": null 53 | } 54 | }, 55 | { 56 | "id": { 57 | "name": "x-ray", 58 | "group": "physics", 59 | "version": "18.9.5" 60 | }, 61 | "set": { 62 | "author": "New author", 63 | "licenses": null 64 | } 65 | }, 66 | { 67 | "id": { 68 | "name": "x-ray", 69 | "group": "physics", 70 | "version-range": "vers:generic/18.9.5" 71 | }, 72 | "set": { 73 | "licenses": [ 74 | { 75 | "license": { 76 | "id": "Apache-2.0" 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /tests/integration/data/set.expected_version-ranges.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "serialNumber": "urn:uuid:9b94319e-ec1c-441f-943d-75421a1d2040", 5 | "version": 2, 6 | "metadata": { 7 | "timestamp": "2024-07-04T09:43:38+00:00", 8 | "component": { 9 | "type": "application", 10 | "bom-ref": "acme-app", 11 | "name": "Acme_Application", 12 | "version": "9.1.1" 13 | }, 14 | "tools": [ 15 | { 16 | "name": "cyclonedx-editor-validator", 17 | "vendor": "Festo SE & Co. KG", 18 | "version": "0.0.0" 19 | } 20 | ] 21 | }, 22 | "components": [ 23 | { 24 | "bom-ref": "web-framework@1.0.0", 25 | "type": "library", 26 | "name": "web-framework", 27 | "version": "1.0.0" 28 | }, 29 | { 30 | "bom-ref": "web-framework@2.0.0", 31 | "type": "library", 32 | "name": "web-framework", 33 | "version": "2.0.0" 34 | }, 35 | { 36 | "bom-ref": "web-framework@2.0.1", 37 | "type": "library", 38 | "name": "web-framework", 39 | "version": "3.0.0", 40 | "copyright": "ACME Inc." 41 | }, 42 | { 43 | "bom-ref": "container@2.0.1", 44 | "type": "library", 45 | "name": "container", 46 | "version": "2.0.1", 47 | "components": [ 48 | { 49 | "bom-ref": "container@2.0.1|web-framework@2.0.1", 50 | "type": "library", 51 | "name": "web-framework", 52 | "version": "2.0.1", 53 | "copyright": "ACME Inc." 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /tests/integration/data/init-sbom.initial_default.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM." 5 | } 6 | ], 7 | "metadata": { 8 | "authors": [ 9 | { 10 | "name": "The person who created the SBOM." 11 | } 12 | ], 13 | "component": { 14 | "bom-ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM.", 15 | "copyright": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.", 16 | "name": "The name of the component described by the SBOM.", 17 | "supplier": { 18 | "name": "The name of the organization that supplied the component." 19 | }, 20 | "type": "application", 21 | "version": "The component version." 22 | }, 23 | "timestamp": "2024-10-15T22:15:37.204103+02:00", 24 | "tools": [ 25 | { 26 | "externalReferences": [ 27 | { 28 | "type": "website", 29 | "url": "https://github.com/Festo-se/cyclonedx-editor-validator" 30 | } 31 | ], 32 | "name": "cyclonedx-editor-validator", 33 | "vendor": "Festo SE & Co. KG", 34 | "version": "0.0.0" 35 | } 36 | ] 37 | }, 38 | "serialNumber": "urn:uuid:1859cebc-67fb-448a-ad66-d00aa5fab3bf", 39 | "version": 1, 40 | "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", 41 | "bomFormat": "CycloneDX", 42 | "specVersion": "1.6", 43 | "compositions": [ 44 | { 45 | "aggregate": "unknown", 46 | "assemblies": [] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /docs/source/usage/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Usage 3 | ============== 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :nosubcommands: 10 | 11 | .. toctree:: 12 | :caption: Available commands 13 | :maxdepth: 1 14 | :glob: 15 | 16 | * 17 | 18 | Exit codes 19 | ---------- 20 | 21 | As the tool should be used in CI/CD, it uses exit codes to indicate possible errors: 22 | 23 | - ``0`` = Success 24 | - ``2`` = Usage error, e.g., missing option, invalid argument, etc. 25 | - ``3`` = Generic application error. This can have various reasons ranging from invalid input files to bugs in our code. 26 | - ``4`` = *[Only for validate]* SBOM failed validation. 27 | 28 | Output 29 | ------ 30 | 31 | Some commands produce a new SBOM as output. By default, this output will be written to stdout but it can be written to a file, using the command's ``--output`` option. 32 | 33 | If the ``--output`` option is specified and set to an existing or non-existing file, the output is written there. If it points to a directory, the output will be written to a file with an auto-generated name in that directory. 34 | 35 | .. attention:: 36 | In both cases, existing files with the same name will be overwritten without warning. 37 | 38 | The filename is generated according to the template ``__.cdx.json``, where: 39 | 40 | - ```` is the name of the component in the SBOM's metadata. 41 | - ```` is the version of the component in the SBOM's metadata. 42 | - ```` is the timestamp in the SBOM's metadata or, if that doesn't exist, the current time. Either is converted to UTC and formatted as ``YYYYMMDDHHMMSS``. 43 | -------------------------------------------------------------------------------- /cdxev/amend/license.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from collections.abc import Callable 4 | 5 | 6 | def license_has_id(license: dict) -> bool: 7 | """ 8 | Returns ``True`` if ``license`` contains an SPDX id. 9 | 10 | :param license: A license object. 11 | :returns: ``True`` if ``license`` contains an SPDX id. 12 | """ 13 | return "id" in license 14 | 15 | 16 | def license_has_text(license: dict) -> bool: 17 | """ 18 | Returns ``True`` if ``license`` contains a non-empty text. 19 | 20 | :param license: A license object. 21 | :returns: ``True`` if ``license`` contains a non-empty text. 22 | """ 23 | return "text" in license and license["text"]["content"] 24 | 25 | 26 | def foreach_license(callable: Callable[[dict, dict], None], component: dict) -> None: 27 | """ 28 | Runs the given callable on every license contained in the given component. 29 | 30 | SPDX license expressions are not considered. Components declaring their licenses in this form 31 | are skipped. 32 | 33 | For every other license, ``callable`` is invoked with the license object (i.e., the object 34 | containing the ``id`` and ``name`` properties) and the component itself as arguments. 35 | 36 | :param callable: A callable object that can accept a license object as its first and the 37 | declaring component as its second argument. 38 | :param component: The component whose licenses to process. 39 | """ 40 | if "licenses" not in component: 41 | return 42 | 43 | for license_container in component["licenses"]: 44 | if "license" not in license_container: 45 | # We don't do anything with SPDX expressions 46 | continue 47 | 48 | license = license_container["license"] 49 | callable(license, component) 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_language_version: 4 | python: python3.11 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v5.0.0 8 | hooks: 9 | - id: trailing-whitespace 10 | args: [--markdown-linebreak-ext=md] 11 | - id: end-of-file-fixer 12 | exclude: ^tests/auxiliary/.* 13 | - id: check-yaml 14 | - id: check-json 15 | - id: pretty-format-json 16 | args: [--autofix, --no-sort-keys] 17 | - repo: https://github.com/psf/black-pre-commit-mirror 18 | rev: "24.1.1" 19 | hooks: 20 | - id: black 21 | - repo: https://github.com/PyCQA/flake8 22 | rev: "7.0.0" 23 | hooks: 24 | - id: flake8 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: "v1.13.0" 27 | hooks: 28 | - id: mypy 29 | # List type stub dependencies explicitly, as --install-types should be avoided as per 30 | # https://github.com/pre-commit/mirrors-mypy/blob/main/README.md 31 | additional_dependencies: 32 | - types-python-dateutil==2.9.0.20251115 33 | - typing-extensions==4.15.0 34 | - types-jsonschema==4.25.1.20251009 35 | - cyclonedx-python-lib==11.6.0 36 | - univers==31.1.0 37 | - charset-normalizer==3.4.4 38 | - natsort==8.4.0 39 | - docstring-parser==0.17.0 40 | files: "^cdxev/.*\\.py$" 41 | args: ["--config-file", "pyproject.toml"] 42 | - repo: https://github.com/PyCQA/bandit 43 | rev: "1.7.7" 44 | hooks: 45 | - id: bandit 46 | args: ["-c", "pyproject.toml"] 47 | additional_dependencies: ["bandit[toml]"] 48 | - repo: https://github.com/astral-sh/uv-pre-commit 49 | # uv version. 50 | rev: 0.8.3 51 | hooks: 52 | - id: uv-lock 53 | -------------------------------------------------------------------------------- /tests/test_spec_version.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import unittest 4 | 5 | from cdxev.auxiliary.sbomFunctions import CycloneDXVersion, SpecVersion 6 | 7 | 8 | class SpecVersionTestCase(unittest.TestCase): 9 | def test_parse(self): 10 | s = "1.2" 11 | parsed = SpecVersion.parse(s) 12 | 13 | self.assertEqual(parsed, CycloneDXVersion.V1_2) 14 | self.assertEqual(CycloneDXVersion.V1_2, parsed) 15 | self.assertEqual(s, str(parsed)) 16 | 17 | def test_compare_parsed(self): 18 | s1 = "1.2" 19 | s2 = "1.4" 20 | parsed1 = SpecVersion.parse(s1) 21 | parsed2 = SpecVersion.parse(s2) 22 | 23 | self.assertNotEqual(parsed1, parsed2) 24 | self.assertLess(parsed1, parsed2) 25 | self.assertGreater(parsed2, parsed1) 26 | 27 | def test_parse_blank(self): 28 | s = "" 29 | parsed = SpecVersion.parse(s) 30 | 31 | self.assertIsNone(parsed) 32 | 33 | def test_parse_no_major(self): 34 | s = ".3" 35 | parsed = SpecVersion.parse(s) 36 | 37 | self.assertIsNone(parsed) 38 | 39 | def test_parse_no_minor(self): 40 | s = "1." 41 | parsed = SpecVersion.parse(s) 42 | 43 | self.assertIsNone(parsed) 44 | 45 | def test_parse_no_dot(self): 46 | s = "15" 47 | parsed = SpecVersion.parse(s) 48 | 49 | self.assertIsNone(parsed) 50 | 51 | def test_parse_invalid(self): 52 | s = "f1.5" 53 | parsed = SpecVersion.parse(s) 54 | 55 | self.assertIsNone(parsed) 56 | 57 | def test_compare(self): 58 | self.assertLessEqual(CycloneDXVersion.V1_0, CycloneDXVersion.V1_0) 59 | self.assertEqual(CycloneDXVersion.V1_0, CycloneDXVersion.V1_0) 60 | self.assertLess(CycloneDXVersion.V1_0, CycloneDXVersion.V1_1) 61 | self.assertGreater(CycloneDXVersion.V1_1, CycloneDXVersion.V1_0) 62 | -------------------------------------------------------------------------------- /.github/workflows/python-test-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Pre-release Python Package 10 | 11 | on: 12 | release: 13 | types: [prereleased] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 24 | environment: 25 | name: testpypi 26 | url: https://test.pypi.org/project/cyclonedx-editor-validator/ 27 | steps: 28 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 29 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 30 | with: 31 | python-version: '3.10' 32 | - name: Install uv 33 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 34 | with: 35 | enable-cache: true 36 | version: "0.8.3" 37 | - name: Install dependencies and set version from release tag 38 | run: | 39 | uv version ${{ github.ref_name }} 40 | uv sync --no-dev --frozen 41 | - name: Build package 42 | run: | 43 | uv version --dry-run 44 | uv build 45 | - name: Publish package distributions to TestPyPI 46 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 47 | with: 48 | repository-url: https://test.pypi.org/legacy/ 49 | attestations: false 50 | -------------------------------------------------------------------------------- /tests/integration/data/vex.expected_list_default.csv: -------------------------------------------------------------------------------- 1 | ID|RefID|CWEs|CVSS-Severity|Status|Published|Updated|Description 2 | CVE-1012-0001|-|-|CVSSv31:9.8(critical),CVSSv2:7.5(high)|exploitable|1012-01-01T01:01Z|1012-02-04T12:24Z|some description of a vulnerability 3 | CVE-1013-0002|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:02Z|1013-03-02T12:24Z|some description of a vulnerability 2 4 | CVE-1013-0003|-|-|CVSSv31:7.2(high)|exploitable|1013-01-01T01:02Z|1013-03-02T12:24Z|some description of a vulnerability 3 5 | CVE-1013-0004|-|-|CVSSv31:7.2(high)|exploitable|1013-01-01T01:02Z|1013-03-02T12:24Z|some description of a vulnerability 4 6 | CVE-1013-0005|-|-|CVSSv31:9.8(critical)|not_affected|1013-01-01T01:03Z|1013-03-03T12:24Z|some description of a vulnerability 5 7 | CVE-1013-0006|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:03Z|1013-03-04T12:24Z|some description of a vulnerability 5 8 | CVE-1012-0007|-|-|CVSSv31:9.8(critical),CVSSv2:7.5(high)|exploitable|1012-01-01T01:04Z|1013-03-05T12:24Z|some description of a vulnerability 6 9 | CVE-1013-0008|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:05Z|1013-03-07T12:24Z|some description of a vulnerability 7 10 | CVE-1013-0009|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:05Z|1013-03-07T12:24Z|some description of a vulnerability 8 11 | CVE-1013-0010|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:06Z|1013-03-08T12:24Z|some description of a vulnerability 9 12 | CVE-1013-0011|-|-|CVSSv31:7.2(high)|not_affected|1013-01-01T01:06Z|1013-03-09T12:24Z|some description of a vulnerability 10 13 | CVE-1012-0012|-|-|CVSSv31:9.8(critical),CVSSv2:7.5(high)|exploitable|1012-01-01T01:07Z|1013-03-10T12:24Z|some description of a vulnerability 11 14 | CVE-1012-0013|-|-|CVSSv31:5.3(medium),CVSSv2:5.0(medium)|not_affected|1012-01-01T01:08Z|1013-03-12T12:24Z|some description of a vulnerability 12 15 | CVE-1012-0014|-|-|CVSSv31:5.3(medium),CVSSv2:5.0(medium)|exploitable|1012-01-01T01:07Z|1013-03-12T12:24Z|some description of a vulnerability 13 16 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import unittest 4 | import unittest.mock 5 | from pathlib import Path 6 | 7 | # noinspection PyProtectedMember 8 | from cdxev.__main__ import InputFileError, load_json, load_xml, read_sbom 9 | 10 | 11 | class TestSupplements(unittest.TestCase): 12 | @unittest.mock.patch("pathlib.Path.is_file") 13 | def test_read_sbom(self, mock_is_file: unittest.mock.Mock) -> None: 14 | with self.assertRaises(InputFileError) as ie: 15 | mock_is_file.return_value = False 16 | read_sbom(Path("test.json")) 17 | self.assertEqual("File not found: test.json", ie.exception.details.description) 18 | with self.assertRaises(InputFileError) as ie: 19 | mock_is_file.return_value = True 20 | read_sbom(Path("test.jason")) 21 | self.assertIn( 22 | "Failed to guess file type from extension", ie.exception.details.description 23 | ) 24 | with unittest.mock.patch("cdxev.__main__.load_json", return_value={}): 25 | mock_is_file.return_value = True 26 | result = read_sbom(Path("test.json"))[0] 27 | self.assertEqual(result, {}) 28 | 29 | @unittest.mock.patch( 30 | "pathlib.Path.open", unittest.mock.mock_open(read_data="not a json") 31 | ) 32 | def test_load_json(self) -> None: 33 | with unittest.mock.patch("json.load", return_value={"sbom": []}): 34 | result = load_json(Path("test.json")) 35 | self.assertDictEqual({"sbom": []}, result) 36 | with self.assertRaises(InputFileError) as ie: 37 | load_json(Path("not_a_json.json")) 38 | self.assertEqual("Invalid JSON", ie.exception.details.description) 39 | 40 | def test_load_xml(self) -> None: 41 | with self.assertRaises(InputFileError) as ie: 42 | load_xml(Path("test.xml")) 43 | self.assertIn("XML files aren't supported", ie.exception.details.description) 44 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import unittest 4 | 5 | import cdxev.error as err 6 | import cdxev.log as log 7 | 8 | 9 | class AppErrorTestCase(unittest.TestCase): 10 | def test_create_error(self): 11 | exc = err.AppError("Foo", "bar") 12 | 13 | self.assertIsInstance(exc.details, log.LogMessage) 14 | self.assertEqual(exc.details.message, "Foo") 15 | self.assertEqual(exc.details.description, "bar") 16 | 17 | def test_create_error_without_args_raises(self): 18 | with self.assertRaisesRegex( 19 | ValueError, "Either log_msg or message and description must be passed" 20 | ): 21 | err.AppError() 22 | 23 | with self.assertRaisesRegex( 24 | ValueError, "Either log_msg or message and description must be passed" 25 | ): 26 | err.AppError("Foo") 27 | 28 | def test_create_error_from_log(self): 29 | exc = err.AppError( 30 | log_msg=log.LogMessage( 31 | message="Foo", description="bar", module_name="module", line_start=0 32 | ) 33 | ) 34 | self.assertIsInstance(exc.details, log.LogMessage) 35 | self.assertEqual(exc.details.message, "Foo") 36 | self.assertEqual(exc.details.description, "bar") 37 | self.assertEqual(exc.details.module_name, "module") 38 | self.assertEqual(exc.details.line_start, 0) 39 | 40 | exc = err.AppError( 41 | message="Foo", 42 | description="bar", 43 | module_name="module", 44 | line_start=0, 45 | log_msg=log.LogMessage(message="test", description="test2"), 46 | ) 47 | self.assertIsInstance(exc.details, log.LogMessage) 48 | self.assertEqual(exc.details.message, "Foo") 49 | self.assertEqual(exc.details.description, "bar") 50 | self.assertEqual(exc.details.module_name, "module") 51 | self.assertEqual(exc.details.line_start, 0) 52 | -------------------------------------------------------------------------------- /tests/auxiliary/test_merge_sboms/governing_program.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-10-17T19:15:49", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "application", 14 | "bom-ref": "governing_program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.governing", 19 | "name": "governing_program", 20 | "version": "T5.0.3.96", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "name": "company internal" 25 | } 26 | } 27 | ], 28 | "copyright": "Company Legal 2022, all rights reserved" 29 | } 30 | }, 31 | "components": [ 32 | { 33 | "type": "library", 34 | "bom-ref": "sub_program", 35 | "supplier": { 36 | "name": "Company Legal" 37 | }, 38 | "group": "com.company.governing", 39 | "name": "sub_program", 40 | "copyright": "Company Legal 2022, all rights reserved", 41 | "version": "T5.0.3.96" 42 | }, 43 | { 44 | "type": "library", 45 | "bom-ref": "gp_first_component-copy", 46 | "supplier": { 47 | "name": "The first component Contributors" 48 | }, 49 | "name": "gp_first_component", 50 | "version": "2.24.0", 51 | "licenses": [ 52 | { 53 | "license": { 54 | "id": "Apache-2.0" 55 | } 56 | } 57 | ] 58 | } 59 | ], 60 | "dependencies": [ 61 | { 62 | "ref": "governing_program", 63 | "dependsOn": [ 64 | "sub_program", 65 | "gp_first_component-copy" 66 | ] 67 | }, 68 | { 69 | "ref": "sub_program", 70 | "dependsOn": [] 71 | }, 72 | { 73 | "ref": "gp_first_component-copy", 74 | "dependsOn": [] 75 | } 76 | ], 77 | "compositions": [ 78 | { 79 | "aggregate": "incomplete", 80 | "assemblies": [ 81 | "sub_program", 82 | "gp_first_component-copy" 83 | ] 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | At Festo, we take the security of our GitHub repositories seriously. We encourage the community to participate in identifying and reporting any security vulnerabilities or concerns they may come across. To ensure a smooth and coordinated process, we have established the following guidelines for reporting security issues: 4 | 5 | ## Reporting a Security Issue 6 | 7 | If you discover a security vulnerability or any other security-related concern, please do not hesitate to reach out to us. We appreciate your responsible disclosure and will work with you to address the issue promptly. 8 | 9 | To report a security issue, please visit our website at [https://festo.com/psirt](https://festo.com/psirt) and follow the instructions provided. Our Product Security Incident Response Team (PSIRT) will review your report and respond accordingly. 10 | 11 | ## Responsible Disclosure 12 | 13 | We kindly request that you adhere to responsible disclosure practices when reporting security issues to us. This includes: 14 | 15 | 1. **Providing Sufficient Information**: Please provide detailed information about the vulnerability or concern you have identified. This will help our PSIRT team understand and address the issue more effectively. 16 | 17 | 2. **Giving Us Time to Respond**: We appreciate your patience while our PSIRT team investigates and resolves the reported issue. We will make every effort to keep you informed of our progress. We aim to process your report within one week, and give you a first response within two weeks. For critical vulnerabilities we aim to resolve them within 90 days. 18 | 19 | 3. **Not Publicly Disclosing the Issue**: To protect the security of our users, we request that you do not publicly disclose the reported issue until we have had an opportunity to address it. 20 | 21 | ## Conclusion 22 | 23 | We appreciate your assistance in keeping our repository secure. By working together, we can promptly address any security issues and ensure the safety of our users and their data. Thank you for your support! 24 | 25 | Please note that this security policy is subject to change without prior notice. For the most up-to-date information regarding vulnerability disclosure at Festo, please refer to our website at [https://festo.com/psirt](https://festo.com/psirt). 26 | -------------------------------------------------------------------------------- /tests/integration/data/list-command.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "list_components_csv": "Name,Version,Supplier\nAcme_Application,9.1.1,Company Legal\nsome_name,T4.0.1.30,somecompany SE & Co.KG\ncard-verifier,1.0.2,Acme, Inc.\ntomcat-catalina,9.0.14,Acme, Inc.\ncommon-util,3.0.0,Acme, Inc.\nutil,2.0.0,Example, Inc.\npersistence,3.1.0,Acme, Inc.\nComponent index 1,1.0.0,Acme, Inc.\nweb-framework,1.0.0,Acme, Inc.\nsub_web-framework,1.0.0,Acme, Inc.\nlicense and copyright less component", 3 | "list_licenses_csv": "Name,Copyright,Licenses\nAcme_Application,Company Legal 2022, all rights reserved\nsome_name,Company Legal 2022, all rights reserved,Apache-2.0\ncard-verifier,EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0,A licenses name of card verifier\ntomcat-catalina,Copyright tomcat-catalina,Apache-1.0,Apache-2.0\ncommon-util,BSD-3-Clause\nutil,Example, Inc. Commercial License,Apache-1.0\npersistence,Apache-2.0\nComponent index 1,(CDDL-1.0 OR GPL-2.0-with-classpath-exception)\nweb-framework,Apache-1.0\nsub_web-framework,Apache-1.0,Apache-2.0,Apache-3.0\nlicense and copyright less component", 4 | "list_components_txt": "Acme_Application\n9.1.1\nCompany Legal\n\nThis product includes material developed by third parties:\n\nsome_name\nT4.0.1.30\nsomecompany SE & Co.KG\n\ncard-verifier\n1.0.2\nAcme, Inc.\n\ntomcat-catalina\n9.0.14\nAcme, Inc.\n\ncommon-util\n3.0.0\nAcme, Inc.\n\nutil\n2.0.0\nExample, Inc.\n\npersistence\n3.1.0\nAcme, Inc.\n\nComponent index 1\n1.0.0\nAcme, Inc.\n\nweb-framework\n1.0.0\nAcme, Inc.\n\nsub_web-framework\n1.0.0\nAcme, Inc.\n\nlicense and copyright less component", 5 | "list_licenses_txt": "Acme_Application:\nCompany Legal 2022, all rights reserved\n\nThis product includes material developed by third parties:\n\nsome_name:\nCompany Legal 2022, all rights reserved\nApache-2.0\n\ncard-verifier:\nEPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\nA licenses name of card verifier\n\ntomcat-catalina:\nCopyright tomcat-catalina\nApache-1.0\nApache-2.0\n\ncommon-util:\nBSD-3-Clause\n\nutil:\nExample, Inc. Commercial License\nApache-1.0\n\npersistence:\nApache-2.0\n\nComponent index 1:\n(CDDL-1.0 OR GPL-2.0-with-classpath-exception)\n\nweb-framework:\nApache-1.0\n\nsub_web-framework:\nApache-1.0\nApache-2.0\nApache-3.0\n\nlicense and copyright less component:\nNo license or copyright information available.\n\n" 6 | } -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import json 4 | import sys 5 | from dataclasses import dataclass 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from tests.integration.test_integration import TestValidate 11 | 12 | 13 | @dataclass 14 | class SbomFixture: 15 | input: Path 16 | expected: Path 17 | expected_json: dict 18 | 19 | 20 | @pytest.fixture(scope="package") 21 | def data_dir(): 22 | """Path to the folder where test fixture data is stored.""" 23 | return _data_dir() 24 | 25 | 26 | def _data_dir(): 27 | return Path(__file__).parent / "data" 28 | 29 | 30 | @pytest.fixture 31 | def argv(monkeypatch): 32 | """ 33 | A function which patches argv to return the specified program arguments. 34 | 35 | argv[0] is automatically prepended and must not be passed to the function. 36 | """ 37 | 38 | def _argv(*argv: str): 39 | monkeypatch.setattr(sys, "argv", [__name__, *argv]) 40 | 41 | return _argv 42 | 43 | 44 | def pytest_generate_tests(metafunc: pytest.Metafunc): 45 | """ 46 | Pytest hook to dynamically generate tests. Called during test collection phase. 47 | """ 48 | 49 | # When collecting the validate test function, generate a test for each input file 50 | # in the data/validate directory. 51 | if metafunc.function == TestValidate.test: 52 | path = _data_dir() 53 | sboms = list(path.glob("validate/**/*.cdx.json")) 54 | 55 | # Some invalid SBOMs might come with a companion json file that contains 56 | # an array of error messages which are expected in the command output. 57 | expected_errors = {} 58 | for sbom in sboms: 59 | errors_file = sbom.with_suffix(".json.errors") 60 | if not errors_file.is_file(): 61 | continue 62 | 63 | with errors_file.open() as f: 64 | expected_errors[sbom] = json.load(f) 65 | 66 | metafunc.parametrize( 67 | "expected_result,schema_type,input,expected_errors", 68 | [ 69 | (x.parent.parent.name, x.parent.name, x, expected_errors.get(x)) 70 | for x in sboms 71 | ], 72 | ids=lambda item: item.name if isinstance(item, Path) else None, 73 | ) 74 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | from typing import Any 10 | 11 | import sphinx 12 | import sphinx.application 13 | from sphinx.ext import autodoc 14 | 15 | from cdxev import pkg 16 | 17 | project = "CycloneDX Editor Validator Tool" 18 | copyright = "2024, Festo SE & Co. KG" 19 | release = pkg.VERSION 20 | 21 | # -- General configuration --------------------------------------------------- 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 23 | 24 | extensions = [ 25 | "sphinxarg.ext", 26 | "sphinx_rtd_theme", 27 | "sphinx.ext.autosectionlabel", 28 | "sphinx.ext.autodoc", 29 | ] 30 | 31 | templates_path = ["_templates"] 32 | 33 | # Make sure the target is unique 34 | autosectionlabel_prefix_document = True 35 | 36 | # Prevents double-dashes being converted to en-dashes in argparse output. 37 | smartquotes_action = "qe" 38 | 39 | # -- Options for HTML output ------------------------------------------------- 40 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 41 | 42 | html_theme = "sphinx_rtd_theme" 43 | html_css_files = [ 44 | "css/festo-web-essentials.css", 45 | "https://www.festo.com/fonts/fonts.css", 46 | ] 47 | 48 | 49 | class OperationDocumenter(autodoc.MethodDocumenter): 50 | """ 51 | Extract docstring only for amend operations. 52 | see https://stackoverflow.com/a/7832437/5726546 53 | """ 54 | 55 | objtype = "operation" 56 | 57 | content_indent = "" 58 | 59 | @classmethod 60 | def can_document_member( 61 | cls, member: Any, membername: str, isattr: bool, parent: Any 62 | ) -> bool: 63 | return False 64 | 65 | # do not add a header to the docstring 66 | def add_directive_header(self, sig: str) -> None: 67 | pass 68 | 69 | 70 | # Register OperationDocumenter to be used in docs 71 | def setup(app: sphinx.application.Sphinx) -> None: 72 | app.add_autodocumenter(OperationDocumenter) 73 | -------------------------------------------------------------------------------- /cdxev/amend/command.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | import typing as t 5 | 6 | from cdxev.auxiliary.sbomFunctions import walk_components 7 | 8 | from .operations import Operation 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def get_all_operations() -> list[type[Operation]]: 14 | return Operation.__subclasses__() 15 | 16 | 17 | def create_operations( 18 | operations: list[type[Operation]], config: dict[type[Operation], dict[str, t.Any]] 19 | ) -> list["Operation"]: 20 | instances = [] 21 | for op in operations: 22 | options = config.get(op, {}) 23 | instances.append(op(**options)) 24 | 25 | return instances 26 | 27 | 28 | def run( 29 | sbom: dict, 30 | selected: t.Optional[list[type[Operation]]] = None, 31 | config: dict[type[Operation], dict[str, t.Any]] = {}, 32 | ) -> None: 33 | """ 34 | Runs the amend command on an SBOM. The SBOM is modified in-place. 35 | 36 | :param dict sbom: The SBOM model. 37 | :param selected: List of operation classes to run on the SBOM. 38 | :param config: Arguments for the operations. They will be passed to the operation's 39 | __init__() method as kw-args. 40 | """ 41 | # If no operations are selected, select the default operations. 42 | if not selected: 43 | selected = [op for op in get_all_operations() if hasattr(op, "_amendDefault")] 44 | 45 | operations = create_operations(selected, config) 46 | 47 | _prepare(operations, sbom) 48 | _metadata(operations, sbom) 49 | walk_components(sbom, _do_amend, operations, skip_meta=True) 50 | 51 | 52 | def _prepare(operations: list[Operation], sbom: dict) -> None: 53 | for operation in operations: 54 | operation.prepare(sbom) 55 | 56 | 57 | def _metadata(operations: list[Operation], sbom: dict) -> None: 58 | if "metadata" not in sbom: 59 | return 60 | 61 | logger.debug("Processing metadata") 62 | metadata = sbom["metadata"] 63 | for operation in operations: 64 | operation.handle_metadata(metadata) 65 | 66 | 67 | def _do_amend(component: dict, operations: list[Operation]) -> None: 68 | for operation in operations: 69 | logger.debug( 70 | "Processing component %s", (component.get("bom-ref", "")) 71 | ) 72 | operation.handle_component(component) 73 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Dependabot pre-commit updater 2 | on: pull_request 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: github.actor == 'dependabot[bot]' 11 | steps: 12 | - name: Dependabot metadata 13 | id: metadata 14 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0 15 | with: 16 | github-token: "${{ secrets.GITHUB_TOKEN }}" 17 | - name: Set up SSH 18 | uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 19 | with: 20 | ssh-private-key: ${{ secrets.COMMIT_KEY }} 21 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 22 | with: 23 | # Check out HEAD of source branch. Without this option, the merge commit would be checked out. 24 | ref: ${{ github.event.pull_request.head.ref }} 25 | - name: Update .pre-commit-config.yaml 26 | id: update-pre-commit 27 | run: | 28 | # Reduces the output of the metadata step above to a simpler JSON format: 29 | # { "": "", ... } 30 | export updates=$(yq --null-input 'env(updates) | .[] as $item ireduce ({}; .[$item | .dependencyName] = ($item | .newVersion))' -o json) 31 | 32 | # Goes through the dependencies of the mypy hook in pre-commit config and if any of them have 33 | # been updated in this PR, replace the version number with the new version 34 | yq --inplace 'env(updates) as $up | (.repos[].hooks[] | select(.id == "mypy").additional_dependencies[] | select(split("==") | .[0] as $pkg | $up | has ($pkg))) |= (split("==") | .[0] as $pkg | $pkg + "==" + $up[$pkg])' .pre-commit-config.yaml 35 | 36 | # Commit the changes 37 | git add .pre-commit-config.yaml 38 | if ! git diff-index --cached --quiet HEAD -- 39 | then 40 | echo "::notice::Committing changes" 41 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 42 | git config --local user.name "github-actions[bot]" 43 | git remote set-url origin "git@github.com:${{ github.repository }}.git" 44 | git commit -m "Update additional dependencies for mypy pre-commit hook" 45 | git push 46 | else 47 | echo "::notice::No changes made" 48 | fi 49 | env: 50 | updates: ${{ steps.metadata.outputs.updated-dependencies-json }} 51 | -------------------------------------------------------------------------------- /docs/source/usage/list.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | list 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: list 10 | 11 | This command lists content of the SBOM. It can currently provide a list: 12 | 13 | * of the license information in the SBOM using the ``licenses`` operation, 14 | * of the components in the SBOM using the ``components`` operation. 15 | 16 | The information can be displayed as a text file or in csv format. 17 | 18 | 19 | Output Format 20 | ------------- 21 | 22 | The txt format for license information (derived from the format of `Apache NOTICE files `_) has the structure: :: 23 | 24 | Metadata component name: 25 | Metadata component copyright 26 | Metadata component license 1 27 | Metadata component license 2 28 | ... 29 | 30 | This product includes material developed by third parties: 31 | 32 | component 1 name: 33 | component 1 copyright 34 | component 1 license 1 35 | component 1 license 1 36 | ... 37 | 38 | component 2 name: 39 | component 2 copyright 40 | component 2 license 1 41 | component 2 license 2 42 | ... 43 | 44 | 45 | The txt format for component information has the structure: :: 46 | 47 | Metadata component name 48 | Metadata component version 49 | Metadata component supplier name 50 | 51 | This product includes material developed by third parties: 52 | 53 | component 1 name 54 | component 1 version 55 | component 1 supplier name 56 | 57 | ... 58 | 59 | 60 | The csv format for license information has the structure: :: 61 | 62 | Name,Copyright,Licenses 63 | "Metadata component name","Metadata component copyright","Metadata component license 1;..." 64 | "component 1 name","component 1 copyright","component 1 license 1;component 1 license 2..." 65 | "component 2 name","component 2 copyright","" 66 | ... 67 | 68 | 69 | The csv format for component information has the structure: :: 70 | 71 | Name,Version,Supplier 72 | "Metadata component name","Metadata component version","Metadata component supplier name" 73 | "component 1 name","component 1 version","component 1 supplier name" 74 | "component 2 name","","component 2 supplier name" 75 | ... 76 | 77 | 78 | Examples:: 79 | 80 | # List the license information from bom.json 81 | cdx-ev list licenses bom.json 82 | 83 | # List the components from bom.json 84 | cdx-ev list components bom.json 85 | -------------------------------------------------------------------------------- /.github/workflows/bump_spdx_schema_version.yaml: -------------------------------------------------------------------------------- 1 | name: Check & Bump SPDX Schema Version 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * 1" # run at 03:00 on Mondays 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | check-spdx-schema-version: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | steps: 19 | - name: Set up SSH 20 | uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 21 | with: 22 | ssh-private-key: ${{ secrets.COMMIT_KEY }} 23 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 24 | with: 25 | # Check out HEAD of source branch. Without this option, the merge commit would be checked out. 26 | ref: ${{ github.event.pull_request.head.ref }} 27 | 28 | - name: Fetch and bump remote spdx.schema.json 29 | id: check_version 30 | run: | 31 | curl -sSL https://cyclonedx.org/schema/spdx.schema.json -o remote_spdx.schema.json 32 | remote_version=$(jq -r '."$comment"' remote_spdx.schema.json) 33 | echo "remote_version=$remote_version" >> $GITHUB_OUTPUT 34 | local_version=$(jq -r '."$comment"' cdxev/auxiliary/schema/spdx.schema.json) 35 | echo "local version is: $local_version" 36 | # Remove "v" prefix 37 | remote_version_norm=${remote_version#v} 38 | local_version_norm=${local_version#v} 39 | # Replace hyphen with dot to make semantic components comparable 40 | remote_sanitized=${remote_version_norm//-/\.} 41 | local_sanitized=${local_version_norm//-/\.} 42 | higher_version=$(printf "%s\n%s\n" "$remote_sanitized" "$local_sanitized" | sort -V | tail -n1) 43 | 44 | if [ "$higher_version" = "$remote_sanitized" ] && [ "$remote_sanitized" != "$local_sanitized" ]; then 45 | echo "update_list=true" >> $GITHUB_OUTPUT 46 | mv remote_spdx.schema.json cdxev/auxiliary/schema/spdx.schema.json 47 | else 48 | echo "update_list=false" >> $GITHUB_OUTPUT 49 | fi 50 | 51 | - name: Create Pull Request if schema was updated 52 | if: steps.check_version.outputs.update_list == 'true' 53 | uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 54 | with: 55 | commit-message: "chore: bump SPDX schema to ${{ steps.check_version.outputs.remote_version }}" 56 | branch: bump-spdx-schema-version-${{ steps.update_schema.outputs.remote_version }} 57 | title: "chore: bump SPDX schema version to ${{ steps.check_version.outputs.remote_version }}" 58 | body: | 59 | This PR updates the `spdx.schema.json` to the latest version `${{ steps.check_version.outputs.remote_version }}` 60 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [released] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 24 | environment: 25 | name: pypi 26 | url: https://pypi.org/project/cyclonedx-editor-validator/ 27 | steps: 28 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 29 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 30 | with: 31 | python-version: '3.10' 32 | - name: Install uv 33 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 34 | with: 35 | enable-cache: true 36 | version: "0.8.3" 37 | - name: Install dependencies and set version from release tag 38 | run: | 39 | uv version ${{ github.ref_name }} 40 | uv sync --no-dev --frozen 41 | - name: Build package 42 | run: | 43 | uv version --dry-run 44 | uv build 45 | - name: Publish package on PyPI 46 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 47 | with: 48 | attestations: false 49 | 50 | deploy-pages: 51 | 52 | runs-on: ubuntu-latest 53 | permissions: 54 | contents: write # required for pushing the pages branch 55 | steps: 56 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 57 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 58 | with: 59 | python-version: '3.10' 60 | - name: Install uv 61 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 62 | with: 63 | enable-cache: true 64 | version: "0.8.3" 65 | - name: Build HTML 66 | run: uv run sphinx-build -a -E docs/source/ docs/build 67 | - name: Upload artifacts 68 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 69 | with: 70 | name: html-docs 71 | path: docs/build 72 | - name: Deploy pages 73 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 74 | with: 75 | github_token: ${{ secrets.GITHUB_TOKEN }} 76 | publish_dir: docs/build 77 | -------------------------------------------------------------------------------- /tests/integration/data/merge-from-folder/merge.input_1.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.5", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-10-17T19:15:49", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "application", 14 | "bom-ref": "governing_program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.governing", 19 | "name": "governing_program", 20 | "version": "T5.0.3.96", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "name": "company internal" 25 | } 26 | } 27 | ], 28 | "copyright": "Company Legal 2022, all rights reserved" 29 | } 30 | }, 31 | "components": [ 32 | { 33 | "type": "library", 34 | "bom-ref": "sub_program", 35 | "supplier": { 36 | "name": "Company Legal" 37 | }, 38 | "group": "com.company.governing", 39 | "name": "sub_program", 40 | "copyright": "Company Legal 2022, all rights reserved", 41 | "version": "T5.0.3.96" 42 | }, 43 | { 44 | "type": "library", 45 | "bom-ref": "gp_first_component-copy", 46 | "supplier": { 47 | "name": "The first component Contributors" 48 | }, 49 | "name": "gp_first_component", 50 | "version": "2.24.0", 51 | "licenses": [ 52 | { 53 | "license": { 54 | "id": "Apache-2.0" 55 | } 56 | } 57 | ] 58 | } 59 | ], 60 | "dependencies": [ 61 | { 62 | "ref": "governing_program", 63 | "dependsOn": [ 64 | "sub_program", 65 | "gp_first_component-copy" 66 | ] 67 | }, 68 | { 69 | "ref": "sub_program", 70 | "dependsOn": [] 71 | }, 72 | { 73 | "ref": "gp_first_component-copy", 74 | "dependsOn": [] 75 | } 76 | ], 77 | "compositions": [ 78 | { 79 | "aggregate": "incomplete", 80 | "assemblies": [ 81 | "sub_program", 82 | "gp_first_component-copy" 83 | ] 84 | } 85 | ], 86 | "vulnerabilities": [ 87 | { 88 | "description": "The application is vulnerable to remote SQL injection and shell upload", 89 | "id": "Vul 1", 90 | "ratings": [ 91 | { 92 | "score": 9.8, 93 | "severity": "critical", 94 | "method": "CVSSv31", 95 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 96 | }, 97 | { 98 | "score": 7.5, 99 | "severity": "high", 100 | "method": "CVSSv2", 101 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 102 | } 103 | ], 104 | "affects": [ 105 | { 106 | "ref": "sub_program" 107 | } 108 | ] 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import io 4 | import logging 5 | import unittest 6 | from unittest.mock import Mock, patch 7 | 8 | import cdxev.log as log 9 | 10 | 11 | class LogConfigTestCase(unittest.TestCase): 12 | @patch(f"{log.__name__}.logging") 13 | def test_configure_logging_not_quiet_not_verbose(self, m_logging): 14 | m_logger = Mock(logging.Logger) 15 | m_logging.getLogger.return_value = m_logger 16 | log.configure_logging(False, False) 17 | 18 | m_logger.addHandler.assert_called_once() 19 | m_logger.setLevel.assert_called_once_with(m_logging.INFO) 20 | 21 | @patch(f"{log.__name__}.logging") 22 | def test_configure_logging_verbose(self, m_logging): 23 | m_logger = Mock(logging.Logger) 24 | m_logging.getLogger.return_value = m_logger 25 | log.configure_logging(False, True) 26 | 27 | m_logger.addHandler.assert_called_once() 28 | m_logger.setLevel.assert_called_once_with(m_logging.DEBUG) 29 | 30 | @patch(f"{log.__name__}.logging") 31 | def test_configure_logging_quiet(self, m_logging): 32 | m_logger = Mock(logging.Logger) 33 | m_logging.getLogger.return_value = m_logger 34 | log.configure_logging(True, False) 35 | 36 | m_logger.addHandler.assert_not_called() 37 | 38 | @patch(f"{log.__name__}.logging") 39 | def test_configure_logging_quiet_and_verbose(self, m_logging): 40 | m_logger = Mock(logging.Logger) 41 | m_logging.getLogger.return_value = m_logger 42 | log.configure_logging(True, True) 43 | 44 | m_logger.addHandler.assert_not_called() 45 | 46 | 47 | class LogFormatterTestCase(unittest.TestCase): 48 | def setUp(self) -> None: 49 | formatter = log.LogMessageFormatter() 50 | self.log_stream = io.StringIO() 51 | handler = logging.StreamHandler(self.log_stream) 52 | handler.setFormatter(formatter) 53 | self.logger = logging.getLogger(__name__) 54 | self.logger.addHandler(handler) 55 | self.logger.setLevel(logging.DEBUG) 56 | 57 | def test_format_full(self): 58 | msg_obj = log.LogMessage("message", "description", "module", 10) 59 | self.logger.info(msg_obj) 60 | msg = self.log_stream.getvalue() 61 | expected = "INFO: message (component: module at line 10) - description\n" 62 | self.assertEqual(expected, msg) 63 | 64 | def test_format_without_line(self): 65 | msg_obj = log.LogMessage("message", "description", "module", None) 66 | self.logger.info(msg_obj) 67 | msg = self.log_stream.getvalue() 68 | expected = "INFO: message (component: module) - description\n" 69 | self.assertEqual(expected, msg) 70 | 71 | def test_format_without_module(self): 72 | msg_obj = log.LogMessage("message", "description", None, 10) 73 | self.logger.info(msg_obj) 74 | msg = self.log_stream.getvalue() 75 | expected = "INFO: message (at line 10) - description\n" 76 | self.assertEqual(expected, msg) 77 | -------------------------------------------------------------------------------- /cdxev/error.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | from typing import Optional 4 | 5 | from cdxev.log import LogMessage 6 | 7 | 8 | class AppError(Exception): 9 | """Parent class of all custom exceptions.""" 10 | 11 | def __init__( 12 | self, 13 | message: Optional[str] = None, 14 | description: Optional[str] = None, 15 | module_name: Optional[str] = None, 16 | line_start: Optional[int] = None, 17 | *, 18 | log_msg: Optional[LogMessage] = None, 19 | ): 20 | """ 21 | :param str message: A short message, often not even a full sentence. Avoid putting a 22 | period at the end. 23 | :param str description: A longer description of the error. 24 | :param str module_name: The module where the error occurred. Typically set to `__name__`. 25 | in the SBOM file, this can be the path to the SBOM. 26 | :param line_start: The line where the error occurred in *file_name*. 27 | :type line_start: int or None 28 | :param log_msg: A LogMessage object can directly be passed to the constructor. If any of 29 | the other arguments are passed, this LogMessage's respective field will be overwritten 30 | with the argument. 31 | """ 32 | super().__init__() 33 | if log_msg is not None: 34 | if message is not None: 35 | log_msg.message = message 36 | if description is not None: 37 | log_msg.description = description 38 | if module_name is not None: 39 | log_msg.module_name = module_name 40 | if line_start is not None: 41 | log_msg.line_start = line_start 42 | self.details = log_msg 43 | elif message is None or description is None: 44 | raise ValueError( 45 | "Either log_msg or message and description must be passed." 46 | ) 47 | else: 48 | self.details = LogMessage( 49 | message, 50 | description, 51 | module_name, 52 | line_start, 53 | ) 54 | 55 | def __str__(self) -> str: 56 | return str(self.details) 57 | 58 | 59 | class InputFileError(AppError): 60 | """Indicates an error while loading an input.""" 61 | 62 | def __init__( 63 | self, 64 | description: str, 65 | module_name: Optional[str] = None, 66 | line_start: Optional[int] = None, 67 | ): 68 | """ 69 | :param str description: A description of the error. 70 | :param str module_name: The module where the error occurred. Typically set to `__name__`. 71 | in the SBOM file, this can be the path to the SBOM. 72 | :param line_start: The line where the error occurred in *file_name*. 73 | :type line_start: int or None 74 | """ 75 | super().__init__( 76 | "Failed to load input file", 77 | description, 78 | module_name, 79 | line_start, 80 | ) 81 | -------------------------------------------------------------------------------- /docs/source/usage/amend.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | amend 3 | ===== 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: amend 10 | 11 | .. note:: 12 | The order of operations cannot be controlled. If you want to ensure two operations run in a certain order you must run the command twice, each time with a different set of operations. 13 | 14 | Examples 15 | -------- 16 | 17 | .. code:: bash 18 | 19 | # Run all default operations on an SBOM. 20 | cdx-ev amend bom.json 21 | 22 | # Run only the default-author and add-bom-ref operations. 23 | cdx-ev amend --operation default-author --operation add-bom-ref bom.json 24 | 25 | # Run the add-license-text operation. License texts are stored in a directory named 'license_texts'. 26 | # Afterwards, run the delete-ambiguous-licenses operation. 27 | cdx-ev amend --operation add-license-text --license-dir ./license_texts bom.json --output bom.json 28 | cdx-ev amend --operation delete-ambiguous-licenses bom.json 29 | 30 | Operation details 31 | ----------------- 32 | 33 | add-bom-ref 34 | ^^^^^^^^^^^ 35 | 36 | .. autooperation:: cdxev.amend.operations::AddBomRef 37 | 38 | add-license-text 39 | ^^^^^^^^^^^^^^^^ 40 | 41 | The operation *add-license-text* can be used to insert known full license texts for licenses identified by name. You can use this, for instance, in workflows where SBOMs are created or edited by hand - so a clutter-free JSON is preferred - then, in a last step, full texts are inserted using this operation. 42 | 43 | License texts are inserted only if: 44 | 45 | * The license has a ``name`` field. 46 | * The license has no ``id`` field. 47 | * The license has no or an empty ``text.content`` field. 48 | * A matching file is found. 49 | 50 | You must provide one file per license text in a flat directory. The stem of the filename, that is everything up to the extension (i.e., up to but not including the last period), must match the license name specified in the SBOM. 51 | 52 | Example 53 | """"""" 54 | 55 | Given this license in the input SBOM:: 56 | 57 | { 58 | "license": { 59 | "name": "My license" 60 | } 61 | } 62 | 63 | the operation would search the full license text in any file named ``My license``, ``My license.txt``, ``My license.md``, or any other extension. 64 | However, the file ``My license.2.txt`` would be disregarded, because its stem (``My license.2``) doesn't match the license name. 65 | 66 | compositions 67 | ^^^^^^^^^^^^ 68 | 69 | .. autooperation:: cdxev.amend.operations::Compositions 70 | 71 | 72 | default-author 73 | ^^^^^^^^^^^^^^ 74 | 75 | .. autooperation:: cdxev.amend.operations::DefaultAuthor 76 | 77 | delete-ambiguous-licenses 78 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 79 | 80 | .. autooperation:: cdxev.amend.operations::DeleteAmbiguousLicenses 81 | 82 | 83 | infer-supplier 84 | ^^^^^^^^^^^^^^ 85 | 86 | .. autooperation:: cdxev.amend.operations::InferSupplier 87 | 88 | license-name-to-id 89 | ^^^^^^^^^^^^^^^^^^ 90 | 91 | .. autooperation:: cdxev.amend.operations::LicenseNameToId 92 | -------------------------------------------------------------------------------- /docs/source/usage/vex.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | vex 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: vex 10 | :nosubcommands: 11 | 12 | This command provides different operations on VEX/SBOM files with embedded vulnerabilities. The vex-command has the following subcommands: 13 | 14 | * ``list``: returns a list of all vulnerability-IDs. 15 | * ``trim``: returns a file with filtered vulnerabilities. 16 | * ``search``: returns a file with a specific vulnerability. 17 | * ``extract``: extract all vulnerabilities from an SBOM file to a VEX file. 18 | 19 | list 20 | ------------- 21 | .. argparse:: 22 | :filename: ./cdxev/__main__.py 23 | :func: create_parser 24 | :prog: cdx-ev 25 | :path: vex list 26 | 27 | This subcommand returns a list of all vulnerability-IDs inside the input file. There are two different options: 28 | 29 | * ``--state default`` (default) returns: :: 30 | 31 | CVE-ID,Description,Status 32 | CVE-1012-0001,some description of a vulnerability,exploitable 33 | CVE-1013-0002,some description of a vulnerability 2,not_affected 34 | CVE-1013-0003,some description of a vulnerability 3,exploitable 35 | 36 | * ``--state lightweight`` returns: :: 37 | 38 | CVE-ID 39 | CVE-1012-0001 40 | CVE-1013-0002 41 | CVE-1013-0003 42 | 43 | 44 | The output can be a text file or a CSV (default) file. 45 | 46 | Example:: 47 | 48 | # Write all vulnerability-IDs to list_vex.json 49 | cdxev vex list input_file.json --scheme default --format csv --output list_vex.json 50 | 51 | 52 | trim 53 | ------------- 54 | .. argparse:: 55 | :filename: ./cdxev/__main__.py 56 | :func: create_parser 57 | :prog: cdx-ev 58 | :path: vex trim 59 | 60 | This subcommand returns a JSON file which contains only filtered vulnerabilities. The vulnerabilities can be filtered by any key-value pair. 61 | 62 | Example:: 63 | 64 | # Writes all vulnerabilities with state "not_affected" to new file 65 | cdxev vex trim input_file.json key=state value=not_affected --output not_affected_vex.json 66 | 67 | 68 | search 69 | ------------- 70 | .. argparse:: 71 | :filename: ./cdxev/__main__.py 72 | :func: create_parser 73 | :prog: cdx-ev 74 | :path: vex search 75 | 76 | This subcommand searches a file for a specific vulnerability based on its ID. The command returns a JSON file. 77 | 78 | Example:: 79 | 80 | # Writes specific vulnerability with based on its ID to new file 81 | cdxev vex search input_file.json CVE-1013-0002 --output searched_vul.json 82 | 83 | 84 | extract 85 | ------------- 86 | .. argparse:: 87 | :filename: ./cdxev/__main__.py 88 | :func: create_parser 89 | :prog: cdx-ev 90 | :path: vex extract 91 | 92 | This subcommand extracts all vulnerabilities from an SBOM file and returns it as a VEX file in JSON format. 93 | 94 | Example:: 95 | 96 | # Writes specific vulnerability with based on its ID to new file 97 | cdxev vex extract input_file.json --output vex.json 98 | -------------------------------------------------------------------------------- /cdxev/log.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import dataclasses 4 | import logging 5 | import logging.handlers 6 | import traceback 7 | import typing as t 8 | 9 | 10 | @dataclasses.dataclass 11 | class LogMessage: 12 | message: str 13 | """ 14 | A short message, often not even a full sentence. Avoid putting a period at the end. 15 | """ 16 | 17 | description: str 18 | """A longer description of the error.""" 19 | 20 | module_name: t.Optional[str] = None 21 | """The module where the error occurred.""" 22 | 23 | line_start: t.Optional[int] = None 24 | """The line where the error occurred in :py:attr:`file_name`.""" 25 | 26 | def __str__(self) -> str: 27 | return f"{self.message} at [{self.module_name}]: {self.description}" 28 | 29 | 30 | class LogMessageFormatter(logging.Formatter): 31 | def format(self, record: logging.LogRecord) -> str: # noqa: N802 32 | if isinstance(record.msg, str): 33 | message = record.msg % record.args 34 | else: 35 | if not isinstance(record.msg, LogMessage): 36 | raise TypeError( 37 | "This formatter can only process strings and LogMessage instances" 38 | ) 39 | 40 | frame = None 41 | if record.exc_info is not None: 42 | tb = traceback.extract_tb(record.exc_info[2]) 43 | frame = tb.pop() 44 | 45 | if record.msg.line_start is not None: 46 | line_start = record.msg.line_start 47 | elif frame is not None: 48 | if frame.lineno is not None: 49 | line_start = frame.lineno 50 | else: 51 | line_start = None 52 | 53 | if record.msg.module_name is not None: 54 | component = record.msg.module_name 55 | else: 56 | component = None 57 | 58 | location = self._generate_location_str(line_start, component) 59 | 60 | message = record.msg.message + location + " - " + record.msg.description 61 | 62 | return f"{record.levelname}: {message}" 63 | 64 | def _generate_location_str( 65 | self, 66 | line_start: t.Optional[int], 67 | component: t.Optional[str], 68 | ) -> str: 69 | if line_start is None and component is None: 70 | return "" 71 | 72 | location = " (" 73 | if component is not None: 74 | location += "component: " + component 75 | 76 | if line_start is not None: 77 | location += " at line " + str(line_start) 78 | if "component: " not in location: 79 | location = location.replace(" at line", "at line") 80 | 81 | location += ")" 82 | return location 83 | 84 | 85 | def configure_logging(quiet: bool, verbose: bool) -> None: 86 | """ 87 | Configures the log level of the module. 88 | """ 89 | root_logger = logging.getLogger() 90 | root_logger.setLevel(logging.DEBUG if verbose else logging.INFO) 91 | 92 | if not quiet: 93 | stderr_handler = logging.StreamHandler() 94 | stderr_handler.setLevel(logging.DEBUG) 95 | stderr_handler.setFormatter(LogMessageFormatter()) 96 | root_logger.addHandler(stderr_handler) 97 | -------------------------------------------------------------------------------- /cdxev/validator/helper.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import json 4 | import logging 5 | import re 6 | import typing as t 7 | from importlib import resources 8 | from pathlib import Path 9 | 10 | from cdxev.auxiliary.filename_gen import generate_validation_pattern 11 | from cdxev.error import AppError 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def open_schema( 17 | spec_version: str, 18 | schema_type: t.Optional[str], 19 | schema_path: t.Optional[Path], 20 | ) -> dict: 21 | try: 22 | if schema_type: 23 | return _get_builtin_schema(schema_type, spec_version) 24 | else: 25 | # Convince mypy that schema_path isn't None, because the caller made sure of this 26 | schema_path = t.cast(Path, schema_path) 27 | 28 | if not schema_path.is_file(): 29 | raise AppError( 30 | "Schema not loaded", 31 | "Path does not exist or is not a file: " + str(schema_path), 32 | ) 33 | with schema_path.open() as fp: 34 | return json.load(fp) # type:ignore [no-any-return] 35 | except OSError as e: 36 | raise AppError("Schema not loaded", str(e)) from e 37 | except json.JSONDecodeError as e: 38 | raise AppError( 39 | "Schema not loaded", 40 | "Invalid JSON in schema file " + str(schema_path), 41 | ) from e 42 | 43 | 44 | def _get_builtin_schema(schema_type: str, spec_version: str) -> dict: 45 | schema_dir = resources.files("cdxev.auxiliary") / "schema" 46 | if schema_type == "default": 47 | schema_file = schema_dir / f"bom-{spec_version}.schema.json" 48 | else: 49 | schema_file = schema_dir / f"bom-{spec_version}-{schema_type}.schema.json" 50 | 51 | if not schema_file.is_file(): 52 | raise AppError( 53 | "Schema not loaded", 54 | f"No built-in schema found for CycloneDX version {spec_version} and " 55 | f"schema type '{schema_type}'.", 56 | ) 57 | schema_json = schema_file.read_text() 58 | schema = json.loads(schema_json) 59 | if isinstance(schema, dict): 60 | return schema 61 | else: 62 | raise AppError( 63 | "Schema error", 64 | ("Loaded builtin schema is not of type dict"), 65 | ) 66 | 67 | 68 | def load_spdx_schema() -> dict: 69 | path_to_embedded_schema = ( 70 | resources.files("cdxev.auxiliary.schema") / "spdx.schema.json" 71 | ) 72 | with path_to_embedded_schema.open() as f: 73 | schema = json.load(f) 74 | if isinstance(schema, dict): 75 | return schema 76 | else: 77 | raise AppError( 78 | "SPDX schema error", 79 | ("Loaded SPDX schema is not type dict"), 80 | ) 81 | 82 | 83 | def validate_filename( 84 | filename: str, 85 | regex: str, 86 | sbom: dict, 87 | schema_type: t.Optional[str], 88 | ) -> t.Union[t.Literal[False], str]: 89 | if not regex: 90 | if schema_type == "custom": 91 | regex = generate_validation_pattern(sbom) 92 | else: 93 | regex = "^(bom\\.json|.+\\.cdx\\.json)$" 94 | 95 | if re.fullmatch(regex, filename) is None: 96 | return "filename doesn't match regular expression " + regex 97 | else: 98 | return False 99 | -------------------------------------------------------------------------------- /docs/source/usage/init-sbom.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | init-sbom 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: init-sbom 10 | 11 | This command provides a first draft of an SBOM for manual completion. 12 | 13 | The created SBOM is according to the CycloneDX specification version 1.6. 14 | 15 | Optional inputs 16 | --------------- 17 | 18 | Values for some fields can be provided to the command, those are: 19 | 20 | * The name for one author of the SBOM (metadata.authors[0].name) using the flag `--authors`, 21 | * The name of the supplier of the software (metadata.component.supplier.name) using the flag `--supplier`, 22 | * The name of the software (metadata.component.name) using the flag `--name`, 23 | * The version of the software (metadata.component.version) using the flag `--version`. 24 | 25 | Examples:: 26 | 27 | # Write an SBOM draft with default content to bom.json 28 | cdx-ev init-sbom -o bom.json 29 | 30 | # Write an SBOM draft with a submitted software name, version, supplier and author of the SBOM to bom.json 31 | cdx-ev init-sbom --name "my software" --supplier "acme inc." --version "1.1.1" --author "acme inc" -o bom.json 32 | 33 | The above provided example without passing arguments to `init-sbom` would result in: :: 34 | 35 | { 36 | "dependencies": [ 37 | { 38 | "ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM." 39 | } 40 | ], 41 | "metadata": { 42 | "authors": [ 43 | { 44 | "email": "The email address of the contact.", 45 | "name": "The person who created the SBOM.", 46 | "phone": "The phone number of the contact." 47 | } 48 | ], 49 | "component": { 50 | "bom-ref": "An optional identifier which can be used to reference the component elsewhere in the SBOM.", 51 | "copyright": "A copyright notice informing users of the underlying claims to copyright ownership in a published work.", 52 | "name": "The name of the component described by the SBOM.", 53 | "supplier": { 54 | "name": "The name of the organization that supplied the component." 55 | }, 56 | "type": "application", 57 | "version": "The component version." 58 | }, 59 | "timestamp": "2024-10-27T10:56:40.095452+01:00", 60 | "tools": [ 61 | { 62 | "externalReferences": [ 63 | { 64 | "type": "website", 65 | "url": "https://github.com/Festo-se/cyclonedx-editor-validator" 66 | } 67 | ], 68 | "name": "cyclonedx-editor-validator", 69 | "vendor": "Festo SE & Co. KG", 70 | "version": "0.0.0" 71 | } 72 | ] 73 | }, 74 | "serialNumber": "urn:uuid:1fa01e4f-04f0-4208-9ea3-b53de58fd6a0", 75 | "version": 1, 76 | "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", 77 | "bomFormat": "CycloneDX", 78 | "specVersion": "1.6" 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '25 15 * * 3' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cyclonedx-editor-validator" 3 | version = "0.0.0" 4 | description = "Tool for creating, modifying and validating CycloneDX SBOMs." 5 | authors = [ 6 | { name = "Aleg Vilinski", email = "aleg.vilinski@festo.com" }, 7 | { name = "Christian Beck", email = "christian.beck@festo.com" }, 8 | { name = "Moritz Marseu", email = "moritz.marseu@festo.com" }, 9 | ] 10 | requires-python = ">=3.10.0,<4" 11 | readme = "README.md" 12 | license = "GPL-3.0-or-later" 13 | dependencies = [ 14 | "python-dateutil==2.9.0.post0", 15 | "jsonschema[format]==4.25.1", 16 | "docstring-parser>=0.16,<0.18", 17 | "charset-normalizer>=3.3.2,<4", 18 | "pyicu>=2.13.1,<3 ; sys_platform == 'darwin'", 19 | "pyicu>=2.13.1,<3 ; sys_platform == 'linux'", 20 | "natsort>=8.4.0,<9", 21 | "univers==31.1.0", 22 | "cyclonedx-python-lib==11.6.0", 23 | "email-validator (>=2.2.0,<3.0.0)", 24 | ] 25 | 26 | [project.urls] 27 | Documentation = "https://festo-se.github.io/cyclonedx-editor-validator/" 28 | Repository = "https://github.com/Festo-se/cyclonedx-editor-validator/" 29 | Issues = "https://github.com/Festo-se/cyclonedx-editor-validator/issues" 30 | Changelog = "https://github.com/Festo-se/cyclonedx-editor-validator/releases" 31 | 32 | [project.scripts] 33 | cdx-ev = "cdxev.__main__:main" 34 | 35 | [dependency-groups] 36 | dev = [ 37 | "flake8==7.3.0", 38 | "black==25.12.0", 39 | "pep8-naming==0.15.1", 40 | "mypy==1.19.1", 41 | "types-python-dateutil==2.9.0.20251115", 42 | "types-jsonschema==4.25.1.20251009", 43 | "pytest==9.0.2", 44 | "coverage==7.13.0", 45 | "toml==0.10.2", 46 | "typing-extensions==4.15.0", 47 | "bandit[toml]==1.9.2", 48 | "isort==7.0.0", 49 | "pre-commit==4.5.1", 50 | ] 51 | docs = [ 52 | "sphinx-argparse==0.5.2", 53 | "sphinx-rtd-theme==3.0.2", 54 | "sphinx==8.1.3", 55 | ] 56 | 57 | [tool.uv] 58 | default-groups = [ 59 | "dev", 60 | "docs", 61 | ] 62 | required-version = ">=0.7.0" 63 | 64 | [build-system] 65 | requires = ["hatchling"] 66 | build-backend = "hatchling.build" 67 | 68 | [tool.hatch.build.targets.wheel] 69 | packages = ["cdxev"] 70 | 71 | [tool.semantic_release] 72 | version_variable = ["pyproject.toml:version"] 73 | branch = "master" 74 | upload_to_repository = false 75 | upload_to_release = false 76 | build_command = "pip install uv && uv build" 77 | 78 | [tool.mypy] 79 | python_version = "3.10" 80 | packages = "cdxev" 81 | # Excludes tests even when mypy is invoked with a path (as the VS Code extension does, for instance) 82 | exclude = ['tests/'] 83 | strict = true 84 | 85 | [[tool.mypy.overrides]] 86 | module = [ 87 | "cdxev.merge", 88 | "cdxev.set", 89 | "cdxev.amend.operations", 90 | "cdxev.amend.license", 91 | "cdxev.amend.command", 92 | "cdxev.__main__", 93 | "cdxev.validator.helper", 94 | "cdxev.validator.validate", 95 | "cdxev.auxiliary.io_processing", 96 | "cdxev.auxiliary.sbomFunctions", 97 | "cdxev.auxiliary.filename_gen", 98 | "cdxev.auxiliary.identity", 99 | "cdxev.validator.customreports", 100 | "cdxev.build_public_bom" 101 | ] 102 | disallow_any_generics = false 103 | 104 | [[tool.mypy.overrides]] 105 | module = [ 106 | "cdxev.__main__", 107 | ] 108 | warn_return_any = false 109 | 110 | [tool.coverage.run] 111 | source = ["cdxev"] 112 | 113 | [tool.coverage.report] 114 | omit = ["*__init__.py*"] 115 | 116 | [tool.black] 117 | 118 | [tool.bandit] 119 | exclude_dirs = ["tests"] 120 | 121 | [tool.isort] 122 | profile = "black" 123 | -------------------------------------------------------------------------------- /tests/integration/helper.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import json 4 | import typing as t 5 | from pathlib import Path 6 | 7 | from pytest import CaptureFixture 8 | 9 | import cdxev.pkg 10 | from cdxev.__main__ import main 11 | 12 | 13 | def load_sbom(path: Path) -> dict: 14 | with path.open() as f: 15 | sbom = json.load(f) 16 | delete_non_reproducible(sbom) 17 | return sbom 18 | 19 | 20 | def load_list(path: Path) -> dict: 21 | with path.open() as f: 22 | list = f.read() 23 | return list 24 | 25 | 26 | def delete_non_reproducible(sbom: dict): 27 | """ 28 | Deletes any fields from the SBOM that are typically not reproducible between builds. 29 | These would lead to wrong test results when comparing actual to expected SBOMs. 30 | """ 31 | _delete_tool_version(sbom) 32 | 33 | if "serialNumber" in sbom: 34 | del sbom["serialNumber"] 35 | if "metadata" in sbom: 36 | if "timestamp" in sbom["metadata"]: 37 | del sbom["metadata"]["timestamp"] 38 | 39 | 40 | def _delete_tool_version(sbom: dict): 41 | tools: t.Optional[t.Union[list, dict]] = sbom.get("metadata", {}).get("tools") 42 | 43 | if tools is None: 44 | return 45 | 46 | if isinstance(tools, dict): 47 | tools = tools.get("components", []) 48 | 49 | assert isinstance(tools, list) 50 | 51 | cdxev_tool = next(tool for tool in tools if tool["name"] == cdxev.pkg.NAME) 52 | 53 | if cdxev_tool is not None and "version" in cdxev_tool: 54 | del cdxev_tool["version"] 55 | 56 | 57 | @t.overload 58 | def run_main(capsys: None = ..., parse_output: None = ...) -> t.Tuple[int, None]: ... 59 | 60 | 61 | @t.overload 62 | def run_main( 63 | capsys: CaptureFixture[str] = ..., parse_output: None = ... 64 | ) -> t.Tuple[int, str, str]: ... 65 | 66 | 67 | @t.overload 68 | def run_main( 69 | capsys: CaptureFixture[str], parse_output: t.Literal["json"] 70 | ) -> t.Tuple[int, dict, str]: ... 71 | 72 | 73 | @t.overload 74 | def run_main( 75 | capsys: CaptureFixture[str], parse_output: t.Literal["filename"] 76 | ) -> t.Tuple[int, Path, str]: ... 77 | 78 | 79 | def run_main( 80 | capsys: t.Optional[CaptureFixture[str]] = None, 81 | parse_output: t.Optional[t.Union[t.Literal["json"], t.Literal["filename"]]] = None, 82 | ): 83 | """ 84 | Runs the main module of this project and returns the exit code as well as stdout. 85 | 86 | :param capsys: Tests must pass the capsys fixture if they need stdout. 87 | :param parse_output: If ``"json""`` stdout is parsed as JSON and the parsed ``dict`` is 88 | returned. 89 | If ``"filename"``, the filename printed to stdout is extracted and 90 | returned as a ``pathlib.Path`` object. 91 | If ``None``, stdout is returned as-is. 92 | :returns: A tuple of ``(exit_code, stdout, stderr)``, where *stdout* might be ``None``, 93 | ``str``, ``Path``, or ``dict`` depending on parameters. *stderr* is always returned 94 | as-is. 95 | """ 96 | exit_code = main() 97 | 98 | if capsys is None: 99 | return (exit_code, None) 100 | 101 | (out, err) = capsys.readouterr() 102 | 103 | if parse_output == "json": 104 | out = json.loads(out) 105 | delete_non_reproducible(out) 106 | elif parse_output == "filename": 107 | # Parse the output filename from stdout 108 | out = Path(out.split(":")[1].strip()) 109 | 110 | return (exit_code, out, err) 111 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: tests 5 | 6 | on: 7 | workflow_call: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test_pages: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 19 | with: 20 | python-version: '3.10' 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 23 | - name: Install dependencies 24 | run: uv sync --only-group docs --frozen 25 | - name: Test build of documentation 26 | run: uv run sphinx-build -a -E docs/source/ docs/build/ 27 | 28 | static_analysis: 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 34 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 35 | with: 36 | python-version: '3.10' 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 39 | with: 40 | enable-cache: true 41 | - name: Install dependencies for development 42 | run: uv sync --only-dev --frozen 43 | - name: Run black 44 | run: uv run black cdxev tests --check 45 | - name: Run isort 46 | run: uv run isort cdxev/ tests/ --check-only 47 | - name: Run flake8 48 | run: uv run flake8 cdxev tests 49 | - name: Run mypy 50 | run: uv run mypy --install-types --non-interactive --config-file=pyproject.toml 51 | - name: Run bandit 52 | run: uv run bandit -c pyproject.toml -r cdxev 53 | 54 | pytest: 55 | runs-on: ubuntu-latest 56 | 57 | needs: static_analysis 58 | 59 | permissions: 60 | pull-requests: write 61 | 62 | steps: 63 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 64 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 65 | with: 66 | python-version: '3.10' 67 | - name: Install uv 68 | uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 69 | - name: Install package 70 | run: uv sync --no-group docs --frozen 71 | - name: Run pytest 72 | run: uv run coverage run -m pytest --junitxml=reports/unittest.xml 73 | - name: Check coverage 74 | run: uv run coverage report --fail-under=95 75 | - name: Save test results 76 | run: uv run coverage xml -o reports/py-coverage.cobertura.xml 77 | - name: Archive test results 78 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 79 | with: 80 | name: test-report 81 | path: reports/unittest.xml 82 | - name: Archive code coverage results 83 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 84 | with: 85 | name: code-coverage-report 86 | path: reports/py-coverage.cobertura.xml 87 | - name: Coverage commit 88 | if: ${{ success() && GITHUB.EVENT_NAME == 'pull_request' && !github.event.pull_request.head.repo.fork }} 89 | uses: MishaKav/pytest-coverage-comment@ae0e8a539a3f310aefb3bfb6a2209778a21fa42b # v1.2.0 90 | with: 91 | junitxml-path: reports/unittest.xml 92 | pytest-xml-coverage-path: reports/py-coverage.cobertura.xml 93 | report-only-changed-files: true 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![build and test](https://github.com/Festo-se/cyclonedx-editor-validator/actions/workflows/main.yml/badge.svg)](https://github.com/Festo-se/cyclonedx-editor-validator/actions/workflows/main.yml) 3 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Festo-se/cyclonedx-editor-validator/badge)](https://scorecard.dev/viewer/?uri=github.com/Festo-se/cyclonedx-editor-validator) 4 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10485/badge)](https://www.bestpractices.dev/projects/10485) 5 | [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit) 6 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 7 | [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) 8 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 9 | [![Static Badge](https://img.shields.io/badge/CycloneDX-v1.2%2C1.3%2C1.4%2C1.5%2C1.6-blue?link=https%3A%2F%2Fcyclonedx.org%2Fdocs%2F1.6%2Fjson%2F%23)](https://cyclonedx.org/docs/1.6/json/) 10 | 11 | # CycloneDX Editor/Validator 12 | 13 | This command-line tool performs various actions on [CycloneDX](https://cyclonedx.org/) SBOMs. It allows you to modify, merge and validate your Software Bill of Materials (SBOM). 14 | 15 | The tool is built with automation in mind, i.e. usage within CI/CD. We try to be as scriptable as possible with various command-line flags, avoiding interactive prompts, providing multiple output options and fine-grained exit codes. 16 | 17 | ## Command overview 18 | 19 | | Command | Description | 20 | | :-- | :-- | 21 | | [amend](https://festo-se.github.io/cyclonedx-editor-validator/usage/amend.html) | Accepts a single input file and will apply one or multiple *operations* to it. Each operation modifies certain aspects of the SBOM. These modifications cannot be targeted at individual components in the SBOM which sets the *amend* command apart from [*set*](https://festo-se.github.io/cyclonedx-editor-validator/usage/set.html). Its use-case is ensuring an SBOM fulfils certain requirements in an automated fashion. | 22 | | [build-public](https://festo-se.github.io/cyclonedx-editor-validator/usage/build-public.html) | Creates a redacted version of an SBOM fit for publication. | 23 | | [init-sbom](https://festo-se.github.io/cyclonedx-editor-validator/usage/init-sbom.html) | Provides a first draft of an SBOM for manual completion. | 24 | | [list](https://festo-se.github.io/cyclonedx-editor-validator/usage/list.html) | Lists content of the SBOM. | 25 | | [merge](https://festo-se.github.io/cyclonedx-editor-validator/usage/merge.html) | Merges two or more CycloneDX documents into one. | 26 | | [set](https://festo-se.github.io/cyclonedx-editor-validator/usage/set.html) | Sets properties on specified components to specified values. If a component in an SBOM is missing a particular property or the property is present but has a wrong value, this command can be used to modify just the affected properties without changing the rest of the SBOM. | 27 | | [validate](https://festo-se.github.io/cyclonedx-editor-validator/usage/validate.html) | Validate the SBOM against a built-in or user-provided JSON schema. | 28 | | [vex](https://festo-se.github.io/cyclonedx-editor-validator/usage/vex.html) | Apply different operations on VEX-files. | 29 | 30 | ## Installation and usage 31 | 32 | This tool is published on [PyPi](https://pypi.org/project/cyclonedx-editor-validator/). 33 | 34 | For detailed installation and usage guides, please refer to our [official documentation](https://festo-se.github.io/cyclonedx-editor-validator). 35 | 36 | ## Contributing 37 | 38 | See our [contribution guidelines](https://festo-se.github.io/cyclonedx-editor-validator/CONTRIBUTING). 39 | 40 | ## License 41 | 42 | This software is made available under the GNU General Public License v3 (GPL-3.0-or-later). 43 | -------------------------------------------------------------------------------- /docs/source/usage/merge.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | merge 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: merge 10 | 11 | Merges two or more CycloneDX input files into one. Inputs can either be specified directly as positional arguments on the command-line or using the ``--from-folder`` option. Files specified as arguments are merged in the order they are given, files in the folder are merged in alphabetical order (see note below). 12 | 13 | If both positional arguments and the ``--from-folder`` option are used, then the position arguments are merged first, followed by the files in the folder. The command will not merge the same file twice, if it is specified on the command-line and also part of the folder. 14 | 15 | When using the ``--from-folder`` option, the program looks for files matching either of the `recommended CycloneDX naming schemes `_: ``bom.json`` or ``*.cdx.json``. 16 | 17 | Details 18 | --------------- 19 | 20 | Input files in the folder provided to the ``--from-folder`` option are sorted by name in a platform-specific way. In other words, they are merged in the same order they appear in your operating system's file browser (e.g., Windows Explorer) when sorted by name. 21 | 22 | The process runs iteratively, merging two SBOMs in each iteration. In the first round, the second submitted SBOM is merged into the first. In the second round the third would be merged into the result of the first round and so on. 23 | 24 | In mathematical terms: :math:`output = (((input_1 * input_2) * input_3) * input_4 ...)` 25 | 26 | The merge is per default not hierarchical for the ``components`` field of a ``component`` (`CycloneDX documentation `_). This means that components that were contained in the ``components`` of an already present component will just be added as new components under the SBOMs' ``components`` sections. 27 | The ``--hierarchical`` flag allows for hierarchical merges. This affects only the top level components of the merged SBOM. The structured of nested components is preserved in both cases (except the removal of already present components), as shown for "component 4" in the image below. 28 | 29 | .. image:: /img/merge_hierarchical_structure.svg 30 | :alt: Merge components structure default and hierarchical. 31 | 32 | A few notes on the merge algorithm: 33 | 34 | - The ``metadata`` field is always retained from the first input and never changed through a merge with the exception of the ``timestamp``. 35 | - The command merges the contents of the fields ``components``, ``dependencies``, ``compositions`` and ``vulnerabilities``. 36 | - Components are merged into the result in the order they **first** appear in the inputs. If any subsequent input specifies the same component (sameness in this case being defined as having identical identifying attributes such as ``name``, ``version``, ``purl``, etc.), the later instance of the component will be dropped with a warning. **This command cannot be used to merge information inside components.** 37 | - The resulting dependency graph will reflect all dependencies from all inputs. Dependencies from later inputs are always added to the result, even if the component is dropped as a duplicate as described above. 38 | - Uniqueness of *bom-refs* will be ensured. 39 | - The command is able to merge inputs containing only VEX information in the form of a ``vulnerabilities``. To ensure a sensible result, it should be ensured that bom-refs in the affects field reference components of the same SBOM. 40 | - Vulnerabilities, like components, are merged into the result in the order they **first** appear in the inputs. 41 | - If a merged vulnerability contains additional entries in the ``affects`` field, those will be added to the original vulnerability object (duplicates are possible if version ranges are used). 42 | -------------------------------------------------------------------------------- /cdxev/initialize_sbom.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import json 4 | from datetime import datetime 5 | from typing import Any, Union 6 | from uuid import uuid4 7 | 8 | from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri 9 | from cyclonedx.model.bom import Bom, BomMetaData 10 | from cyclonedx.model.bom_ref import BomRef 11 | from cyclonedx.model.component import Component, ComponentType 12 | from cyclonedx.model.contact import OrganizationalContact, OrganizationalEntity 13 | from cyclonedx.model.dependency import Dependency 14 | from cyclonedx.model.tool import Tool 15 | from cyclonedx.output.json import JsonV1Dot6 16 | from email_validator import ( 17 | EmailNotValidError, 18 | validate_email, 19 | ) 20 | 21 | from cdxev import pkg 22 | 23 | 24 | def initialize_sbom( 25 | software_name: Union[str, None], 26 | version: Union[str, None], 27 | supplier: Union[str, None], 28 | authors: Union[str, None], 29 | email: Union[str, None] = None, 30 | ) -> dict[str, Any]: 31 | """ 32 | Creates an initial SBOM draft to work with, containing the most basic fields. 33 | 34 | param software_name: the name of the component. 35 | param version: the component version. 36 | param authors: the person(s) who created the BOM. 37 | param supplier: the name of the organization that supplied the component. 38 | 39 | returns: initial SBOM for the software. 40 | """ 41 | if software_name is None: 42 | software_name = "The name of the component described by the SBOM." 43 | if version is None: 44 | version = "The component version." 45 | if authors is None: 46 | authors = "The person who created the SBOM." 47 | if supplier is None: 48 | supplier = "The name of the organization that supplied the component." 49 | 50 | timestamp = datetime.now() 51 | copyright = ( 52 | "A copyright notice informing users of " 53 | "the underlying claims to copyright ownership in a published work." 54 | ) 55 | 56 | if email is None: 57 | metadata_authors = OrganizationalContact( 58 | name=authors, 59 | ) 60 | else: 61 | try: 62 | validate_email(email, check_deliverability=False) 63 | except EmailNotValidError: 64 | raise ValueError("Provided email is invalid.") 65 | 66 | metadata_authors = OrganizationalContact( 67 | name=authors, 68 | email=email, 69 | ) 70 | 71 | component_supplier = OrganizationalEntity(name=supplier) 72 | 73 | refrence_to_cdxev_tool = ExternalReference( 74 | url=XsUri("https://github.com/Festo-se/cyclonedx-editor-validator"), 75 | type=ExternalReferenceType.WEBSITE, 76 | ) 77 | 78 | bom_ref = BomRef(str(uuid4())) 79 | 80 | metadata_component = Component( 81 | name=software_name, 82 | type=ComponentType.APPLICATION, 83 | supplier=component_supplier, 84 | version=version, 85 | copyright=copyright, 86 | bom_ref=bom_ref, 87 | ) 88 | 89 | metadata = BomMetaData( 90 | tools=[ 91 | Tool( 92 | name=pkg.NAME, 93 | version=pkg.VERSION, 94 | vendor=pkg.VENDOR, 95 | external_references=[refrence_to_cdxev_tool], 96 | ) 97 | ], 98 | authors=[metadata_authors], 99 | component=metadata_component, 100 | timestamp=timestamp, 101 | ) 102 | 103 | sbom = Bom( 104 | version=1, 105 | metadata=metadata, 106 | dependencies=[Dependency(bom_ref, dependencies=[])], 107 | ) 108 | 109 | my_json_outputter = JsonV1Dot6(sbom) 110 | 111 | serialized_json: dict[str, Any] = json.loads( 112 | my_json_outputter.output_as_string(indent=4) 113 | ) 114 | 115 | # Not yet supported by the model 116 | serialized_json["compositions"] = [{"aggregate": "unknown", "assemblies": []}] 117 | 118 | return serialized_json 119 | -------------------------------------------------------------------------------- /tests/auxiliary/test_merge_sboms/ratings_lists_for_tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "ratings1": [ 3 | { 4 | "score": 9.8, 5 | "severity": "critical", 6 | "method": "CVSSv31", 7 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 8 | } 9 | ], 10 | "ratings2": [ 11 | { 12 | "score": 9.8, 13 | "severity": "critical", 14 | "method": "other method", 15 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 16 | } 17 | ], 18 | "merged_ratings_1_and_ratings_2": [ 19 | { 20 | "score": 9.8, 21 | "severity": "critical", 22 | "method": "CVSSv31", 23 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 24 | }, 25 | { 26 | "score": 9.8, 27 | "severity": "critical", 28 | "method": "other method", 29 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 30 | } 31 | ], 32 | "ratings3": [ 33 | { 34 | "score": 9.8, 35 | "severity": "critical", 36 | "method": "method", 37 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 38 | }, 39 | { 40 | "score": 7.5, 41 | "severity": "high", 42 | "method": "New 2", 43 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 44 | }, 45 | { 46 | "score": 2, 47 | "severity": "high", 48 | "method": "New 1", 49 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 50 | } 51 | ], 52 | "ratings4": [ 53 | { 54 | "score": 9.8, 55 | "severity": "critical", 56 | "method": "method 2", 57 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 58 | }, 59 | { 60 | "score": 7.5, 61 | "severity": "high", 62 | "method": "New 1", 63 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 64 | }, 65 | { 66 | "score": 3, 67 | "severity": "high", 68 | "method": "New 2", 69 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 70 | } 71 | ], 72 | "merged_ratings_3_and_ratings_4_flag_0": [ 73 | { 74 | "score": 9.8, 75 | "severity": "critical", 76 | "method": "method", 77 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 78 | }, 79 | { 80 | "score": 7.5, 81 | "severity": "high", 82 | "method": "New 2", 83 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 84 | }, 85 | { 86 | "score": 9.8, 87 | "severity": "critical", 88 | "method": "method 2", 89 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 90 | }, 91 | { 92 | "score": 7.5, 93 | "severity": "high", 94 | "method": "New 1", 95 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 96 | } 97 | ], 98 | "merged_ratings_3_and_ratings_4_flag_1": [ 99 | { 100 | "score": 9.8, 101 | "severity": "critical", 102 | "method": "method", 103 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 104 | }, 105 | { 106 | "score": 7.5, 107 | "severity": "high", 108 | "method": "New 2", 109 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 110 | }, 111 | { 112 | "score": 2, 113 | "severity": "high", 114 | "method": "New 1", 115 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 116 | }, 117 | { 118 | "score": 9.8, 119 | "severity": "critical", 120 | "method": "method 2", 121 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 122 | } 123 | ], 124 | "merged_ratings_3_and_ratings4_flag_2": [ 125 | { 126 | "score": 9.8, 127 | "severity": "critical", 128 | "method": "method", 129 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 130 | }, 131 | { 132 | "score": 9.8, 133 | "severity": "critical", 134 | "method": "method 2", 135 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 136 | }, 137 | { 138 | "score": 7.5, 139 | "severity": "high", 140 | "method": "New 1", 141 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 142 | }, 143 | { 144 | "score": 3, 145 | "severity": "high", 146 | "method": "New 2", 147 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 148 | } 149 | ] 150 | } 151 | -------------------------------------------------------------------------------- /docs/source/usage/validate.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | validate 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: validate 10 | 11 | Validates an SBOM against a JSON schema. 12 | 13 | Schema selection 14 | ---------------- 15 | 16 | This tool can validate SBOMs against any user-provided JSON schema but for convenience, two schema types are built in: 17 | 18 | * The *default* schema type validates against the `stock CycloneDX schema `_. 19 | * The *strict* schema type refers to the strict variants of the stock CycloneDX schema which were discontinued after version 1.3. 20 | * The *custom* schema type uses a more restrictive schema which accepts a subset of CycloneDX. Additional requirements incorporated into the schema mostly originate from the `NTIA `_. 21 | 22 | You can select the schema with the ``--schema-type`` or ``--schema-path`` options:: 23 | 24 | cdx-ev validate bom.json [--schema-type default] # stock CycloneDX schema 25 | cdx-ev validate bom.json --schema-type custom # built-in custom schema 26 | cdx-ev validate bom.json --schema-path # your own schema 27 | 28 | For all built-in schemas, the tool determines the CycloneDX version from the input SBOM. The following versions are currently supported: 29 | 30 | =========== ============================ 31 | Type Supported CycloneDX versions 32 | =========== ============================ 33 | ``default`` 1.2 to 1.6 34 | ``strict`` 1.2 to 1.3 35 | ``custom`` 1.3 to 1.6 36 | =========== ============================ 37 | 38 | Validation of filename 39 | ---------------------- 40 | 41 | The tool, by default, also validates the filename of the SBOM. Which filenames are accepted depends on several command-line options: 42 | 43 | * ``--no-filename-validation`` completely disables validation. 44 | * Use ``--filename-pattern`` to provide a custom regex. 45 | 46 | * The filename must be a full match, regex anchors (^ and $) are not required. 47 | * Regex patterns often include special characters. Pay attention to escaping rules for your shell to ensure proper results. 48 | 49 | * In all other cases, the acceptable filenames depend on the selected schema: 50 | 51 | * When using the stock CycloneDX schema (``--schema-type default`` or no option at all) or when using your own schema (``--schema-path`` option), the validator accepts the two patterns recommended by the `CycloneDX specification `_: ``bom.json`` or ``*.cdx.json``. 52 | * When validating against the built-in custom schema (``--schema-type custom``), filenames must match one of these patterns: ``bom.json`` or ``__||_.cdx.json``. See below for explanations of the placeholders. 53 | 54 | ```` and ```` correspond to the respective fields in ``metadata.component`` in the SBOM. 55 | 56 | ```` corresponds to ``metadata.timestamp`` and ```` means any value in ``metadata.component.hashes[].content``. 57 | 58 | Either ```` or ```` must be present. If both are specified, ```` must come first. 59 | 60 | Output 61 | ------ 62 | 63 | By default, the command writes human-readable validation results to *stdout* only. For integration into CI/CD several machine-readable report formats are supported as well. To have a report written to a file, select the format using the ``--report-format`` option and an output path using the ``--report-path`` option. 64 | 65 | These formats are currently supported: 66 | 67 | * `Jenkins warnings-ng-plugin `_ 68 | * `GitLab Code Quality `_ 69 | 70 | Examples:: 71 | 72 | # Write human-readable messages to stdout and a report in warnings-ng format to report.json 73 | cdx-ev validate bom.json --report-format warnings-ng --report-path report.json 74 | 75 | # Write only a report in GitLab Code Quality format to cq.json 76 | cdx-ev --quiet validate bom.json --report-format gitlab-code-quality --report-path cq.json 77 | -------------------------------------------------------------------------------- /tests/auxiliary/test_build_public_bom_sboms/internal_removed_sbom.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.4", 4 | "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-02-17T10:14:58Z", 8 | "authors": [ 9 | { 10 | "name": "anonymous" 11 | } 12 | ], 13 | "component": { 14 | "type": "application", 15 | "bom-ref": "acme-app", 16 | "group": "com.company.internal", 17 | "supplier": { 18 | "name": "Company Legal" 19 | }, 20 | "name": "Acme_Application", 21 | "version": "9.1.1", 22 | "copyright": "Company Legal 2022, all rights reserved", 23 | "properties": [ 24 | { 25 | "name": "notinternal:stuff", 26 | "value": "something" 27 | } 28 | ] 29 | } 30 | }, 31 | "components": [ 32 | { 33 | "type": "library", 34 | "bom-ref": "comp1", 35 | "supplier": { 36 | "name": "Acme, Inc." 37 | }, 38 | "licenses": [ 39 | { 40 | "license": { 41 | "id": "Apache-1.0" 42 | } 43 | } 44 | ], 45 | "group": "org.acme", 46 | "name": "web-framework", 47 | "version": "1.0.0", 48 | "components": [ 49 | { 50 | "type": "library", 51 | "bom-ref": "sub_comp1", 52 | "supplier": { 53 | "name": "Acme, Inc." 54 | }, 55 | "licenses": [ 56 | { 57 | "license": { 58 | "id": "Apache-1.0" 59 | } 60 | } 61 | ], 62 | "group": "org.acme", 63 | "name": "sub_web-framework", 64 | "version": "1.0.0" 65 | } 66 | ] 67 | }, 68 | { 69 | "type": "library", 70 | "bom-ref": "comp2", 71 | "supplier": { 72 | "name": "Acme, Inc." 73 | }, 74 | "group": "com.something", 75 | "name": "persistence", 76 | "version": "3.1.0", 77 | "properties": [ 78 | { 79 | "name": "the Other", 80 | "value": "something" 81 | }, 82 | { 83 | "name": "not:internal:stuff", 84 | "value": "should be in" 85 | } 86 | ], 87 | "licenses": [ 88 | { 89 | "license": { 90 | "id": "Apache-2.0" 91 | } 92 | } 93 | ] 94 | }, 95 | { 96 | "type": "library", 97 | "bom-ref": "comp3", 98 | "supplier": { 99 | "name": "Acme, Inc." 100 | }, 101 | "group": "com.acme", 102 | "name": "tomcat-catalina", 103 | "version": "9.0.14", 104 | "licenses": [ 105 | { 106 | "license": { 107 | "id": "Apache-2.0" 108 | } 109 | } 110 | ] 111 | }, 112 | { 113 | "type": "library", 114 | "bom-ref": "comp4", 115 | "supplier": { 116 | "name": "Acme, Inc." 117 | }, 118 | "group": "", 119 | "name": "card-verifier", 120 | "version": "1.0.2", 121 | "licenses": [ 122 | { 123 | "expression": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0" 124 | } 125 | ] 126 | } 127 | ], 128 | "dependencies": [ 129 | { 130 | "ref": "acme-app", 131 | "dependsOn": [ 132 | "comp1", 133 | "comp2", 134 | "comp4", 135 | "comp3" 136 | ] 137 | }, 138 | { 139 | "ref": "comp1", 140 | "dependsOn": [ 141 | "sub_comp1", 142 | "comp2", 143 | "comp4" 144 | ] 145 | }, 146 | { 147 | "ref": "sub_comp1", 148 | "dependsOn": [] 149 | }, 150 | { 151 | "ref": "comp2", 152 | "dependsOn": [ 153 | "comp4", 154 | "comp3" 155 | ] 156 | }, 157 | { 158 | "ref": "comp3", 159 | "dependsOn": [ 160 | "comp4" 161 | ] 162 | }, 163 | { 164 | "ref": "comp4", 165 | "dependsOn": [ 166 | "comp3" 167 | ] 168 | } 169 | ], 170 | "compositions": [ 171 | { 172 | "aggregate": "incomplete", 173 | "assemblies": [ 174 | "comp1", 175 | "sub_comp1", 176 | "comp2", 177 | "comp3", 178 | "comp4" 179 | ] 180 | } 181 | ] 182 | } 183 | -------------------------------------------------------------------------------- /tests/auxiliary/test_amend_sboms/bom.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-09-03T01:06:14", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "library", 14 | "bom-ref": "some program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.internal", 19 | "name": "some program", 20 | "version": "T5.0.1.25", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "name": "company internal" 25 | } 26 | } 27 | ], 28 | "copyright": "Company Legal 2022, all rights reserved" 29 | } 30 | }, 31 | "components": [ 32 | { 33 | "type": "library", 34 | "bom-ref": "11231231", 35 | "supplier": { 36 | "name": "Company Legal" 37 | }, 38 | "group": "com.company.internal", 39 | "name": "some name", 40 | "copyright": "Company Legal 2022, all rights reserved", 41 | "version": "1.0" 42 | }, 43 | { 44 | "type": "library", 45 | "bom-ref": "first_component", 46 | "supplier": { 47 | "name": "Company Legal" 48 | }, 49 | "group": "com.company.internal", 50 | "name": "first_component", 51 | "copyright": "Company Legal 2022, all rights reserved", 52 | "version": "e84ec5567d56d2dcf963ed2e454c106d9a9f323b" 53 | }, 54 | { 55 | "type": "library", 56 | "bom-ref": "pkg:nuget/some name@1.3.3", 57 | "publisher": "some publisher", 58 | "name": "some name", 59 | "version": "1.3.2", 60 | "cpe": "", 61 | "description": "some description", 62 | "scope": "required", 63 | "hashes": [ 64 | { 65 | "alg": "SHA-512", 66 | "content": "5F6996E38A31861449A493B9387E91097ABE06F3CA936E618E6B914091699B7319BDA7E392A532A96C06287E9B3C28786183C5FBC212AC2BBDD10809151E6DBB" 67 | } 68 | ], 69 | "licenses": [ 70 | { 71 | "license": { 72 | "id": "MIT" 73 | } 74 | } 75 | ], 76 | "copyright": "Copyright 2000-2021 some name Contributors", 77 | "purl": "pkg:nuget/some name@1.3.2" 78 | } 79 | ], 80 | "dependencies": [ 81 | { 82 | "ref": "some program", 83 | "dependsOn": [ 84 | "first_component", 85 | "second_component", 86 | "third_component", 87 | "fourth_component", 88 | "fifth_component", 89 | "sixth_component", 90 | "seventh_component", 91 | "eight_component", 92 | "ninth_component", 93 | "tenth_component", 94 | "eleventh_component", 95 | "json" 96 | ] 97 | }, 98 | { 99 | "ref": "first_component", 100 | "dependsOn": [] 101 | }, 102 | { 103 | "ref": "second_component", 104 | "dependsOn": [] 105 | }, 106 | { 107 | "ref": "third_component", 108 | "dependsOn": [] 109 | }, 110 | { 111 | "ref": "fourth_component", 112 | "dependsOn": [] 113 | }, 114 | { 115 | "ref": "fifth_component", 116 | "dependsOn": [] 117 | }, 118 | { 119 | "ref": "sixth_component", 120 | "dependsOn": [] 121 | }, 122 | { 123 | "ref": "seventh_component", 124 | "dependsOn": [] 125 | }, 126 | { 127 | "ref": "eight_component", 128 | "dependsOn": [] 129 | }, 130 | { 131 | "ref": "ninth_component", 132 | "dependsOn": [] 133 | }, 134 | { 135 | "ref": "tenth_component", 136 | "dependsOn": [] 137 | }, 138 | { 139 | "ref": "eleventh_component", 140 | "dependsOn": [] 141 | }, 142 | { 143 | "ref": "json", 144 | "dependsOn": [] 145 | } 146 | ], 147 | "compositions": [ 148 | { 149 | "aggregate": "incomplete", 150 | "assemblies": [ 151 | "first_component", 152 | "second_component", 153 | "third_component", 154 | "fourth_component", 155 | "fifth_component", 156 | "sixth_component", 157 | "seventh_component", 158 | "eight_component", 159 | "ninth_component", 160 | "tenth_component", 161 | "eleventh_component", 162 | "json" 163 | ] 164 | } 165 | ] 166 | } 167 | -------------------------------------------------------------------------------- /tests/integration/data/merge.input_1.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.5", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-10-17T19:15:49", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "application", 14 | "bom-ref": "governing_program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.governing", 19 | "name": "governing_program", 20 | "version": "T5.0.3.96", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "name": "company internal" 25 | } 26 | } 27 | ], 28 | "copyright": "Company Legal 2022, all rights reserved" 29 | } 30 | }, 31 | "components": [ 32 | { 33 | "type": "library", 34 | "bom-ref": "sub_program", 35 | "supplier": { 36 | "name": "Company Legal" 37 | }, 38 | "group": "com.company.governing", 39 | "name": "sub_program", 40 | "copyright": "Company Legal 2022, all rights reserved", 41 | "version": "T5.0.3.96" 42 | }, 43 | { 44 | "type": "library", 45 | "bom-ref": "gp_first_component-copy", 46 | "supplier": { 47 | "name": "The first component Contributors" 48 | }, 49 | "name": "gp_first_component", 50 | "version": "2.24.0", 51 | "licenses": [ 52 | { 53 | "license": { 54 | "id": "Apache-2.0" 55 | } 56 | } 57 | ] 58 | }, 59 | { 60 | "type": "library", 61 | "bom-ref": "gov_comp_1", 62 | "name": "gov_comp_1", 63 | "supplier": { 64 | "name": "Company Legal" 65 | }, 66 | "group": "com.company.governing", 67 | "copyright": "Company Legal 2022, all rights reserved", 68 | "version": "5.0.3.96", 69 | "components": [ 70 | { 71 | "type": "library", 72 | "bom-ref": "gov_comp_1_sub_1", 73 | "name": "gov_comp_1_sub_1", 74 | "supplier": { 75 | "name": "Company Legal" 76 | }, 77 | "group": "com.company.governing", 78 | "copyright": "Company Legal 2022, all rights reserved", 79 | "version": "5.0.3.96", 80 | "components": [ 81 | { 82 | "type": "library", 83 | "bom-ref": "gov_comp_1_sub_1_sub_1", 84 | "name": "gov_comp_1_sub_1_sub_1", 85 | "supplier": { 86 | "name": "Company Legal" 87 | }, 88 | "group": "com.company.governing", 89 | "copyright": "Company Legal 2022, all rights reserved", 90 | "version": "5.0.3.96" 91 | } 92 | ] 93 | } 94 | ] 95 | }, 96 | { 97 | "type": "library", 98 | "bom-ref": "gov_comp_2", 99 | "name": "gov_comp_2", 100 | "supplier": { 101 | "name": "Company Legal" 102 | }, 103 | "group": "com.company.governing", 104 | "copyright": "Company Legal 2022, all rights reserved", 105 | "version": "5.0.3.96" 106 | } 107 | ], 108 | "dependencies": [ 109 | { 110 | "ref": "governing_program", 111 | "dependsOn": [ 112 | "sub_program", 113 | "gp_first_component-copy" 114 | ] 115 | }, 116 | { 117 | "ref": "sub_program", 118 | "dependsOn": [] 119 | }, 120 | { 121 | "ref": "gp_first_component-copy", 122 | "dependsOn": [] 123 | } 124 | ], 125 | "compositions": [ 126 | { 127 | "aggregate": "incomplete", 128 | "assemblies": [ 129 | "sub_program", 130 | "gp_first_component-copy" 131 | ] 132 | } 133 | ], 134 | "vulnerabilities": [ 135 | { 136 | "description": "The application is vulnerable to remote SQL injection and shell upload", 137 | "id": "Vul 1", 138 | "ratings": [ 139 | { 140 | "score": 9.8, 141 | "severity": "critical", 142 | "method": "CVSSv31", 143 | "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 144 | }, 145 | { 146 | "score": 7.5, 147 | "severity": "high", 148 | "method": "CVSSv2", 149 | "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" 150 | } 151 | ], 152 | "affects": [ 153 | { 154 | "ref": "sub_program" 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /tests/auxiliary/test_vex/vex.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.4", 4 | "version": 1, 5 | "vulnerabilities": [ 6 | { 7 | "id": "CVE-2020-25649", 8 | "source": { 9 | "name": "NVD", 10 | "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25649" 11 | }, 12 | "references": [ 13 | { 14 | "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302", 15 | "source": { 16 | "name": "SNYK", 17 | "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" 18 | } 19 | } 20 | ], 21 | "ratings": [ 22 | { 23 | "source": { 24 | "name": "NVD", 25 | "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&version=3.1" 26 | }, 27 | "score": 7.5, 28 | "severity": "high", 29 | "method": "CVSSv31", 30 | "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" 31 | }, 32 | { 33 | "source": { 34 | "name": "SNYK", 35 | "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" 36 | }, 37 | "score": 8.2, 38 | "severity": "high", 39 | "method": "CVSSv31", 40 | "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" 41 | }, 42 | { 43 | "source": { 44 | "name": "Acme Inc", 45 | "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" 46 | }, 47 | "score": 0.0, 48 | "severity": "none", 49 | "method": "CVSSv31", 50 | "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" 51 | } 52 | ], 53 | "cwes": [ 54 | 611 55 | ], 56 | "description": "com.fasterxml.jackson.core:jackson-databind is a library which contains the general-purpose data-binding functionality and tree-model for Jackson Data Processor.\n\nAffected versions of this package are vulnerable to XML External Entity (XXE) Injection. A flaw was found in FasterXML Jackson Databind, where it does not have entity expansion secured properly in the DOMDeserializer class. The highest threat from this vulnerability is data integrity.", 57 | "detail": "XXE Injection is a type of attack against an application that parses XML input. XML is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. By default, many XML processors allow specification of an external entity, a URI that is dereferenced and evaluated during XML processing. When an XML document is being parsed, the parser can make a request and include the content at the specified URI inside of the XML document.\n\nAttacks can include disclosing local files, which may contain sensitive data such as passwords or private user data, using file: schemes or relative paths in the system identifier.", 58 | "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.6.7.4, 2.9.10.7, 2.10.5.1 or higher.", 59 | "advisories": [ 60 | { 61 | "title": "GitHub Commit", 62 | "url": "https://github.com/FasterXML/jackson-databind/commit/612f971b78c60202e9cd75a299050c8f2d724a59" 63 | }, 64 | { 65 | "title": "GitHub Issue", 66 | "url": "https://github.com/FasterXML/jackson-databind/issues/2589" 67 | }, 68 | { 69 | "title": "RedHat Bugzilla Bug", 70 | "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1887664" 71 | } 72 | ], 73 | "created": "2020-12-03T00:00:00.000Z", 74 | "published": "2020-12-03T00:00:00.000Z", 75 | "updated": "2021-10-26T00:00:00.000Z", 76 | "credits": { 77 | "individuals": [ 78 | { 79 | "name": "Bartosz Baranowski" 80 | } 81 | ] 82 | }, 83 | "analysis": { 84 | "state": "not_affected", 85 | "justification": "code_not_reachable", 86 | "response": ["will_not_fix", "update"], 87 | "detail": "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly." 88 | }, 89 | "affects": [ 90 | { 91 | "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.10.0?type=jar" 92 | } 93 | ] 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /tests/auxiliary/test_set_sboms/Acme_Application_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-02-17T10:14:58Z", 8 | "authors": [ 9 | { 10 | "name": "automated" 11 | } 12 | ], 13 | "component": { 14 | "type": "application", 15 | "bom-ref": "acme-app", 16 | "group": "com.acme.internal", 17 | "supplier": { 18 | "name": "Festo SE & Co. KG" 19 | }, 20 | "name": "Acme_Application", 21 | "version": "9.1.1", 22 | "copyright": "Festo SE & Co. KG 2022, all rights reserved", 23 | "hashes": [ 24 | { 25 | "alg": "MD5", 26 | "content": "ec7781220ec7781220ec778122012345" 27 | } 28 | ], 29 | "properties": [ 30 | { 31 | "name": "internal:component:status", 32 | "value": "internal" 33 | } 34 | ] 35 | } 36 | }, 37 | "components": [ 38 | { 39 | "group": "org.acme", 40 | "name": "web-framework", 41 | "version": "1.0.0", 42 | "type": "library", 43 | "bom-ref": "pkg:maven/org.acme/web-framework@1.0.0", 44 | "supplier": { 45 | "name": "Acme, Inc." 46 | }, 47 | "licenses": [ 48 | { 49 | "license": { 50 | "id": "Apache-1.0" 51 | } 52 | } 53 | ] 54 | }, 55 | { 56 | "group": "org.acme", 57 | "name": "web-framework", 58 | "version": "2.0.0", 59 | "type": "library", 60 | "bom-ref": "pkg:maven/org.acme/web-framework@2.0.0", 61 | "supplier": { 62 | "name": "Acme, Inc." 63 | }, 64 | "licenses": [ 65 | { 66 | "license": { 67 | "id": "Apache-1.0" 68 | } 69 | } 70 | ] 71 | }, 72 | { 73 | "group": "org.acme", 74 | "name": "web-framework", 75 | "version": "3.0.0", 76 | "type": "library", 77 | "bom-ref": "pkg:maven/org.acme/web-framework@3.0.0", 78 | "supplier": { 79 | "name": "Acme, Inc." 80 | }, 81 | "licenses": [ 82 | { 83 | "license": { 84 | "id": "Apache-1.0" 85 | } 86 | } 87 | ] 88 | }, 89 | { 90 | "group": "org.acme", 91 | "name": "web-framework", 92 | "version": "4.0.0", 93 | "type": "library", 94 | "bom-ref": "pkg:maven/org.acme/web-framework@4.0.0", 95 | "supplier": { 96 | "name": "Acme, Inc." 97 | }, 98 | "licenses": [ 99 | { 100 | "license": { 101 | "id": "Apache-1.0" 102 | } 103 | } 104 | ] 105 | }, 106 | { 107 | "group": "org.acme", 108 | "name": "web-framework", 109 | "version": "4.1.0", 110 | "type": "library", 111 | "bom-ref": "pkg:maven/org.acme/web-framework@4.1.0", 112 | "supplier": { 113 | "name": "Acme, Inc." 114 | }, 115 | "licenses": [ 116 | { 117 | "license": { 118 | "id": "Apache-1.0" 119 | } 120 | } 121 | ] 122 | }, 123 | { 124 | "group": "org.acme", 125 | "name": "web-framework", 126 | "version": "4.1.9", 127 | "type": "library", 128 | "bom-ref": "pkg:maven/org.acme/web-framework@4.1.9", 129 | "supplier": { 130 | "name": "Acme, Inc." 131 | }, 132 | "licenses": [ 133 | { 134 | "license": { 135 | "id": "Apache-1.0" 136 | } 137 | } 138 | ] 139 | } 140 | ], 141 | "dependencies": [ 142 | { 143 | "ref": "acme-app", 144 | "dependsOn": [ 145 | "pkg:maven/org.acme/web-framework@1.0.0", 146 | "pkg:maven/org.acme/web-framework@2.0.0", 147 | "pkg:maven/org.acme/web-framework@3.0.0", 148 | "pkg:maven/org.acme/web-framework@4.0.0", 149 | "pkg:maven/org.acme/web-framework@4.1.0", 150 | "pkg:maven/org.acme/web-framework@4.1.9" 151 | ] 152 | } 153 | ], 154 | "compositions": [ 155 | { 156 | "aggregate": "incomplete", 157 | "assemblies": [ 158 | "pkg:maven/org.acme/web-framework@1.0.0", 159 | "pkg:maven/org.acme/web-framework@2.0.0", 160 | "pkg:maven/org.acme/web-framework@3.0.0", 161 | "pkg:maven/org.acme/web-framework@4.0.0", 162 | "pkg:maven/org.acme/web-framework@4.1.0", 163 | "pkg:maven/org.acme/web-framework@4.1.9" 164 | ] 165 | } 166 | ] 167 | } 168 | -------------------------------------------------------------------------------- /tests/auxiliary/test_amend_sboms/bom_licenses_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-09-03T01:06:14", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "library", 14 | "bom-ref": "some program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.internal", 19 | "name": "some program", 20 | "version": "T5.0.1.25", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "name": "company internal" 25 | } 26 | }, 27 | { 28 | "license": { 29 | "name": "company internal 2" 30 | } 31 | }, 32 | { 33 | "license": { 34 | "name": "Apache-2.0" 35 | } 36 | } 37 | ], 38 | "copyright": "Company Legal 2022, all rights reserved" 39 | } 40 | }, 41 | "components": [ 42 | { 43 | "type": "library", 44 | "bom-ref": "11231231", 45 | "supplier": { 46 | "name": "Company Legal" 47 | }, 48 | "group": "com.company.internal", 49 | "name": "some name", 50 | "copyright": "Company Legal 2022, all rights reserved", 51 | "version": "1.0" 52 | }, 53 | { 54 | "type": "library", 55 | "bom-ref": "first_component", 56 | "supplier": { 57 | "name": "Company Legal" 58 | }, 59 | "group": "com.company.internal", 60 | "name": "first_component", 61 | "copyright": "Company Legal 2022, all rights reserved", 62 | "version": "e84ec5567d56d2dcf963ed2e454c106d9a9f323b" 63 | }, 64 | { 65 | "type": "library", 66 | "bom-ref": "pkg:nuget/some name@1.3.3", 67 | "publisher": "some publisher", 68 | "name": "some name", 69 | "version": "1.3.2", 70 | "cpe": "", 71 | "description": "some description", 72 | "scope": "required", 73 | "hashes": [ 74 | { 75 | "alg": "SHA-512", 76 | "content": "5F6996E38A31861449A493B9387E91097ABE06F3CA936E618E6B914091699B7319BDA7E392A532A96C06287E9B3C28786183C5FBC212AC2BBDD10809151E6DBB" 77 | } 78 | ], 79 | "licenses": [ 80 | { 81 | "license": { 82 | "name": "License 2" 83 | } 84 | } 85 | ], 86 | "copyright": "Copyright 2000-2021 some name Contributors", 87 | "purl": "pkg:nuget/some name@1.3.2" 88 | } 89 | ], 90 | "dependencies": [ 91 | { 92 | "ref": "some program", 93 | "dependsOn": [ 94 | "first_component", 95 | "second_component", 96 | "third_component", 97 | "fourth_component", 98 | "fifth_component", 99 | "sixth_component", 100 | "seventh_component", 101 | "eight_component", 102 | "ninth_component", 103 | "tenth_component", 104 | "eleventh_component", 105 | "json" 106 | ] 107 | }, 108 | { 109 | "ref": "first_component", 110 | "dependsOn": [] 111 | }, 112 | { 113 | "ref": "second_component", 114 | "dependsOn": [] 115 | }, 116 | { 117 | "ref": "third_component", 118 | "dependsOn": [] 119 | }, 120 | { 121 | "ref": "fourth_component", 122 | "dependsOn": [] 123 | }, 124 | { 125 | "ref": "fifth_component", 126 | "dependsOn": [] 127 | }, 128 | { 129 | "ref": "sixth_component", 130 | "dependsOn": [] 131 | }, 132 | { 133 | "ref": "seventh_component", 134 | "dependsOn": [] 135 | }, 136 | { 137 | "ref": "eight_component", 138 | "dependsOn": [] 139 | }, 140 | { 141 | "ref": "ninth_component", 142 | "dependsOn": [] 143 | }, 144 | { 145 | "ref": "tenth_component", 146 | "dependsOn": [] 147 | }, 148 | { 149 | "ref": "eleventh_component", 150 | "dependsOn": [] 151 | }, 152 | { 153 | "ref": "json", 154 | "dependsOn": [] 155 | } 156 | ], 157 | "compositions": [ 158 | { 159 | "aggregate": "incomplete", 160 | "assemblies": [ 161 | "first_component", 162 | "second_component", 163 | "third_component", 164 | "fourth_component", 165 | "fifth_component", 166 | "sixth_component", 167 | "seventh_component", 168 | "eight_component", 169 | "ninth_component", 170 | "tenth_component", 171 | "eleventh_component", 172 | "json" 173 | ] 174 | } 175 | ] 176 | } 177 | -------------------------------------------------------------------------------- /tests/auxiliary/test_amend_sboms/bom_licenses_changed_with_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "version": 1, 5 | "metadata": { 6 | "timestamp": "2022-09-03T01:06:14", 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "library", 14 | "bom-ref": "some program", 15 | "supplier": { 16 | "name": "Company Legal" 17 | }, 18 | "group": "com.company.internal", 19 | "name": "some program", 20 | "version": "T5.0.1.25", 21 | "licenses": [ 22 | { 23 | "license": { 24 | "id": "Test Name to Licenses" 25 | } 26 | }, 27 | { 28 | "license": { 29 | "id": "Test Name to Licenses 3" 30 | } 31 | }, 32 | { 33 | "license": { 34 | "id": "Apache-2.0" 35 | } 36 | } 37 | ], 38 | "copyright": "Company Legal 2022, all rights reserved" 39 | } 40 | }, 41 | "components": [ 42 | { 43 | "type": "library", 44 | "bom-ref": "11231231", 45 | "supplier": { 46 | "name": "Company Legal" 47 | }, 48 | "group": "com.company.internal", 49 | "name": "some name", 50 | "copyright": "Company Legal 2022, all rights reserved", 51 | "version": "1.0" 52 | }, 53 | { 54 | "type": "library", 55 | "bom-ref": "first_component", 56 | "supplier": { 57 | "name": "Company Legal" 58 | }, 59 | "group": "com.company.internal", 60 | "name": "first_component", 61 | "copyright": "Company Legal 2022, all rights reserved", 62 | "version": "e84ec5567d56d2dcf963ed2e454c106d9a9f323b" 63 | }, 64 | { 65 | "type": "library", 66 | "bom-ref": "pkg:nuget/some name@1.3.3", 67 | "publisher": "some publisher", 68 | "name": "some name", 69 | "version": "1.3.2", 70 | "cpe": "", 71 | "description": "some description", 72 | "scope": "required", 73 | "hashes": [ 74 | { 75 | "alg": "SHA-512", 76 | "content": "5F6996E38A31861449A493B9387E91097ABE06F3CA936E618E6B914091699B7319BDA7E392A532A96C06287E9B3C28786183C5FBC212AC2BBDD10809151E6DBB" 77 | } 78 | ], 79 | "licenses": [ 80 | { 81 | "license": { 82 | "id": "Test Name to Licenses 2" 83 | } 84 | } 85 | ], 86 | "copyright": "Copyright 2000-2021 some name Contributors", 87 | "purl": "pkg:nuget/some name@1.3.2" 88 | } 89 | ], 90 | "dependencies": [ 91 | { 92 | "ref": "some program", 93 | "dependsOn": [ 94 | "first_component", 95 | "second_component", 96 | "third_component", 97 | "fourth_component", 98 | "fifth_component", 99 | "sixth_component", 100 | "seventh_component", 101 | "eight_component", 102 | "ninth_component", 103 | "tenth_component", 104 | "eleventh_component", 105 | "json" 106 | ] 107 | }, 108 | { 109 | "ref": "first_component", 110 | "dependsOn": [] 111 | }, 112 | { 113 | "ref": "second_component", 114 | "dependsOn": [] 115 | }, 116 | { 117 | "ref": "third_component", 118 | "dependsOn": [] 119 | }, 120 | { 121 | "ref": "fourth_component", 122 | "dependsOn": [] 123 | }, 124 | { 125 | "ref": "fifth_component", 126 | "dependsOn": [] 127 | }, 128 | { 129 | "ref": "sixth_component", 130 | "dependsOn": [] 131 | }, 132 | { 133 | "ref": "seventh_component", 134 | "dependsOn": [] 135 | }, 136 | { 137 | "ref": "eight_component", 138 | "dependsOn": [] 139 | }, 140 | { 141 | "ref": "ninth_component", 142 | "dependsOn": [] 143 | }, 144 | { 145 | "ref": "tenth_component", 146 | "dependsOn": [] 147 | }, 148 | { 149 | "ref": "eleventh_component", 150 | "dependsOn": [] 151 | }, 152 | { 153 | "ref": "json", 154 | "dependsOn": [] 155 | } 156 | ], 157 | "compositions": [ 158 | { 159 | "aggregate": "incomplete", 160 | "assemblies": [ 161 | "first_component", 162 | "second_component", 163 | "third_component", 164 | "fourth_component", 165 | "fifth_component", 166 | "sixth_component", 167 | "seventh_component", 168 | "eight_component", 169 | "ninth_component", 170 | "tenth_component", 171 | "eleventh_component", 172 | "json" 173 | ] 174 | } 175 | ] 176 | } 177 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '24 10 * * 0' 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze (${{ matrix.language }}) 28 | # Runner size impacts CodeQL analysis time. To learn more, please see: 29 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 30 | # - https://gh.io/supported-runners-and-hardware-resources 31 | # - https://gh.io/using-larger-runners (GitHub.com only) 32 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 33 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 34 | permissions: 35 | # required for all workflows 36 | security-events: write 37 | 38 | # required to fetch internal or private CodeQL packs 39 | packages: read 40 | 41 | # only required for workflows in private repositories 42 | actions: read 43 | contents: read 44 | 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | include: 49 | - language: python 50 | build-mode: none 51 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 52 | # Use `c-cpp` to analyze code written in C, C++ or both 53 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 54 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 55 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 56 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 57 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 58 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 59 | steps: 60 | - name: Checkout repository 61 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 62 | 63 | # Initializes the CodeQL tools for scanning. 64 | - name: Initialize CodeQL 65 | uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 66 | with: 67 | languages: ${{ matrix.language }} 68 | build-mode: ${{ matrix.build-mode }} 69 | # If you wish to specify custom queries, you can do so here or in a config file. 70 | # By default, queries listed here will override any specified in a config file. 71 | # Prefix the list here with "+" to use these queries and those in the config file. 72 | 73 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 74 | # queries: security-extended,security-and-quality 75 | 76 | # If the analyze step fails for one of the languages you are analyzing with 77 | # "We were unable to automatically build your code", modify the matrix above 78 | # to set the build mode to "manual" for that language. Then modify this step 79 | # to build your code. 80 | # ℹ️ Command-line programs to run using the OS shell. 81 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 82 | - if: matrix.build-mode == 'manual' 83 | shell: bash 84 | run: | 85 | echo 'If you are using a "manual" build mode for one or more of the' \ 86 | 'languages you are analyzing, replace this with the commands to build' \ 87 | 'your code, for example:' 88 | echo ' make bootstrap' 89 | echo ' make release' 90 | exit 1 91 | 92 | - name: Perform CodeQL Analysis 93 | uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 94 | with: 95 | category: "/language:${{matrix.language}}" 96 | -------------------------------------------------------------------------------- /cdxev/auxiliary/filename_gen.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import logging 4 | import re 5 | import typing as t 6 | import unicodedata 7 | from datetime import datetime, timezone 8 | 9 | from dateutil.parser import isoparse 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def generate_filename(sbom: dict) -> str: 15 | """ 16 | Generates a filename for the given SBOM. 17 | 18 | If the SBOM doesn't contain any of the required metadata to generate a unique filename, 19 | it will default to ``bom.json``. 20 | 21 | :param dict sbom: The SBOM to generate a filename for. 22 | 23 | :return: The filename. 24 | """ 25 | name = sbom.get("metadata", {}).get("component", {}).get("name", "") 26 | name = _sanitize(name) 27 | version = sbom.get("metadata", {}).get("component", {}).get("version", "") 28 | version = _sanitize(version) 29 | timestamp_str: t.Union[str, None] = sbom.get("metadata", {}).get("timestamp") 30 | 31 | if not name and not version and not timestamp_str: 32 | return "bom.json" 33 | 34 | try: 35 | timestamp = isoparse(timestamp_str) # type: ignore # because type errors are caught below 36 | except (ValueError, TypeError): 37 | logger.info( 38 | "SBOM has no or an unparsable timestamp. Using current time in filename." 39 | ) 40 | timestamp = datetime.now(timezone.utc) 41 | 42 | name = name or "unknown" 43 | timestamp_str = _timestamp_to_utc_str(timestamp) 44 | 45 | components = [name] 46 | if version: 47 | components.append(version) 48 | 49 | components.append(timestamp_str) 50 | 51 | return "_".join(components) + ".cdx.json" 52 | 53 | 54 | def generate_validation_pattern(sbom: dict) -> str: 55 | """ 56 | Creates a regular expression which can be used to validate the filename of the given SBOM. 57 | 58 | The pattern allows the following variants of the filename: 59 | 60 | * ``bom.json`` is always an allowed name. 61 | * ``_[_][_||].cdx.json``, where 62 | * ```` == ``metadata.component.name``, if it exists, otherwise ``unknown``. 63 | * ```` MUST be present, if and only if ``metadata.component.version`` exists. 64 | * ```` == ``metadata.component.hashes[x].content`` for any index x. 65 | 66 | :param dict sbom: The SBOM whose filename to validate. 67 | 68 | :return: A regular expression. 69 | """ 70 | regex = "bom\\.json|" 71 | 72 | name = sbom.get("metadata", {}).get("component", {}).get("name", "unknown") 73 | name = _sanitize(name) 74 | regex += re.escape(name) + "_" 75 | 76 | version = sbom.get("metadata", {}).get("component", {}).get("version", "") 77 | version = _sanitize(version) 78 | if version: 79 | regex += re.escape(version) + "_" 80 | 81 | timestamp = sbom.get("metadata", {}).get("timestamp") 82 | if timestamp: 83 | try: 84 | timestamp_regex = _timestamp_to_utc_str(isoparse(timestamp)) 85 | except: 86 | timestamp_regex = "[0-9]{8}T[0-9]{6}" 87 | else: 88 | timestamp_regex = "[0-9]{8}T[0-9]{6}" 89 | 90 | hashes = [ 91 | hash["content"] 92 | for hash in sbom.get("metadata", {}).get("component", {}).get("hashes", []) 93 | ] 94 | hashes_regex = "(" + "|".join(hashes) + ")" 95 | 96 | if not hashes: 97 | regex += timestamp_regex 98 | else: 99 | regex += f"({hashes_regex}|{hashes_regex}_{timestamp_regex}|{timestamp_regex})" 100 | 101 | regex += "\\.cdx\\.json" 102 | 103 | return regex 104 | 105 | 106 | def _timestamp_to_utc_str(timestamp: datetime) -> str: 107 | """ 108 | Converts a timestamp to the string format used in filenames. 109 | 110 | The timestamp will be converted to UTC if it isn't already. 111 | 112 | :param datetime timestamp: The timestamp to convert. 113 | 114 | :return: The string representation for use in the filename. 115 | """ 116 | timestamp = timestamp.astimezone(timezone.utc) 117 | return timestamp.strftime("%Y%m%dT%H%M%S") 118 | 119 | 120 | def _sanitize(s: str) -> str: 121 | """ 122 | Converts a string to a representation safe for filenames. 123 | 124 | The following transformations are done: 125 | * Normalize Unicode 126 | * Filter out any characters which aren't alphanumeric, spaces, dashes, periods or underscores. 127 | * Strip any leading or trailing non-alphanumeric characters. 128 | 129 | :param str s: The original string. 130 | 131 | :return: A representation safe for use as a filename. 132 | """ 133 | s = unicodedata.normalize("NFKC", s) 134 | s = "".join(c for c in s if (c.isalnum() or c in r" .-_")) 135 | s.strip(r" .-_") 136 | return s 137 | -------------------------------------------------------------------------------- /docs/source/usage/build-public.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | build-public 3 | ============ 4 | 5 | .. argparse:: 6 | :filename: ./cdxev/__main__.py 7 | :func: create_parser 8 | :prog: cdx-ev 9 | :path: build-public 10 | 11 | This command creates a redacted version of an SBOM fit for publication. It: 12 | 13 | * deletes any *property* (i.e., item in the ``properties`` array of a component) whose name starts with ``internal:`` from all components, 14 | * deletes any ``externalReferences`` that are marked as internal by the regex pattern specified by the user, if provided, 15 | * can optionally delete entire components matching a JSON schema provided by the user. 16 | 17 | For the deletion of entire components *internal* properties and external references will be taken into account when matching the JSON schema. 18 | If a component containing nested components is deleted, those nested components are deleted as well. 19 | 20 | .. warning:: 21 | The ``metadata.component`` will not be removed even when the JSON schema applies to it. 22 | This is because the ``metadata.component`` is the component the BOM describes, therefore removing it, would make the SBOM ambiguous. 23 | If the schema applies to the ``metadata.component``, the SBOM is likely not intended for public use. 24 | However, this behavior does not affect the deletion of any property, which starts with the name ``internal:``. 25 | 26 | The JSON schema must be formulated according to the Draft 7 specification. 27 | 28 | Dependency-resolution 29 | --------------------- 30 | 31 | Any components deleted by this command are equally removed from the dependency graph. Their dependencies are assigned as new dependencies to their dependents. 32 | 33 | .. image:: /img/dependency-resolution.svg 34 | :alt: Dependencies of deleted components are assigned to their dependents. 35 | 36 | Examples 37 | -------- 38 | 39 | Here are some JSON schemata for common scenarios to get you started. 40 | 41 | When passed to the command, this schema will remove any component whose ``group`` is ``com.acme.internal``:: 42 | 43 | { 44 | "properties": { 45 | "group": { 46 | "const": "com.acme.internal" 47 | } 48 | }, 49 | "required": ["group"] 50 | } 51 | 52 | An extension of the above, the next schema will delete any component with that ``group``, *unless* it contains a property with the name ``internal:public`` and the value ``true``. *Note that the property itself will still be removed from the component, because its name starts with* ``internal:``. 53 | :: 54 | 55 | { 56 | "properties": { 57 | "group": { 58 | "const": "com.acme.internal" 59 | } 60 | }, 61 | "required": ["group"], 62 | "not": { 63 | "properties": { 64 | "properties": { 65 | "contains": { 66 | "properties": { 67 | "name": { 68 | "const": "internal:public" 69 | }, 70 | "value": { 71 | "const": "true" 72 | } 73 | }, 74 | "required": ["name", "value"] 75 | } 76 | } 77 | }, 78 | "required": ["properties"] 79 | } 80 | } 81 | 82 | This schema will delete the three components with the names ``AcmeSecret``, ``AcmeNotPublic`` and ``AcmeSensitive``:: 83 | 84 | { 85 | "properties": { 86 | "name": { 87 | "enum": ["AcmeSecret", "AcmeNotPublic", "AcmeSensitive"] 88 | } 89 | }, 90 | "required": ["name"] 91 | } 92 | 93 | The following schema is a little more involved. It will delete any component whose license text contains the string ``This must not be made public``:: 94 | 95 | { 96 | "properties": { 97 | "licenses": { 98 | "contains": { 99 | "properties": { 100 | "license": { 101 | "properties": { 102 | "text": { 103 | "properties": { 104 | "content": { 105 | "pattern": "This must not be made public" 106 | } 107 | } 108 | } 109 | }, 110 | "required": ["text"] 111 | } 112 | }, 113 | "required": ["license"] 114 | } 115 | } 116 | }, 117 | "required": ["licenses"] 118 | } 119 | -------------------------------------------------------------------------------- /tests/auxiliary/helper.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import json 4 | import typing as t 5 | 6 | 7 | def compare_sboms(first_sbom: dict, second_sbom: dict) -> bool: 8 | """ 9 | Compares two sboms while ignoring the timestamp. 10 | The order of elements contained in the sbom 11 | affects the result. 12 | 13 | Parameters 14 | ---------- 15 | :first_sbom: dict 16 | A sbom dictionary 17 | :second_sbom: dict 18 | A sbom dictionary 19 | 20 | Returns 21 | ------- 22 | bool: 23 | Returns True if the sboms are 24 | identical except of the timestamp, 25 | False if not 26 | """ 27 | # Make copies so that we don't modify the original dicts 28 | first_sbom = dict(first_sbom) 29 | second_sbom = dict(second_sbom) 30 | 31 | # Isolate metadata out of the sboms because we have to remove the timestamp from it. 32 | metadata1 = first_sbom.pop("metadata", {}) 33 | metadata2 = second_sbom.pop("metadata", {}) 34 | 35 | metadata1.pop("timestamp", None) 36 | metadata2.pop("timestamp", None) 37 | 38 | return metadata1 == metadata2 and first_sbom == second_sbom 39 | 40 | 41 | def compare_list_content(first_list: list, second_list: list) -> bool: 42 | """ 43 | Compares the contents of two lists 44 | while ignoring the order of those 45 | elements in the respective lists. 46 | Basically treats lists as sets. 47 | 48 | Parameters 49 | ---------- 50 | first_list: list 51 | A list 52 | second_list: list 53 | A list 54 | 55 | Returns 56 | ------- 57 | bool: 58 | Returns True if the content of the lists 59 | is identical 60 | """ 61 | is_equal = True 62 | if len(first_list) == len(second_list): 63 | for element in first_list: 64 | if element not in second_list: 65 | is_equal = False 66 | else: 67 | is_equal = False 68 | return is_equal 69 | 70 | 71 | def search_entry(haystack: dict, key: t.Any, value: t.Any) -> t.Optional[dict]: 72 | """ 73 | Recursively searches a dict of dicts for a specific key/value pair. 74 | 75 | :param haystack: A dict of dicts (any other values). 76 | :param key: The key of the entry to search. 77 | :param value: The value of the entry to search. 78 | :return: The sub-dict of haystack which contains the key/value pair or None if not found. 79 | """ 80 | 81 | def _recurse(d: dict) -> t.Optional[dict]: 82 | for k, v in d.items(): 83 | if key == k and value == v: 84 | return d 85 | elif isinstance(v, dict): 86 | return _recurse(v) 87 | elif isinstance(v, list): 88 | for i in v: 89 | if isinstance(i, dict): 90 | found = _recurse(i) 91 | if found is not None: 92 | return found 93 | return None 94 | 95 | return _recurse(haystack) 96 | 97 | 98 | # Import functions for json files used in test_merge and test_sbom_functions 99 | path_to_folder_with_test_sboms = "tests/auxiliary/test_merge_sboms/" 100 | 101 | 102 | def get_ratings_dict() -> dict: 103 | with open( 104 | path_to_folder_with_test_sboms + "ratings_lists_for_tests.json", 105 | "r", 106 | encoding="utf-8-sig", 107 | ) as my_file: 108 | return json.load(my_file) 109 | 110 | 111 | def get_dictionary_with_stuff() -> dict: 112 | with open( 113 | path_to_folder_with_test_sboms + "sections_for_test_sbom.json", 114 | "r", 115 | encoding="utf-8-sig", 116 | ) as my_file: 117 | return json.load(my_file) 118 | 119 | 120 | def load_governing_program() -> dict: 121 | with open( 122 | path_to_folder_with_test_sboms + "governing_program.json", 123 | "r", 124 | encoding="utf-8-sig", 125 | ) as my_file: 126 | sbom = json.load(my_file) 127 | return sbom 128 | 129 | 130 | def load_sections_for_test_sbom() -> dict: 131 | with open( 132 | path_to_folder_with_test_sboms + "sections_for_test_sbom.json", 133 | "r", 134 | encoding="utf-8-sig", 135 | ) as my_file: 136 | sbom = json.load(my_file) 137 | return sbom 138 | 139 | 140 | def load_governing_program_merged_sub_program() -> dict: 141 | with open( 142 | path_to_folder_with_test_sboms + "merged_sbom.json", 143 | "r", 144 | encoding="utf-8-sig", 145 | ) as my_file: 146 | sbom = json.load(my_file) 147 | return sbom 148 | 149 | 150 | def load_sub_program() -> dict: 151 | with open( 152 | path_to_folder_with_test_sboms + "sub_program.json", 153 | "r", 154 | encoding="utf-8-sig", 155 | ) as my_file: 156 | sbom = json.load(my_file) 157 | return sbom 158 | 159 | 160 | def load_additional_sbom_dict() -> dict: 161 | with open( 162 | path_to_folder_with_test_sboms + "additional_sboms.json", 163 | "r", 164 | encoding="utf-8-sig", 165 | ) as my_file: 166 | sbom = json.load(my_file) 167 | return sbom 168 | -------------------------------------------------------------------------------- /tests/test_initialize_sbom.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import unittest 4 | 5 | from cdxev import pkg 6 | from cdxev.initialize_sbom import initialize_sbom 7 | 8 | 9 | class TestInitializeSbom(unittest.TestCase): 10 | def test_no_arguments_given(self) -> None: 11 | sbom = initialize_sbom( 12 | software_name=None, authors=None, supplier=None, version=None 13 | ) 14 | self.assertEqual( 15 | sbom["metadata"]["component"]["name"], 16 | "The name of the component described by the SBOM.", 17 | ) 18 | self.assertEqual( 19 | sbom["metadata"]["component"]["supplier"]["name"], 20 | "The name of the organization that supplied the component.", 21 | ) 22 | self.assertEqual( 23 | sbom["metadata"]["component"]["version"], "The component version." 24 | ) 25 | self.assertEqual( 26 | sbom["metadata"]["authors"][0]["name"], "The person who created the SBOM." 27 | ) 28 | self.assertEqual(sbom["metadata"]["tools"][0]["version"], pkg.VERSION) 29 | 30 | def test_name_argument_given(self) -> None: 31 | sbom = initialize_sbom( 32 | software_name="xyz", 33 | authors=None, 34 | supplier=None, 35 | version=None, 36 | ) 37 | self.assertEqual(sbom["metadata"]["component"]["name"], "xyz") 38 | self.assertEqual( 39 | sbom["metadata"]["component"]["supplier"]["name"], 40 | "The name of the organization that supplied the component.", 41 | ) 42 | self.assertEqual( 43 | sbom["metadata"]["component"]["version"], "The component version." 44 | ) 45 | self.assertEqual( 46 | sbom["metadata"]["authors"][0]["name"], "The person who created the SBOM." 47 | ) 48 | 49 | def test_authors_arguments_given(self) -> None: 50 | sbom = initialize_sbom( 51 | software_name=None, 52 | authors="xyz", 53 | supplier=None, 54 | version=None, 55 | ) 56 | self.assertEqual( 57 | sbom["metadata"]["component"]["name"], 58 | "The name of the component described by the SBOM.", 59 | ) 60 | self.assertEqual( 61 | sbom["metadata"]["component"]["supplier"]["name"], 62 | "The name of the organization that supplied the component.", 63 | ) 64 | self.assertEqual( 65 | sbom["metadata"]["component"]["version"], "The component version." 66 | ) 67 | self.assertEqual(sbom["metadata"]["authors"][0]["name"], "xyz") 68 | 69 | def test_supplier_arguments_given(self) -> None: 70 | sbom = initialize_sbom( 71 | software_name=None, 72 | authors=None, 73 | supplier="xyz", 74 | version=None, 75 | ) 76 | self.assertEqual( 77 | sbom["metadata"]["component"]["name"], 78 | "The name of the component described by the SBOM.", 79 | ) 80 | self.assertEqual(sbom["metadata"]["component"]["supplier"]["name"], "xyz") 81 | self.assertEqual( 82 | sbom["metadata"]["component"]["version"], "The component version." 83 | ) 84 | self.assertEqual( 85 | sbom["metadata"]["authors"][0]["name"], "The person who created the SBOM." 86 | ) 87 | 88 | def test_version_arguments_given(self) -> None: 89 | sbom = initialize_sbom( 90 | software_name=None, authors=None, supplier=None, version="xyz" 91 | ) 92 | self.assertEqual( 93 | sbom["metadata"]["component"]["name"], 94 | "The name of the component described by the SBOM.", 95 | ) 96 | self.assertEqual( 97 | sbom["metadata"]["component"]["supplier"]["name"], 98 | "The name of the organization that supplied the component.", 99 | ) 100 | self.assertEqual(sbom["metadata"]["component"]["version"], "xyz") 101 | self.assertEqual( 102 | sbom["metadata"]["authors"][0]["name"], "The person who created the SBOM." 103 | ) 104 | 105 | def test_email_arguments_given(self) -> None: 106 | sbom = initialize_sbom( 107 | software_name=None, 108 | authors=None, 109 | supplier=None, 110 | version=None, 111 | email="test@test.com", 112 | ) 113 | self.assertEqual(sbom["metadata"]["authors"][0]["email"], "test@test.com") 114 | 115 | def test_email_arguments_not_given(self) -> None: 116 | sbom = initialize_sbom( 117 | software_name=None, authors=None, supplier=None, version=None 118 | ) 119 | self.assertEqual(sbom["metadata"]["authors"][0].get("email", None), None) 120 | 121 | def test_invalid_email(self) -> None: 122 | with self.assertRaises(ValueError): 123 | initialize_sbom( 124 | software_name=None, 125 | authors=None, 126 | supplier=None, 127 | version=None, 128 | email="notValidMail.com", 129 | ) 130 | -------------------------------------------------------------------------------- /tests/integration/data/merge.vex.input_1.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.4", 4 | "version": 1, 5 | "vulnerabilities": [ 6 | { 7 | "id": "Vulnerability 1", 8 | "source": { 9 | "name": "NVD", 10 | "url": "https://nvd.nist.gov/vuln/detail/Vulnerability 1" 11 | }, 12 | "references": [ 13 | { 14 | "id": "Vulnerability 1: Alias 1" 15 | } 16 | ], 17 | "affects": [ 18 | { 19 | "ref": "product 1", 20 | "versions": [ 21 | { 22 | "version": "2.4", 23 | "status": "affected" 24 | }, 25 | { 26 | "version": "2.6", 27 | "status": "affected" 28 | }, 29 | { 30 | "range": "vers:generic/>=2.9|<=4.1", 31 | "status": "affected" 32 | } 33 | ] 34 | }, 35 | { 36 | "ref": "product 2", 37 | "versions": [ 38 | { 39 | "version": "3.4", 40 | "status": "affected" 41 | } 42 | ] 43 | } 44 | ] 45 | }, 46 | { 47 | "id": "Vulnerability 1: Alias 2", 48 | "source": { 49 | "name": "NVD", 50 | "url": "https://nvd.nist.gov/vuln/detail/Vulnerability 1: Alias 2" 51 | }, 52 | "references": [ 53 | { 54 | "id": "Vulnerability 1: Alias 1" 55 | } 56 | ], 57 | "affects": [ 58 | { 59 | "ref": "product 3", 60 | "versions": [ 61 | { 62 | "version": "2.4", 63 | "status": "affected" 64 | }, 65 | { 66 | "version": "2.6", 67 | "status": "affected" 68 | }, 69 | { 70 | "range": "vers:generic/>=2.9|<=4.1", 71 | "status": "affected" 72 | } 73 | ] 74 | }, 75 | { 76 | "ref": "product 4", 77 | "versions": [ 78 | { 79 | "version": "3.4", 80 | "status": "affected" 81 | } 82 | ] 83 | }, 84 | { 85 | "ref": "product 3", 86 | "versions": [ 87 | { 88 | "version": "10", 89 | "status": "affected" 90 | } 91 | ] 92 | }, 93 | { 94 | "ref": "product 3", 95 | "versions": [ 96 | { 97 | "version": "11", 98 | "status": "affected" 99 | } 100 | ] 101 | } 102 | ] 103 | }, 104 | { 105 | "id": "Vulnerability 2", 106 | "source": { 107 | "name": "NVD", 108 | "url": "https://nvd.nist.gov/vuln/detail/Vulnerability 2" 109 | }, 110 | "affects": [ 111 | { 112 | "ref": "product 5", 113 | "versions": [ 114 | { 115 | "version": "2.4", 116 | "status": "affected" 117 | }, 118 | { 119 | "version": "2.6", 120 | "status": "affected" 121 | }, 122 | { 123 | "range": "vers:generic/>=2.9|<=4.1", 124 | "status": "affected" 125 | } 126 | ] 127 | }, 128 | { 129 | "ref": "product 6", 130 | "versions": [ 131 | { 132 | "version": "3.4", 133 | "status": "affected" 134 | } 135 | ] 136 | } 137 | ] 138 | }, 139 | { 140 | "id": "Vulnerability 3", 141 | "source": { 142 | "name": "NVD", 143 | "url": "https://nvd.nist.gov/vuln/detail/Vulnerability 3" 144 | }, 145 | "references": [ 146 | { 147 | "id": "Vulnerability 3: Alias 3" 148 | } 149 | ], 150 | "affects": [ 151 | { 152 | "ref": "product 7", 153 | "versions": [ 154 | { 155 | "version": "2.4", 156 | "status": "affected" 157 | }, 158 | { 159 | "version": "2.6", 160 | "status": "affected" 161 | }, 162 | { 163 | "range": "vers:generic/>=2.9|<=4.1", 164 | "status": "affected" 165 | } 166 | ] 167 | }, 168 | { 169 | "ref": "product 8", 170 | "versions": [ 171 | { 172 | "version": "3.4", 173 | "status": "affected" 174 | } 175 | ] 176 | } 177 | ] 178 | }, 179 | { 180 | "id": "Vulnerability 1", 181 | "source": { 182 | "name": "NVD", 183 | "url": "https://nvd.nist.gov/vuln/detail/Vulnerability 1" 184 | }, 185 | "affects": [ 186 | { 187 | "ref": "product 1", 188 | "versions": [ 189 | { 190 | "version": "12.4", 191 | "status": "affected" 192 | } 193 | ] 194 | }, 195 | { 196 | "ref": "product 2", 197 | "versions": [ 198 | { 199 | "version": "13.4", 200 | "status": "affected" 201 | } 202 | ] 203 | } 204 | ] 205 | } 206 | ] 207 | } 208 | -------------------------------------------------------------------------------- /tests/auxiliary/test_validate_sboms/modified_sbom/Acme_Application_9.1.1_20220217T101458_bom.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.4", 4 | "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", 5 | "version": 1, 6 | "metadata": { 7 | "authors": [ 8 | { 9 | "name": "automated" 10 | } 11 | ], 12 | "component": { 13 | "type": "application", 14 | "bom-ref": "acme-app", 15 | "group": "com.company.internal", 16 | "supplier": { 17 | "name": "Company Legal" 18 | }, 19 | "name": "Acme_Application", 20 | "version": "9.1.1", 21 | "copyright": "Company Legal 2022, all rights reserved", 22 | "properties": [ 23 | { 24 | "name": "internal:component:status", 25 | "value": "internal" 26 | } 27 | ] 28 | } 29 | }, 30 | "components": [ 31 | { 32 | "type": "library", 33 | "bom-ref": "pkg:maven/org.acme/web-framework@1.0.0", 34 | "supplier": { 35 | "name": "Acme, Inc." 36 | }, 37 | "licenses": [ 38 | { 39 | "license": { 40 | "id": "Apache-1.0" 41 | } 42 | } 43 | ], 44 | "group": "org.acme", 45 | "name": "web-framework", 46 | "version": "1.0.0", 47 | "purl": "pkg:maven/org.acme/web-framework@1.0.0" 48 | }, 49 | { 50 | "type": "library", 51 | "bom-ref": "pkg:maven/org.acme/persistence@3.1.0", 52 | "supplier": { 53 | "name": "Acme, Inc." 54 | }, 55 | "group": "org.acme", 56 | "name": "persistence", 57 | "version": "3.1.0", 58 | "purl": "pkg:maven/org.acme/persistence@3.1.0", 59 | "licenses": [ 60 | { 61 | "license": { 62 | "id": "Apache-2.0" 63 | } 64 | } 65 | ] 66 | }, 67 | { 68 | "type": "library", 69 | "bom-ref": "pkg:maven/org.acme/common-util@3.0.0", 70 | "supplier": { 71 | "name": "Acme, Inc." 72 | }, 73 | "licenses": [ 74 | { 75 | "license": { 76 | "id": "BSD-3-Clause" 77 | } 78 | } 79 | ], 80 | "group": "org.acme", 81 | "name": "common-util", 82 | "version": "3.0.0", 83 | "purl": "pkg:maven/org.acme/common-util@3.0.0" 84 | }, 85 | { 86 | "type": "library", 87 | "bom-ref": "tomcat-catalina ref", 88 | "supplier": { 89 | "name": "Acme, Inc." 90 | }, 91 | "group": "com.acme", 92 | "name": "tomcat-catalina", 93 | "version": "9.0.14", 94 | "licenses": [ 95 | { 96 | "license": { 97 | "id": "Apache-2.0" 98 | } 99 | } 100 | ] 101 | }, 102 | { 103 | "type": "library", 104 | "bom-ref": "card-verifier bom-ref", 105 | "supplier": { 106 | "name": "Acme, Inc." 107 | }, 108 | "group": "", 109 | "name": "card-verifier", 110 | "version": "1.0.2", 111 | "licenses": [ 112 | { 113 | "expression": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0" 114 | } 115 | ] 116 | }, 117 | { 118 | "type": "library", 119 | "bom-ref": "ref on util", 120 | "supplier": { 121 | "name": "Example, Inc." 122 | }, 123 | "group": "com.example", 124 | "name": "util", 125 | "version": "2.0.0", 126 | "licenses": [ 127 | { 128 | "expression": "Example, Inc. Commercial License" 129 | } 130 | ], 131 | "cpe": "cpe:2.3:a:*:util:2.0.0:*:*:*:*:*:*:*" 132 | }, 133 | { 134 | "type": "application", 135 | "bom-ref": "some program application", 136 | "supplier": { 137 | "name": "Company Legal" 138 | }, 139 | "group": "com.company.internal", 140 | "name": "some program", 141 | "version": "T4.0.1.30", 142 | "hashes": [ 143 | { 144 | "alg": "SHA-256", 145 | "content": "3942447fac867ae5cdb3229b658f4d48" 146 | } 147 | ], 148 | "licenses": [ 149 | { 150 | "license": { 151 | "id": "Apache-2.0" 152 | } 153 | } 154 | ], 155 | "copyright": "Company Legal 2022, all rights reserved", 156 | "properties": [ 157 | { 158 | "name": "internal:component:status", 159 | "value": "internal" 160 | } 161 | ] 162 | } 163 | ], 164 | "dependencies": [ 165 | { 166 | "ref": "acme-app", 167 | "dependsOn": [ 168 | "pkg:maven/org.acme/web-framework@1.0.0", 169 | "pkg:maven/org.acme/persistence@3.1.0", 170 | "some program application" 171 | ] 172 | }, 173 | { 174 | "ref": "pkg:maven/org.acme/web-framework@1.0.0", 175 | "dependsOn": [ 176 | "pkg:maven/org.acme/common-util@3.0.0" 177 | ] 178 | }, 179 | { 180 | "ref": "pkg:maven/org.acme/persistence@3.1.0", 181 | "dependsOn": [ 182 | "pkg:maven/org.acme/common-util@3.0.0" 183 | ] 184 | }, 185 | { 186 | "ref": "pkg:maven/org.acme/common-util@3.0.0", 187 | "dependsOn": [] 188 | } 189 | ], 190 | "compositions": [ 191 | { 192 | "aggregate": "incomplete", 193 | "assemblies": [ 194 | "pkg:maven/org.acme/web-framework@1.0.0", 195 | "pkg:maven/org.acme/persistence@3.1.0", 196 | "pkg:maven/org.acme/common-util@3.0.0", 197 | "tomcat-catalina ref", 198 | "card-verifier bom-ref", 199 | "ref on util", 200 | "some program application" 201 | ] 202 | } 203 | ] 204 | } 205 | -------------------------------------------------------------------------------- /tests/auxiliary/test_validate_sboms/Acme_Application_9.1.1_ec7781220ec7781220ec778122012345_20220217T101458.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.3", 4 | "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2022-02-17T10:14:58Z", 8 | "authors": [ 9 | { 10 | "name": "automated" 11 | } 12 | ], 13 | "component": { 14 | "type": "application", 15 | "bom-ref": "acme-app", 16 | "group": "com.festo.internal", 17 | "supplier": { 18 | "name": "Festo SE & Co. KG" 19 | }, 20 | "name": "Acme_Application", 21 | "version": "9.1.1", 22 | "copyright": "Festo SE & Co. KG 2022, all rights reserved", 23 | "hashes": [ 24 | { 25 | "alg": "MD5", 26 | "content": "ec7781220ec7781220ec778122012345" 27 | } 28 | ], 29 | "properties": [ 30 | { 31 | "name": "internal:component:status", 32 | "value": "internal" 33 | } 34 | ] 35 | } 36 | }, 37 | "components": [ 38 | { 39 | "type": "library", 40 | "bom-ref": "pkg:maven/org.acme/web-framework@1.0.0", 41 | "supplier": { 42 | "name": "Acme, Inc." 43 | }, 44 | "licenses": [ 45 | { 46 | "license": { 47 | "id": "Apache-1.0" 48 | } 49 | } 50 | ], 51 | "group": "org.acme", 52 | "name": "web-framework", 53 | "version": "1.0.0" 54 | }, 55 | { 56 | "type": "library", 57 | "bom-ref": "pkg:maven/org.acme/persistence@3.1.0", 58 | "supplier": { 59 | "name": "Acme, Inc." 60 | }, 61 | "group": "org.acme", 62 | "name": "persistence", 63 | "version": "3.1.0", 64 | "licenses": [ 65 | { 66 | "license": { 67 | "id": "Apache-2.0" 68 | } 69 | } 70 | ] 71 | }, 72 | { 73 | "type": "library", 74 | "bom-ref": "pkg:maven/org.acme/common-util@3.0.0", 75 | "supplier": { 76 | "name": "Acme, Inc." 77 | }, 78 | "licenses": [ 79 | { 80 | "license": { 81 | "id": "BSD-3-Clause" 82 | } 83 | } 84 | ], 85 | "group": "org.acme", 86 | "name": "common-util", 87 | "version": "3.0.0" 88 | }, 89 | { 90 | "type": "library", 91 | "bom-ref": "tomcat-catalina ref", 92 | "supplier": { 93 | "name": "Acme, Inc." 94 | }, 95 | "group": "com.acme", 96 | "name": "tomcat-catalina", 97 | "version": "9.0.14", 98 | "licenses": [ 99 | { 100 | "license": { 101 | "id": "Apache-2.0" 102 | } 103 | } 104 | ] 105 | }, 106 | { 107 | "type": "library", 108 | "bom-ref": "card-verifier bomref", 109 | "supplier": { 110 | "name": "Acme, Inc." 111 | }, 112 | "group": "", 113 | "name": "card-verifier", 114 | "version": "1.0.2", 115 | "licenses": [ 116 | { 117 | "expression": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0" 118 | } 119 | ] 120 | }, 121 | { 122 | "type": "library", 123 | "bom-ref": "ref on util", 124 | "supplier": { 125 | "name": "Example, Inc." 126 | }, 127 | "group": "com.example", 128 | "name": "util", 129 | "version": "2.0.0", 130 | "licenses": [ 131 | { 132 | "expression": "Example, Inc. Commercial License" 133 | } 134 | ], 135 | "cpe": "cpe:2.3:a:*:util:2.0.0:*:*:*:*:*:*:*" 136 | }, 137 | { 138 | "type": "application", 139 | "bom-ref": "someprogramm application", 140 | "supplier": { 141 | "name": "Festo SE & Co.KG" 142 | }, 143 | "group": "com.festo.internal", 144 | "name": "someprogramm", 145 | "version": "T4.0.1.30", 146 | "hashes": [ 147 | { 148 | "alg": "SHA-256", 149 | "content": "3942447fac867ae5cdb3229b658f4d48" 150 | } 151 | ], 152 | "licenses": [ 153 | { 154 | "license": { 155 | "id": "Apache-2.0" 156 | } 157 | } 158 | ], 159 | "copyright": "Festo SE & Co. KG 2022, all rights reserved", 160 | "properties": [ 161 | { 162 | "name": "internal:component:status", 163 | "value": "internal" 164 | } 165 | ] 166 | } 167 | ], 168 | "dependencies": [ 169 | { 170 | "ref": "acme-app", 171 | "dependsOn": [ 172 | "pkg:maven/org.acme/web-framework@1.0.0", 173 | "pkg:maven/org.acme/persistence@3.1.0", 174 | "someprogramm application" 175 | ] 176 | }, 177 | { 178 | "ref": "pkg:maven/org.acme/web-framework@1.0.0", 179 | "dependsOn": [ 180 | "pkg:maven/org.acme/common-util@3.0.0" 181 | ] 182 | }, 183 | { 184 | "ref": "pkg:maven/org.acme/persistence@3.1.0", 185 | "dependsOn": [ 186 | "pkg:maven/org.acme/common-util@3.0.0" 187 | ] 188 | }, 189 | { 190 | "ref": "pkg:maven/org.acme/common-util@3.0.0", 191 | "dependsOn": [] 192 | } 193 | ], 194 | "compositions": [ 195 | { 196 | "aggregate": "incomplete", 197 | "assemblies": [ 198 | "pkg:maven/org.acme/web-framework@1.0.0", 199 | "pkg:maven/org.acme/persistence@3.1.0", 200 | "pkg:maven/org.acme/common-util@3.0.0", 201 | "tomcat-catalina ref", 202 | "card-verifier bomref", 203 | "ref on util", 204 | "someprogramm application" 205 | ] 206 | } 207 | ] 208 | } 209 | -------------------------------------------------------------------------------- /tests/test_list_command.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | import cdxev.list_command as lc 5 | from cdxev.auxiliary.sbomFunctions import deserialize 6 | 7 | path_to_sbom = ( 8 | "tests/auxiliary/test_list_command_sboms/" 9 | "Acme_Application_9.1.1_20220217T101458.cdx.json" 10 | ) 11 | 12 | 13 | def get_test_sbom(path_sbom: str = path_to_sbom) -> dict: 14 | with open(path_sbom, "r") as read_file: 15 | sbom = json.load(read_file) 16 | return sbom 17 | 18 | 19 | def extract_license(license: dict) -> str: 20 | if license.get("expression", ""): 21 | return license.get("expression", "") 22 | elif license.get("license", {}).get("id", ""): 23 | return license.get("license", {}).get("id", "") 24 | else: 25 | return license.get("license", {}).get("name", "") 26 | 27 | 28 | class TestListCommand(unittest.TestCase): 29 | def test_list_license_information(self) -> None: 30 | sbom = get_test_sbom() 31 | deserialized_sbom = deserialize(sbom) 32 | license_file = lc.list_license_information(deserialized_sbom) 33 | components = sbom.get("components", []) 34 | list_file_licenses = license_file[ 35 | license_file.find( 36 | "This product includes material developed by third parties:\n\n" 37 | ) 38 | + len("This product includes material developed by third parties:\n") 39 | + 1 : 40 | ] 41 | list_file_licenses_split = list_file_licenses.split("\n\n") 42 | 43 | for component in components: 44 | found = False 45 | for license_entry in list_file_licenses_split: 46 | license_component_name = license_entry[: license_entry.find(":\n")] 47 | license_component_name = license_component_name.replace("\n", "") 48 | licenses = license_entry[license_entry.find(":\n") + len(":\n") :] 49 | license_list = licenses.split("\n") 50 | license_list = [entry.replace("\n", "") for entry in license_list] 51 | 52 | if component.get("name", "") != license_component_name: 53 | continue 54 | 55 | if ( 56 | component.get("copyright", "") 57 | and component.get("copyright", "") not in license_list 58 | ): 59 | continue 60 | for license in component.get("licenses", []): 61 | license_content = extract_license(license) 62 | if license_content not in license_list: 63 | continue 64 | 65 | found = True 66 | 67 | self.assertTrue(found) 68 | 69 | def test_list_components(self) -> None: 70 | sbom = get_test_sbom() 71 | deserialized_sbom = deserialize(sbom) 72 | license_file = lc.list_components(sbom=deserialized_sbom, format="txt") 73 | components = sbom.get("components", []) 74 | component_txt_list = license_file.split("\n\n") 75 | for component in components: 76 | found = False 77 | for component_entry in component_txt_list: 78 | attribute_text = component_entry.replace("\n", "") 79 | if not component.get("name", "") in attribute_text: 80 | continue 81 | 82 | if not ( 83 | component.get("version", "") in attribute_text 84 | and component.get("supplier", {}).get("name", "") in attribute_text 85 | ): 86 | continue 87 | found = True 88 | 89 | self.assertTrue(found) 90 | 91 | def test_licenses_csv(self) -> None: 92 | sbom = get_test_sbom() 93 | deserialized_sbom = deserialize(sbom) 94 | license_file = lc.list_license_information(deserialized_sbom, format="csv") 95 | license_txt_list = license_file.split("\n") 96 | 97 | components = sbom.get("components", []) 98 | 99 | for component in components: 100 | found = False 101 | for license_entry in license_txt_list: 102 | 103 | if component.get("name", "") not in license_entry: 104 | continue 105 | 106 | if ( 107 | component.get("copyright", "") 108 | and component.get("copyright", "") not in license_entry 109 | ): 110 | continue 111 | for license in component.get("licenses", []): 112 | license_content = extract_license(license) 113 | if license_content not in license_entry: 114 | continue 115 | 116 | found = True 117 | 118 | self.assertTrue(found) 119 | 120 | def test_component_csv(self) -> None: 121 | sbom = get_test_sbom() 122 | deserialized_sbom = deserialize(sbom) 123 | license_file = lc.list_components(deserialized_sbom, format="csv") 124 | license_txt_list = license_file.split("\n") 125 | 126 | components = sbom.get("components", []) 127 | 128 | for component in components: 129 | found = False 130 | for license_entry in license_txt_list: 131 | 132 | if component.get("name", "") not in license_entry: 133 | continue 134 | 135 | if ( 136 | component.get("version", "") not in license_entry 137 | and component.get("copyright", "") not in license_entry 138 | ): 139 | continue 140 | 141 | found = True 142 | 143 | self.assertTrue(found) 144 | -------------------------------------------------------------------------------- /tests/integration/data/amend.expected_delete-ambiguous-licenses.cdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", 3 | "bomFormat": "CycloneDX", 4 | "specVersion": "1.4", 5 | "version": 2, 6 | "metadata": { 7 | "timestamp": "2024-05-24T11:43:11+00:00", 8 | "tools": [ 9 | { 10 | "vendor": "Company SE", 11 | "name": "some-tool", 12 | "version": "3.0.0" 13 | }, 14 | { 15 | "name": "cyclonedx-editor-validator", 16 | "vendor": "Festo SE & Co. KG", 17 | "version": "0.0.0" 18 | } 19 | ], 20 | "component": { 21 | "type": "application", 22 | "name": "test-app", 23 | "version": "1.0.0", 24 | "bom-ref": "pkg:npm/test-app@1.0.0", 25 | "author": "Company SE", 26 | "purl": "pkg:npm/test-app@1.0.0" 27 | } 28 | }, 29 | "components": [ 30 | { 31 | "type": "library", 32 | "name": "depA", 33 | "group": "com.company.unit", 34 | "version": "4.0.2", 35 | "bom-ref": "com.company.unit/depA@4.0.2", 36 | "author": "Company Unit SE", 37 | "licenses": [ 38 | { 39 | "license": { 40 | "id": "Apache-2.0" 41 | } 42 | } 43 | ], 44 | "externalReferences": [ 45 | { 46 | "type": "website", 47 | "url": "https://www.company.com" 48 | } 49 | ] 50 | }, 51 | { 52 | "type": "library", 53 | "name": "depB", 54 | "group": "some-vendor", 55 | "version": "1.2.3", 56 | "bom-ref": "some-vendor/depB@1.2.3", 57 | "author": "dude@some-vendor.com", 58 | "supplier": { 59 | "name": "Some Vendor Inc.", 60 | "url": [ 61 | "https://www.some-vendor.com" 62 | ] 63 | }, 64 | "components": [ 65 | { 66 | "type": "library", 67 | "name": "gravity", 68 | "group": "physics", 69 | "version": "0.0.1", 70 | "bom-ref": "some-vendor/depB@1.2.3:physics/gravity@0.0.1", 71 | "licenses": [ 72 | { 73 | "license": { 74 | "id": "MIT" 75 | } 76 | } 77 | ], 78 | "externalReferences": [ 79 | { 80 | "type": "vcs", 81 | "url": "https://github.com/physics/gravity.git" 82 | }, 83 | { 84 | "type": "website", 85 | "url": "https://www.universe.com" 86 | } 87 | ] 88 | }, 89 | { 90 | "type": "library", 91 | "name": "x-ray", 92 | "group": "physics", 93 | "version": "18.9.5", 94 | "bom-ref": "some-vendor/depB@1.2.3:physics/x-ray@18.9.5", 95 | "author": "Acme Inc", 96 | "externalReferences": [ 97 | { 98 | "type": "vcs", 99 | "url": "git://not-a-browsable-url.com/x-ray.git" 100 | } 101 | ], 102 | "components": [ 103 | { 104 | "type": "library", 105 | "name": "Rudolph", 106 | "version": "6.6.6", 107 | "bom-ref": "some-vendor/depB@1.2.3:physics/x-ray@18.9.5:Rudolph@6.6.6", 108 | "copyright": "2000 Santa Claus", 109 | "externalReferences": [ 110 | { 111 | "type": "vcs", 112 | "url": "https://northpole.com/rudolph.git" 113 | } 114 | ] 115 | } 116 | ] 117 | } 118 | ] 119 | }, 120 | { 121 | "type": "library", 122 | "name": "depC", 123 | "version": "3.2.1", 124 | "bom-ref": "depC@3.2.1", 125 | "author": "Acme Inc", 126 | "components": [ 127 | { 128 | "type": "library", 129 | "name": "Rudolph", 130 | "version": "6.6.6", 131 | "bom-ref": "depC@3.2.1:Rudolph@6.6.6", 132 | "copyright": "2000 Santa Claus", 133 | "externalReferences": [ 134 | { 135 | "type": "vcs", 136 | "url": "https://northpole.com/rudolph.git" 137 | } 138 | ] 139 | } 140 | ] 141 | } 142 | ], 143 | "dependencies": [ 144 | { 145 | "ref": "com.company.unit/depA@4.0.2", 146 | "dependsOn": [ 147 | "some-vendor/depB@1.2.3", 148 | "depC@3.2.1" 149 | ] 150 | }, 151 | { 152 | "ref": "some-vendor/depB@1.2.3", 153 | "dependsOn": [ 154 | "some-vendor/depB@1.2.3:physics/gravity@0.0.1" 155 | ] 156 | }, 157 | { 158 | "ref": "some-vendor/depB@1.2.3:physics/gravity@0.0.1", 159 | "dependsOn": [ 160 | "depC@3.2.1" 161 | ] 162 | }, 163 | { 164 | "ref": "some-vendor/depB@1.2.3:physics/x-ray@18.9.5", 165 | "dependsOn": [ 166 | "some-vendor/depB@1.2.3:physics/x-ray@18.9.5:Rudolph@6.6.6" 167 | ] 168 | }, 169 | { 170 | "ref": "some-vendor/depB@1.2.3:physics/x-ray@18.9.5:Rudolph@6.6.6", 171 | "dependsOn": [] 172 | }, 173 | { 174 | "ref": "depC@3.2.1", 175 | "dependsOn": [] 176 | } 177 | ], 178 | "compositions": [ 179 | { 180 | "aggregate": "complete", 181 | "assemblies": [ 182 | "some-vendor/depB@1.2.3", 183 | "some-vendor/depB@1.2.3:physics/gravity@0.0.1", 184 | "some-vendor/depB@1.2.3:physics/x-ray@18.9.5", 185 | "some-vendor/depB@1.2.3:physics/x-ray@18.9.5:Rudolph@6.6.6" 186 | ] 187 | }, 188 | { 189 | "aggregate": "incomplete", 190 | "assemblies": [ 191 | "com.company.unit/depA@4.0.2" 192 | ] 193 | } 194 | ], 195 | "serialNumber": "urn:uuid:c1179f53-0405-4602-8457-0c91f9ddcc32" 196 | } 197 | --------------------------------------------------------------------------------