Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions scanpipe/pipes/spdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,31 @@ def as_dict(self):
"SPDXID": self.spdx_id,
"name": self.safe_document_name(self.name),
"documentNamespace": self.namespace,
"documentDescribes": self.describes,
"creationInfo": self.creation_info.as_dict(),
"packages": [package.as_dict(self.version) for package in self.packages],
}

# For SPDX 2.2, use deprecated documentDescribes field for backward compatibility
# For SPDX 2.3+, use DESCRIBES relationships instead (per SPDX spec)
if self.version == SPDX_SPEC_VERSION_2_2:
data["documentDescribes"] = self.describes
else:
# Generate DESCRIBES relationships from document to described elements
describes_relationships = [
Relationship(
spdx_id=self.spdx_id,
related_spdx_id=described_id,
relationship="DESCRIBES",
).as_dict()
for described_id in self.describes
]
# Merge with existing relationships
all_relationships = describes_relationships + [
relationship.as_dict() for relationship in self.relationships
]
if all_relationships:
data["relationships"] = all_relationships

if self.files:
data["files"] = [file.as_dict() for file in self.files]

Expand All @@ -620,7 +640,8 @@ def as_dict(self):
license_info.as_dict() for license_info in self.extracted_licenses
]

if self.relationships:
# For SPDX 2.2, add relationships separately (without DESCRIBES)
if self.version == SPDX_SPEC_VERSION_2_2 and self.relationships:
data["relationships"] = [
relationship.as_dict() for relationship in self.relationships
]
Expand All @@ -636,13 +657,37 @@ def as_json(self, indent=2):

@classmethod
def from_data(cls, data):
spdx_id = data.get("SPDXID", "SPDXRef-DOCUMENT")
relationships_data = data.get("relationships", [])

# Extract describes from either documentDescribes (SPDX 2.2) or
# DESCRIBES relationships (SPDX 2.3+)
describes = data.get("documentDescribes")
if describes is None:
# Extract from DESCRIBES relationships
describes = [
rel.get("relatedSpdxElement")
for rel in relationships_data
if rel.get("relationshipType") == "DESCRIBES"
and rel.get("spdxElementId") == spdx_id
]

# Filter out DESCRIBES relationships to avoid duplication
filtered_relationships = [
rel for rel in relationships_data
if not (
rel.get("relationshipType") == "DESCRIBES"
and rel.get("spdxElementId") == spdx_id
)
]

return cls(
spdx_id=data.get("SPDXID"),
spdx_id=spdx_id,
version=data.get("spdxVersion", "").split("SPDX-")[-1],
data_license=data.get("dataLicense"),
name=data.get("name"),
namespace=data.get("documentNamespace"),
describes=data.get("documentDescribes"),
describes=describes or [],
creation_info=CreationInfo.from_data(data.get("creationInfo", {})),
packages=[
Package.from_data(package_data)
Expand All @@ -655,7 +700,7 @@ def from_data(cls, data):
],
relationships=[
Relationship.from_data(relationship_data)
for relationship_data in data.get("relationships", [])
for relationship_data in filtered_relationships
],
comment=data.get("comment"),
)
Expand Down
123 changes: 115 additions & 8 deletions scanpipe/tests/pipes/test_spdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,13 @@ def setUp(self):
],
"comment": "This document was created using SPDXCode-1.0",
}
# SPDX 2.3 uses DESCRIBES relationships instead of documentDescribes
self.document_spdx_data = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "document_name",
"documentNamespace": "https://[CreatorWebsite]/[DocumentName]-[UUID]",
"documentDescribes": ["SPDXRef-project"],
"creationInfo": {
"created": "2022-09-21T13:50:20Z",
"creators": [
Expand Down Expand Up @@ -246,6 +246,19 @@ def setUp(self):
],
},
],
# DESCRIBES relationship replaces documentDescribes in SPDX 2.3+
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-project",
"relationshipType": "DESCRIBES",
},
{
"spdxElementId": "SPDXRef-package1",
"relatedSpdxElement": "SPDXRef-file1",
"relationshipType": "CONTAINS",
},
],
"files": [
{
"SPDXID": "SPDXRef-file1",
Expand All @@ -271,13 +284,6 @@ def setUp(self):
"seeAlsos": ["https://license1.text", "https://license1.homepage"],
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-package1",
"relatedSpdxElement": "SPDXRef-file1",
"relationshipType": "CONTAINS",
}
],
"comment": "This document was created using SPDXCode-1.0",
}

