# pylint: disable=protected-access import hashlib import json from datetime import datetime import jsonschema import pytest from osbuild.util.sbom.spdx2.model import ( CATEGORY_TO_REPOSITORY_TYPE, Checksum, ChecksumAlgorithm, CreationInfo, Creator, CreatorType, Document, EntityWithSpdxId, ExternalPackageRef, ExternalPackageRefCategory, ExtractedLicensingInfo, NoAssertionValue, NoneValue, Package, Relationship, RelationshipType, datetime_to_iso8601, generate_license_id, normalize_name_for_license_id, ) zoneinfo = pytest.importorskip("zoneinfo") def test_creator_type_str(): assert str(CreatorType.PERSON) == "Person" assert str(CreatorType.ORGANIZATION) == "Organization" assert str(CreatorType.TOOL) == "Tool" @pytest.mark.parametrize("test_object,expected_str", ( ( Creator(CreatorType.TOOL, "Sample-Tool-123"), "Tool: Sample-Tool-123" ), ( Creator(CreatorType.ORGANIZATION, "Sample Organization"), "Organization: Sample Organization" ), ( Creator(CreatorType.ORGANIZATION, "Sample Organization", "email@example.com"), "Organization: Sample Organization (email@example.com)" ), ( Creator(CreatorType.PERSON, "John Foo"), "Person: John Foo" ), ( Creator(CreatorType.PERSON, "John Foo", "email@example.com"), "Person: John Foo (email@example.com)" ) )) def test_creator_str(test_object, expected_str): assert str(test_object) == expected_str @pytest.mark.parametrize("test_spdx_id,error", ( ("SPDXRef-DOCUMENT", False), ("SPDXRef-package-1.2.3", False), ("SPDXRef-package-1.2.3-0ec6114d-8d46-4553-a310-4df502c29082", False), ("", True), ("SPDXRef-", True), ("SPDxRef-DOCUMENT", True), ("SPDXRef-createrepo_c-1.2.3-1", True) )) def test_entity_with_spdx_id(test_spdx_id, error): if error: with pytest.raises(ValueError): _ = EntityWithSpdxId(test_spdx_id) else: _ = EntityWithSpdxId(test_spdx_id) @pytest.mark.parametrize("test_date,expected_str", ( (datetime(2024, 11, 15, 14, 33, tzinfo=zoneinfo.ZoneInfo("UTC")), "2024-11-15T14:33:00Z"), (datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("UTC")), "2024-11-15T14:33:59Z"), (datetime(2024, 11, 15, 14, 33, 59, 123456, tzinfo=zoneinfo.ZoneInfo("UTC")), "2024-11-15T14:33:59Z"), (datetime(2024, 11, 15, 14, 33, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "2024-11-15T13:33:00Z"), (datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "2024-11-15T13:33:59Z") )) def test_datetime_to_iso8601(test_date, expected_str): assert datetime_to_iso8601(test_date) == expected_str @pytest.mark.parametrize("test_case", ( { "instance_args": { "spdx_version": "SPDX-2.3", "spdx_id": "SPDXRef-DOCUMENT", "name": "Sample-Document", "document_namespace": "https://example.com", "creators": [Creator(CreatorType.TOOL, "Sample-Tool-123")], "created": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "data_license": "Public Domain" }, "expected": { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Sample-Document", "dataLicense": "Public Domain", "documentNamespace": "https://example.com", "creationInfo": { "created": "2024-11-15T13:33:59Z", "creators": [ "Tool: Sample-Tool-123" ] } }, }, { "instance_args": { "spdx_version": "SPDX-2.3", "spdx_id": "SPDXRef-DOCUMENT", "name": "Sample-Document", "document_namespace": "https://example.com", "creators": [Creator(CreatorType.TOOL, "Sample-Tool-123")], "created": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), }, "expected": { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Sample-Document", "dataLicense": "CC0-1.0", "documentNamespace": "https://example.com", "creationInfo": { "created": "2024-11-15T13:33:59Z", "creators": [ "Tool: Sample-Tool-123" ] } } }, { "instance_args": { "spdx_version": "SPDX-2.3", "spdx_id": "DOCUMENT", "name": "Sample-Document", "document_namespace": "https://example.com", "creators": [Creator(CreatorType.TOOL, "Sample-Tool-123")], "created": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), }, "error": True }, { "instance_args": { "spdx_version": "SPDX-2.3", "spdx_id": "SPDXRef-YOLO", "name": "Sample-Document", "document_namespace": "https://example.com", "creators": [Creator(CreatorType.TOOL, "Sample-Tool-123")], "created": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), }, "error": True } )) def test_creation_info_to_dict(test_case): if test_case.get("error", False): with pytest.raises(ValueError): CreationInfo(**test_case["instance_args"]) else: ci = CreationInfo(**test_case["instance_args"]) assert ci.to_dict() == test_case["expected"] def test_no_assertion_value_str(): assert str(NoAssertionValue()) == "NOASSERTION" def test_none_value_str(): assert str(NoneValue()) == "NONE" def test_external_package_ref_category_str(): assert str(ExternalPackageRefCategory.SECURITY) == "SECURITY" assert str(ExternalPackageRefCategory.PACKAGE_MANAGER) == "PACKAGE-MANAGER" assert str(ExternalPackageRefCategory.PERSISTENT_ID) == "PERSISTENT-ID" assert str(ExternalPackageRefCategory.OTHER) == "OTHER" def test_external_package_ref_cat_type_combinations(): for category, types in CATEGORY_TO_REPOSITORY_TYPE.items(): if category == ExternalPackageRefCategory.OTHER: _ = ExternalPackageRef(category, "made-up", "https://example.com") _ = ExternalPackageRef(category, "yolo-type", "https://example.com") continue for ref_type in types: _ = ExternalPackageRef(category, ref_type, "https://example.com") with pytest.raises(ValueError): _ = ExternalPackageRef(category, "made-up", "https://example.com") def test_external_package_ref_to_dict(): ref = ExternalPackageRef(ExternalPackageRefCategory.PACKAGE_MANAGER, "purl", "https://example.com") assert ref.to_dict() == { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "https://example.com" } def test_checksum_algorithm_str(): assert str(ChecksumAlgorithm.SHA1) == "SHA1" assert str(ChecksumAlgorithm.SHA224) == "SHA224" assert str(ChecksumAlgorithm.SHA256) == "SHA256" assert str(ChecksumAlgorithm.SHA384) == "SHA384" assert str(ChecksumAlgorithm.SHA512) == "SHA512" assert str(ChecksumAlgorithm.SHA3_256) == "SHA3-256" assert str(ChecksumAlgorithm.SHA3_384) == "SHA3-384" assert str(ChecksumAlgorithm.SHA3_512) == "SHA3-512" assert str(ChecksumAlgorithm.BLAKE2b_256) == "BLAKE2b-256" assert str(ChecksumAlgorithm.BLAKE2b_384) == "BLAKE2b-384" assert str(ChecksumAlgorithm.BLAKE2b_512) == "BLAKE2b-512" assert str(ChecksumAlgorithm.BLAKE3) == "BLAKE3" assert str(ChecksumAlgorithm.MD2) == "MD2" assert str(ChecksumAlgorithm.MD4) == "MD4" assert str(ChecksumAlgorithm.MD5) == "MD5" assert str(ChecksumAlgorithm.MD6) == "MD6" assert str(ChecksumAlgorithm.ADLER32) == "ADLER32" def test_checksum_to_dict(): assert Checksum(ChecksumAlgorithm.SHA1, "123456").to_dict() == { "algorithm": "SHA1", "checksumValue": "123456" } @pytest.mark.parametrize("name,expected_str", ( ("MIT", "MIT"), ("MIT-2.0", "MIT-2.0"), ("My License", "My-License"), ("%$MIT)) ((@\",))", "-MIT-"), ("MIT.and more-12345", "MIT.and-more-12345"), ("......-----------.......---------", ".-.-"), ("", "")) ) def test_normalize_name_for_license_id(name, expected_str): assert normalize_name_for_license_id(name) == expected_str @pytest.mark.parametrize("extracted_text,name", ( ("MIT", "test"), ("MIT", None), ("MIT and GPLv2", "MIT and GPLv2")) ) def test_generate_license_id(extracted_text, name): extracted_text_hash = hashlib.sha256(extracted_text.encode()).hexdigest() expect = f"LicenseRef-{normalize_name_for_license_id(name)}-{extracted_text_hash}" \ if name else f"LicenseRef-{extracted_text_hash}" assert generate_license_id(extracted_text, name) == expect # The __str__ just returns the license ID, which is generated by the _generate_license_id method # tested above. So, the test case looks the same for now. @pytest.mark.parametrize("extracted_text,name", ( ("MIT", "test"), ("MIT", None), ("MIT and GPLv2", "MIT and GPLv2")) ) def test_extracted_licensing_info___str__(extracted_text, name): extracted_text_hash = hashlib.sha256(extracted_text.encode()).hexdigest() expect = f"LicenseRef-{normalize_name_for_license_id(name)}-{extracted_text_hash}" \ if name else f"LicenseRef-{extracted_text_hash}" assert str(ExtractedLicensingInfo(extracted_text, name)) == expect @pytest.mark.parametrize("test_case", ( { "instance_args": { "extracted_text": "MIT and GPLv2", }, "expected": { "licenseId": "LicenseRef-7805d4303e817ddd5f86dcf6541af84daac5c5b4a8ad1fb4cd14def8a4ca3d13", "extractedText": "MIT and GPLv2" } }, { "instance_args": { "extracted_text": "MIT and GPLv2", "name": "MIT and GPLv2", }, "expected": { "licenseId": "LicenseRef-MIT-and-GPLv2-7805d4303e817ddd5f86dcf6541af84daac5c5b4a8ad1fb4cd14def8a4ca3d13", "extractedText": "MIT and GPLv2", "name": "MIT and GPLv2" } } )) def test_extracted_licensing_info_to_dict(test_case): p = ExtractedLicensingInfo(**test_case["instance_args"]) assert p.to_dict() == test_case["expected"] @pytest.mark.parametrize("test_case", ( { "instance_args": { "spdx_id": "SPDXRef-package-1.2.3", "name": "package", "download_location": "https://example.org/package-1.2.3.rpm" }, "expected": { "SPDXID": "SPDXRef-package-1.2.3", "name": "package", "downloadLocation": "https://example.org/package-1.2.3.rpm" } }, { "instance_args": { "spdx_id": "SPDXRef-package-1.2.3", "name": "package", "download_location": NoAssertionValue(), "files_analyzed": True }, "expected": { "SPDXID": "SPDXRef-package-1.2.3", "name": "package", "downloadLocation": "NOASSERTION", "filesAnalyzed": True } }, { "instance_args": { "spdx_id": "SPDXRef-package-1.2.3", "name": "package", "download_location": NoneValue(), "files_analyzed": False, "checksums": [ Checksum(ChecksumAlgorithm.SHA256, "123456") ], "version": "1.2.3", "homepage": "https://example.org/package", "source_info": "https://example.org/package-1.2.3.src.rpm", "license_declared": "MIT", "summary": "A sample package", "description": "A sample package description", "external_references": [ ExternalPackageRef( ExternalPackageRefCategory.PACKAGE_MANAGER, "purl", "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" ) ], "built_date": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")) }, "expected": { "SPDXID": "SPDXRef-package-1.2.3", "name": "package", "downloadLocation": "NONE", "filesAnalyzed": False, "checksums": [ { "algorithm": "SHA256", "checksumValue": "123456" } ], "versionInfo": "1.2.3", "homepage": "https://example.org/package", "sourceInfo": "https://example.org/package-1.2.3.src.rpm", "licenseDeclared": "MIT", "summary": "A sample package", "description": "A sample package description", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" } ], "builtDate": "2024-11-15T13:33:59Z" } }, # Test with ExtractedLicensingInfo { "instance_args": { "spdx_id": "SPDXRef-package-1.2.3", "name": "package", "download_location": NoneValue(), "files_analyzed": False, "checksums": [ Checksum(ChecksumAlgorithm.SHA256, "123456") ], "version": "1.2.3", "homepage": "https://example.org/package", "source_info": "https://example.org/package-1.2.3.src.rpm", "license_declared": ExtractedLicensingInfo("MIT and GPLv2"), "summary": "A sample package", "description": "A sample package description", "external_references": [ ExternalPackageRef( ExternalPackageRefCategory.PACKAGE_MANAGER, "purl", "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" ) ], "built_date": datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")) }, "expected": { "SPDXID": "SPDXRef-package-1.2.3", "name": "package", "downloadLocation": "NONE", "filesAnalyzed": False, "checksums": [ { "algorithm": "SHA256", "checksumValue": "123456" } ], "versionInfo": "1.2.3", "homepage": "https://example.org/package", "sourceInfo": "https://example.org/package-1.2.3.src.rpm", "licenseDeclared": "LicenseRef-7805d4303e817ddd5f86dcf6541af84daac5c5b4a8ad1fb4cd14def8a4ca3d13", "summary": "A sample package", "description": "A sample package description", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" } ], "builtDate": "2024-11-15T13:33:59Z" } } )) def test_package_to_dict(test_case): p = Package(**test_case["instance_args"]) assert p.to_dict() == test_case["expected"] def test_relationship_type_str(): assert str(RelationshipType.DESCRIBES) == "DESCRIBES" assert str(RelationshipType.DEPENDS_ON) == "DEPENDS_ON" assert str(RelationshipType.OPTIONAL_DEPENDENCY_OF) == "OPTIONAL_DEPENDENCY_OF" @pytest.mark.parametrize("test_case", ( { "instance_args": { "spdx_element_id": "SPDXRef-packageA-1.2.3", "relationship_type": RelationshipType.DEPENDS_ON, "related_spdx_element_id": "SPDXRef-packageB-3.2.1" }, "expected": { "spdxElementId": "SPDXRef-packageA-1.2.3", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-packageB-3.2.1" } }, { "instance_args": { "spdx_element_id": "SPDXRef-DOCUMENT", "relationship_type": RelationshipType.DESCRIBES, "related_spdx_element_id": "SPDXRef-packageB-3.2.1", "comment": "This document describes package B" }, "expected": { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-packageB-3.2.1", "comment": "This document describes package B" } }, )) def test_relationship_to_dict(test_case): r = Relationship(**test_case["instance_args"]) assert r.to_dict() == test_case["expected"] @pytest.mark.parametrize("test_case", ( { "instance_args": { "creation_info": CreationInfo( "SPDX-2.3", "SPDXRef-DOCUMENT", "Sample-Document", "https://example.com", [Creator(CreatorType.TOOL, "Sample-Tool-123")], datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "Public Domain" ) }, "expected": { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Sample-Document", "dataLicense": "Public Domain", "documentNamespace": "https://example.com", "creationInfo": { "created": "2024-11-15T13:33:59Z", "creators": [ "Tool: Sample-Tool-123" ] } } }, { "instance_args": { "creation_info": CreationInfo( "SPDX-2.3", "SPDXRef-DOCUMENT", "Sample-Document", "https://example.com", [Creator(CreatorType.TOOL, "Sample-Tool-123")], datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "Public Domain" ), "packages": [ Package( "SPDXRef-packageA-1.2.3", "package", "https://example.org/packageA-1.2.3.rpm" ), Package( "SPDXRef-packageB-3.2.1", "package", "https://example.org/packageB-3.2.1.rpm" ), ], "relationships": [ Relationship( "SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-packageA-1.2.3" ), Relationship( "SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-packageB-3.2.1" ), Relationship( "SPDXRef-packageA-1.2.3", RelationshipType.DEPENDS_ON, "SPDXRef-packageB-3.2.1" ) ] }, "expected": { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Sample-Document", "dataLicense": "Public Domain", "documentNamespace": "https://example.com", "creationInfo": { "created": "2024-11-15T13:33:59Z", "creators": [ "Tool: Sample-Tool-123" ] }, "packages": [ { "SPDXID": "SPDXRef-packageA-1.2.3", "name": "package", "downloadLocation": "https://example.org/packageA-1.2.3.rpm" }, { "SPDXID": "SPDXRef-packageB-3.2.1", "name": "package", "downloadLocation": "https://example.org/packageB-3.2.1.rpm" } ], "relationships": [ { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-packageA-1.2.3" }, { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-packageB-3.2.1" }, { "spdxElementId": "SPDXRef-packageA-1.2.3", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-packageB-3.2.1" } ] } }, # Test with ExtractedLicensingInfo { "instance_args": { "creation_info": CreationInfo( "SPDX-2.3", "SPDXRef-DOCUMENT", "Sample-Document", "https://example.com", [Creator(CreatorType.TOOL, "Sample-Tool-123")], datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")), "Public Domain" ), "packages": [ Package( "SPDXRef-packageA-1.2.3", "package", "https://example.org/packageA-1.2.3.rpm" ), Package( "SPDXRef-packageB-3.2.1", "package", "https://example.org/packageB-3.2.1.rpm" ), Package( "SPDXRef-package-1.2.3", "package", NoneValue(), "1.2.3", False, [Checksum(ChecksumAlgorithm.SHA256, "123456")], "https://example.org/package", "https://example.org/package-1.2.3.src.rpm", ExtractedLicensingInfo("MIT and GPLv2"), "A sample package", "A sample package description", [ ExternalPackageRef( ExternalPackageRefCategory.PACKAGE_MANAGER, "purl", "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" ) ], datetime(2024, 11, 15, 14, 33, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Prague")) ), ], "relationships": [ Relationship( "SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-packageA-1.2.3" ), Relationship( "SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-packageB-3.2.1" ), Relationship( "SPDXRef-packageA-1.2.3", RelationshipType.DEPENDS_ON, "SPDXRef-packageB-3.2.1" ) ], "extracted_licensing_infos": [ ExtractedLicensingInfo("MIT and GPLv2") ] }, "expected": { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Sample-Document", "dataLicense": "Public Domain", "documentNamespace": "https://example.com", "creationInfo": { "created": "2024-11-15T13:33:59Z", "creators": [ "Tool: Sample-Tool-123" ] }, "packages": [ { "SPDXID": "SPDXRef-packageA-1.2.3", "name": "package", "downloadLocation": "https://example.org/packageA-1.2.3.rpm" }, { "SPDXID": "SPDXRef-packageB-3.2.1", "name": "package", "downloadLocation": "https://example.org/packageB-3.2.1.rpm" }, { "SPDXID": "SPDXRef-package-1.2.3", "name": "package", "downloadLocation": "NONE", "filesAnalyzed": False, "checksums": [ { "algorithm": "SHA256", "checksumValue": "123456" } ], "versionInfo": "1.2.3", "homepage": "https://example.org/package", "sourceInfo": "https://example.org/package-1.2.3.src.rpm", "licenseDeclared": "LicenseRef-7805d4303e817ddd5f86dcf6541af84daac5c5b4a8ad1fb4cd14def8a4ca3d13", "summary": "A sample package", "description": "A sample package description", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": "pkg:rpm:/example/package@1.2.3-1?arch=x86_64" } ], "builtDate": "2024-11-15T13:33:59Z" } ], "relationships": [ { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-packageA-1.2.3" }, { "spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-packageB-3.2.1" }, { "spdxElementId": "SPDXRef-packageA-1.2.3", "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-packageB-3.2.1" } ], "hasExtractedLicensingInfos": [ { 'extractedText': 'MIT and GPLv2', 'licenseId': 'LicenseRef-7805d4303e817ddd5f86dcf6541af84daac5c5b4a8ad1fb4cd14def8a4ca3d13', } ] } } )) def test_document_to_dict(test_case): d = Document(**test_case["instance_args"]) assert d.to_dict() == test_case["expected"] spdx_2_3_1_schema_file = './test/data/spdx/spdx-schema-v2.3.1.json' with open(spdx_2_3_1_schema_file, encoding="utf-8") as f: spdx_schema = json.load(f) validator = jsonschema.Draft4Validator validator.check_schema(spdx_schema) spdx_validator = validator(spdx_schema) spdx_validator.validate(d.to_dict())