Add initial SBOM library implementation

Add implementation of standard-agnostic model for SBOM, and simple SPDX
v2.3 model. Also add convenience functions for converting DNF4 package
set to the standard-agnostic model and for converting it to SPDX model.

Cover the functionality with unit tests.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
This commit is contained in:
Tomáš Hozza 2024-06-26 13:22:00 +02:00 committed by Simon de Vlieger
parent 75b6fb4abe
commit 0b68f8123b
11 changed files with 1436 additions and 1 deletions

View file

@ -0,0 +1,48 @@
import os
from datetime import datetime
import pytest
testutil_dnf4 = pytest.importorskip("osbuild.testutil.dnf4")
bom_dnf = pytest.importorskip("osbuild.util.bom.dnf")
def test_dnf_pkgset_to_sbom_pkgset():
dnf_pkgset = testutil_dnf4.depsolve_pkgset([os.path.abspath("./test/data/testrepos/baseos")], ["bash"])
bom_pkgset = bom_dnf.dnf_pkgset_to_sbom_pkgset(dnf_pkgset)
assert len(bom_pkgset) == len(dnf_pkgset)
for bom_pkg, dnf_pkg in zip(bom_pkgset, dnf_pkgset):
assert bom_pkg.name == dnf_pkg.name
assert bom_pkg.version == dnf_pkg.version
assert bom_pkg.release == dnf_pkg.release
assert bom_pkg.architecture == dnf_pkg.arch
assert bom_pkg.epoch == dnf_pkg.epoch
assert bom_pkg.license_declared == dnf_pkg.license
assert bom_pkg.vendor == dnf_pkg.vendor
assert bom_pkg.build_date == datetime.fromtimestamp(dnf_pkg.buildtime)
assert bom_pkg.summary == dnf_pkg.summary
assert bom_pkg.description == dnf_pkg.description
assert bom_pkg.source_rpm == dnf_pkg.sourcerpm
assert bom_pkg.homepage == dnf_pkg.url
assert bom_pkg.checksums == {
bom_dnf.bom_chksum_algorithm_from_hawkey(dnf_pkg.chksum[0]): dnf_pkg.chksum[1].hex()
}
assert bom_pkg.download_url == dnf_pkg.remote_location()
assert bom_pkg.repository_url == dnf_pkg.remote_location()[:-len("/" + dnf_pkg.relativepath)]
assert [dep.name for dep in bom_pkg.rpm_provides] == [dep.name for dep in dnf_pkg.provides]
assert [dep.name for dep in bom_pkg.rpm_requires] == [dep.name for dep in dnf_pkg.requires]
assert [dep.name for dep in bom_pkg.rpm_recommends] == [dep.name for dep in dnf_pkg.recommends]
assert [dep.name for dep in bom_pkg.rpm_suggests] == [dep.name for dep in dnf_pkg.suggests]
# smoke test the inter-package relationships on bash
bash = [pkg for pkg in bom_pkgset if pkg.name == "bash"][0]
assert len(bash.depends_on) == 3
assert sorted(
bash.depends_on,
key=lambda x: x.name) == sorted(
[pkg for pkg in bom_pkgset if pkg.name in ["filesystem", "glibc", "ncurses-libs"]],
key=lambda x: x.name)
assert len(bash.optional_depends_on) == 0

View file

@ -0,0 +1,49 @@
import pytest
from osbuild.util.sbom.model import RPMPackage
def test_rpmpackage_uuid():
pkg_a = RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64")
pkg_a_duplicate = RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64")
pkg_a_v2 = RPMPackage("PackageA", "2.0.0", "1.fc40", "x86_64")
pkg_a_fc41 = RPMPackage("PackageA", "1.0.0", "1.fc41", "x86_64")
pkg_a_aarch64 = RPMPackage("PackageA", "1.0.0", "1.fc40", "aarch64")
pkg_b = RPMPackage("PackageB", "1.0.0", "1.fc40", "x86_64")
assert pkg_a.uuid() == pkg_a_duplicate.uuid()
for pkg in [pkg_a_v2, pkg_a_fc41, pkg_a_aarch64, pkg_b]:
assert pkg_a.uuid() != pkg.uuid()
@pytest.mark.parametrize("package,purl", (
(
RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64"),
"pkg:rpm/PackageA@1.0.0-1.fc40?arch=x86_64"
),
(
RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64", epoch=123),
"pkg:rpm/PackageA@1.0.0-1.fc40?arch=x86_64&epoch=123"
),
(
RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64", vendor="Fedora Project"),
"pkg:rpm/fedora%20project/PackageA@1.0.0-1.fc40?arch=x86_64"
),
(
RPMPackage("PackageA", "1.0.0", "1.el9", "x86_64", vendor="CentOS"),
"pkg:rpm/centos/PackageA@1.0.0-1.el9?arch=x86_64"
),
(
RPMPackage("PackageA", "1.0.0", "1.el9", "x86_64", vendor="Red Hat, Inc."),
"pkg:rpm/red%20hat%2C%20inc./PackageA@1.0.0-1.el9?arch=x86_64"
),
(
RPMPackage("PackageA", "1.0.0", "1.fc40", "x86_64", vendor="Fedora Project", repository_url="https://example.org/repo/"),
"pkg:rpm/fedora%20project/PackageA@1.0.0-1.fc40?arch=x86_64&repository_url=https://example.org/repo/"
),
))
def test_rpmpackage_purl(package, purl):
assert package.purl() == purl