Expand Down Expand Up @@ -412,3 +418,104 @@ def test_spdx_validate_document(self):

with self.assertRaises(Exception):
spdx.validate_document({}, self.schema_2_3)

def test_spdx_document_2_3_uses_describes_relationships(self):
"""Test that SPDX 2.3 documents use DESCRIBES relationships instead of
the deprecated documentDescribes field."""
document = spdx.Document(**self.document_data)
document.version = spdx.SPDX_SPEC_VERSION_2_3
result = document.as_dict()

# Should NOT have documentDescribes
assert "documentDescribes" not in result

# Should have DESCRIBES relationship
describes_rels = [
rel for rel in result.get("relationships", [])
if rel.get("relationshipType") == "DESCRIBES"
]
assert len(describes_rels) == 1
assert describes_rels[0]["spdxElementId"] == "SPDXRef-DOCUMENT"
assert describes_rels[0]["relatedSpdxElement"] == "SPDXRef-project"

def test_spdx_document_2_2_uses_document_describes(self):
"""Test that SPDX 2.2 documents still use documentDescribes for
backward compatibility."""
document = spdx.Document(**self.document_data)
document.version = spdx.SPDX_SPEC_VERSION_2_2
result = document.as_dict()

# Should have documentDescribes
assert "documentDescribes" in result
assert result["documentDescribes"] == ["SPDXRef-project"]

# Should NOT have DESCRIBES relationship
describes_rels = [
rel for rel in result.get("relationships", [])
if rel.get("relationshipType") == "DESCRIBES"
]
assert len(describes_rels) == 0

def test_spdx_document_from_data_with_describes_relationships(self):
"""Test that from_data correctly extracts describes from DESCRIBES
relationships (SPDX 2.3+ format)."""
data_with_describes_rel = {
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.3",
"name": "test",
"documentNamespace": "https://example.com/test",
"creationInfo": {"created": "2022-01-01T00:00:00Z", "creators": ["Tool: test"]},
"packages": [],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-pkg1",
"relationshipType": "DESCRIBES",
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-pkg2",
"relationshipType": "DESCRIBES",
},
{
"spdxElementId": "SPDXRef-pkg1",
"relatedSpdxElement": "SPDXRef-file1",
"relationshipType": "CONTAINS",
},
],
}
document = spdx.Document.from_data(data_with_describes_rel)

# Should extract describes from DESCRIBES relationships
assert document.describes == ["SPDXRef-pkg1", "SPDXRef-pkg2"]

# Should filter out DESCRIBES relationships from relationships list
assert len(document.relationships) == 1
assert document.relationships[0].relationship == "CONTAINS"

def test_spdx_document_from_data_with_document_describes(self):
"""Test that from_data correctly handles documentDescribes
(SPDX 2.2 format)."""
data_with_document_describes = {
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.2",
"name": "test",
"documentNamespace": "https://example.com/test",
"documentDescribes": ["SPDXRef-pkg1", "SPDXRef-pkg2"],
"creationInfo": {"created": "2022-01-01T00:00:00Z", "creators": ["Tool: test"]},
"packages": [],
"relationships": [
{
"spdxElementId": "SPDXRef-pkg1",
"relatedSpdxElement": "SPDXRef-file1",
"relationshipType": "CONTAINS",
},
],
}
document = spdx.Document.from_data(data_with_document_describes)

# Should use documentDescribes directly
assert document.describes == ["SPDXRef-pkg1", "SPDXRef-pkg2"]

# Should keep all relationships
assert len(document.relationships) == 1