View file

@ -0,0 +1,76 @@
import os
import pytest
import osbuild
from osbuild.util.sbom.spdx import bom_pkgset_to_spdx2_doc, create_spdx2_document, spdx2_checksum_algorithm
from osbuild.util.sbom.spdx2.model import CreatorType, ExternalPackageRefCategory, RelationshipType
testutil_dnf4 = pytest.importorskip("osbuild.testutil.dnf4")
bom_dnf = pytest.importorskip("osbuild.util.bom.dnf")
def test_create_spdx2_document():
doc1 = create_spdx2_document()
assert doc1.creation_info.spdx_version == "SPDX-2.3"
assert doc1.creation_info.spdx_id == "SPDXRef-DOCUMENT"
assert doc1.creation_info.name == f"sbom-by-osbuild-{osbuild.__version__}"
assert doc1.creation_info.data_license == "CC0-1.0"
assert doc1.creation_info.document_namespace.startswith("https://osbuild.org/spdxdocs/sbom-by-osbuild-")
assert len(doc1.creation_info.creators) == 1
assert doc1.creation_info.creators[0].creator_type == CreatorType.TOOL
assert doc1.creation_info.creators[0].name == f"osbuild-{osbuild.__version__}"
assert doc1.creation_info.created
doc2 = create_spdx2_document()
assert doc1.creation_info.document_namespace != doc2.creation_info.document_namespace
assert doc1.creation_info.created != doc2.creation_info.created
doc1_dict = doc1.to_dict()
doc2_dict = doc2.to_dict()
del doc1_dict["creationInfo"]["created"]
del doc2_dict["creationInfo"]["created"]
del doc1_dict["documentNamespace"]
del doc2_dict["documentNamespace"]
assert doc1_dict == doc2_dict
def test_sbom_pkgset_to_spdx2_doc():
dnf_pkgset = testutil_dnf4.depsolve_pkgset([os.path.abspath("./test/data/testrepos/baseos")], ["bash"])
bom_pkgset = bom_dnf.dnf_pkgset_to_sbom_pkgset(dnf_pkgset)
doc = bom_pkgset_to_spdx2_doc(bom_pkgset)
assert len(doc.packages) == len(bom_pkgset)
for spdx_pkg, bom_pkg in zip(doc.packages, bom_pkgset):
assert spdx_pkg.spdx_id == f"SPDXRef-{bom_pkg.uuid()}"
assert spdx_pkg.name == bom_pkg.name
assert spdx_pkg.version == bom_pkg.version
assert not spdx_pkg.files_analyzed
assert spdx_pkg.license_declared == bom_pkg.license_declared
assert spdx_pkg.download_location == bom_pkg.download_url
assert spdx_pkg.homepage == bom_pkg.homepage
assert spdx_pkg.summary == bom_pkg.summary
assert spdx_pkg.description == bom_pkg.description
assert spdx_pkg.source_info == bom_pkg.source_info()
assert spdx_pkg.built_date == bom_pkg.build_date
assert len(spdx_pkg.checksums) == 1
assert spdx_pkg.checksums[0].algorithm == spdx2_checksum_algorithm(list(bom_pkg.checksums.keys())[0])
assert spdx_pkg.checksums[0].value == list(bom_pkg.checksums.values())[0]
assert len(spdx_pkg.external_references) == 1
assert spdx_pkg.external_references[0].category == ExternalPackageRefCategory.PACKAGE_MANAGER
assert spdx_pkg.external_references[0].reference_type == "purl"
assert spdx_pkg.external_references[0].locator == bom_pkg.purl()
assert len([rel for rel in doc.relationships if rel.relationship_type ==
RelationshipType.DESCRIBES]) == len(bom_pkgset)
deps_count = sum(len(bom_pkg.depends_on) for bom_pkg in bom_pkgset)
assert len([rel for rel in doc.relationships if rel.relationship_type ==
RelationshipType.DEPENDS_ON]) == deps_count
optional_deps_count = sum(len(bom_pkg.optional_depends_on) for bom_pkg in bom_pkgset)
assert len([rel for rel in doc.relationships if rel.relationship_type ==
RelationshipType.OPTIONAL_DEPENDENCY_OF]) == optional_deps_count

View file

@ -0,0 +1,469 @@
from datetime import datetime
import pytest
from osbuild.util.sbom.spdx2.model import (
CATEGORY_TO_REPOSITORY_TYPE,
Checksum,
ChecksumAlgorithm,
CreationInfo,
Creator,
CreatorType,
Document,
EntityWithSpdxId,
ExternalPackageRef,
ExternalPackageRefCategory,
NoAssertionValue,
NoneValue,
Package,
Relationship,
RelationshipType,
datetime_to_iso8601,
)
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("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"
}
}
))
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"
}
]
}
}
))
def test_document_to_dict(test_case):
d = Document(**test_case["instance_args"])
assert d.to_dict() == test_case["expected"]