Skip to content

Commit 3752d1e

Browse files
committed
Add full support for SPDX 2.2 spec version
Signed-off-by: tdruez <[email protected]>
1 parent 9746835 commit 3752d1e

File tree

13 files changed

+839
-50
lines changed

13 files changed

+839
-50
lines changed

scanpipe/api/views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,19 @@ def results_download(self, request, *args, **kwargs):
153153
"""Return the results in the provided `output_format` as an attachment."""
154154
project = self.get_object()
155155
format = request.query_params.get("output_format", "json")
156+
156157
version = request.query_params.get("version")
157158
output_kwargs = {}
159+
if version:
160+
output_kwargs["version"] = version
158161

159162
if format == "json":
160163
return project_results_json_response(project, as_attachment=True)
161164
elif format == "xlsx":
162165
output_file = output.to_xlsx(project)
163166
elif format == "spdx":
164-
output_file = output.to_spdx(project)
167+
output_file = output.to_spdx(project, **output_kwargs)
165168
elif format == "cyclonedx":
166-
if version:
167-
output_kwargs["version"] = version
168169
output_file = output.to_cyclonedx(project, **output_kwargs)
169170
elif format == "attribution":
170171
output_file = output.to_attribution(project)

scanpipe/management/commands/output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def handle_output(self, output_format):
7171
output_kwargs = {}
7272
if ":" in output_format:
7373
output_format, version = output_format.split(":", maxsplit=1)
74-
if output_format != "cyclonedx":
74+
if output_format not in ["cyclonedx", "spdx"]:
7575
raise CommandError(
7676
'The ":" version syntax is only supported for the cyclonedx format.'
7777
)

scanpipe/pipes/output.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,12 +713,15 @@ def get_inputs_as_spdx_packages(project):
713713
return inputs_as_spdx_packages
714714

715715

716-
def to_spdx(project, include_files=False):
716+
def to_spdx(project, version=spdx.SPDX_SPEC_VERSION_2_3, include_files=False):
717717
"""
718718
Generate output for the provided ``project`` in SPDX document format.
719719
The output file is created in the ``project`` "output/" directory.
720720
Return the path of the generated output file.
721721
"""
722+
if version not in [spdx.SPDX_SPEC_VERSION_2_2, spdx.SPDX_SPEC_VERSION_2_3]:
723+
raise ValueError(f"SPDX {version} is not supported.")
724+
722725
output_file = project.get_output_file_path("results", "spdx.json")
723726
document_spdx_id = f"SPDXRef-DOCUMENT-{project.uuid}"
724727

@@ -786,6 +789,7 @@ def to_spdx(project, include_files=False):
786789
]
787790

788791
document = spdx.Document(
792+
version=version,
789793
spdx_id=document_spdx_id,
790794
name=f"scancodeio_{project.name}",
791795
namespace=f"https://scancode.io/spdxdocs/{project.uuid}",

scanpipe/pipes/schemas/spdx-schema-2.2.json

Lines changed: 721 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
about_resource: spdx-schema-2.2.json
2+
name: spdx-spec
3+
version: 2.2
4+
download_url: https://github.com/spdx/spdx-spec/raw/development/v2.2/schemas/spdx-schema.json
5+
description: The Software Package Data Exchange® (SPDX®) specification is a standard format
6+
for communicating the components, licenses and copyrights associated with software packages.
7+
homepage_url: https://spdx.org
8+
package_url: pkg:github/spdx/[email protected]?version_prefix=v#schemas/spdx-schema.json
9+
license_expression: cc-by-3.0
10+
copyright: Copyright (c) SPDX project contributors
11+
attribute: yes
12+
track_changes: yes
13+
licenses:
14+
- key: cc-by-3.0
15+
name: Creative Commons Attribution License 3.0

scanpipe/pipes/spdx.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,21 @@
2929
from datetime import timezone
3030
from pathlib import Path
3131

32-
SPDX_SPEC_VERSION = "2.3"
32+
SCHEMAS_LOCATION = Path(__file__).parent / "schemas"
3333
SPDX_LICENSE_LIST_VERSION = "3.20"
34-
SPDX_SCHEMA_NAME = "spdx-schema-2.3.json"
35-
SPDX_SCHEMA_PATH = Path(__file__).parent / "schemas" / SPDX_SCHEMA_NAME
36-
SPDX_SCHEMA_URL = (
34+
35+
SPDX_SPEC_VERSION_2_3 = "2.3"
36+
SPDX_SCHEMA_2_3_PATH = SCHEMAS_LOCATION / "spdx-schema-2.3.json"
37+
SPDX_SCHEMA_2_3_URL = (
3738
"https://github.com/spdx/spdx-spec/raw/development/v2.3.1/schemas/spdx-schema.json"
3839
)
3940

41+
SPDX_SPEC_VERSION_2_2 = "2.2"
42+
SPDX_SCHEMA_2_2_PATH = SCHEMAS_LOCATION / "spdx-schema-2.2.json"
43+
SPDX_SCHEMA_2_2_URL = (
44+
"https://github.com/spdx/spdx-spec/raw/development/v2.2/schemas/spdx-schema.json"
45+
)
46+
4047
"""
4148
Generate SPDX Documents.
4249
Spec documentation: https://spdx.github.io/spdx-spec/v2.3/
@@ -98,7 +105,7 @@
98105
print(document.as_json())
99106
100107
# Validate document
101-
schema = spdx.SPDX_SCHEMA_PATH.read_text()
108+
schema = spdx.SPDX_SCHEMA_2_3_PATH.read_text()
102109
document.validate(schema)
103110
104111
# Write document to a file:
@@ -233,14 +240,22 @@ class ExternalRef:
233240
downloadable content believed to be relevant to the Package.
234241
"""
235242

236-
category: str # Supported values: OTHER, SECURITY, PERSISTENT-ID, PACKAGE-MANAGER
243+
# Supported values:
244+
# v2.3: OTHER, SECURITY, PERSISTENT-ID, PACKAGE-MANAGER
245+
# v2.2: OTHER, SECURITY, PACKAGE_MANAGER
246+
category: str
237247
type: str
238248
locator: str
239249

240250
comment: str = ""
241251

242-
def as_dict(self):
252+
def as_dict(self, spec_version=SPDX_SPEC_VERSION_2_3):
243253
"""Return the data as a serializable dict."""
254+
255+
if spec_version == SPDX_SPEC_VERSION_2_2:
256+
if self.category == "PACKAGE-MANAGER":
257+
self.category = "PACKAGE_MANAGER"
258+
244259
data = {
245260
"referenceCategory": self.category,
246261
"referenceType": self.type,
@@ -345,7 +360,7 @@ class Package:
345360
external_refs: list[ExternalRef] = field(default_factory=list)
346361
attribution_texts: list[str] = field(default_factory=list)
347362

348-
def as_dict(self):
363+
def as_dict(self, spec_version=SPDX_SPEC_VERSION_2_3):
349364
"""Return the data as a serializable dict."""
350365
spdx_id = str(self.spdx_id)
351366
if not spdx_id.startswith("SPDXRef-"):
@@ -355,6 +370,7 @@ def as_dict(self):
355370
"name": self.name,
356371
"SPDXID": spdx_id,
357372
"downloadLocation": self.download_location or "NOASSERTION",
373+
"licenseDeclared": self.license_declared or "NOASSERTION",
358374
"licenseConcluded": self.license_concluded or "NOASSERTION",
359375
"copyrightText": self.copyright_text or "NOASSERTION",
360376
"filesAnalyzed": self.files_analyzed,
@@ -363,24 +379,28 @@ def as_dict(self):
363379
optional_data = {
364380
"versionInfo": self.version,
365381
"packageFileName": self.filename,
366-
"licenseDeclared": self.license_declared,
367382
"supplier": self.supplier,
368383
"originator": self.originator,
369384
"homepage": self.homepage,
370385
"description": self.description,
371386
"summary": self.summary,
372387
"sourceInfo": self.source_info,
373-
"releaseDate": self.date_to_iso(self.release_date),
374-
"builtDate": self.date_to_iso(self.built_date),
375-
"validUntilDate": self.date_to_iso(self.valid_until_date),
376-
"primaryPackagePurpose": self.primary_package_purpose,
377388
"comment": self.comment,
378389
"licenseComments": self.license_comments,
379390
"checksums": [checksum.as_dict() for checksum in self.checksums],
380-
"externalRefs": [ref.as_dict() for ref in self.external_refs],
391+
"externalRefs": [ref.as_dict(spec_version) for ref in self.external_refs],
381392
"attributionTexts": self.attribution_texts,
382393
}
383394

395+
# Fields only valid in 2.3
396+
if spec_version == SPDX_SPEC_VERSION_2_3:
397+
optional_data.update({
398+
"releaseDate": self.date_to_iso(self.release_date),
399+
"builtDate": self.date_to_iso(self.built_date),
400+
"validUntilDate": self.date_to_iso(self.valid_until_date),
401+
"primaryPackagePurpose": self.primary_package_purpose,
402+
})
403+
384404
optional_data = {key: value for key, value in optional_data.items() if value}
385405
return {**required_data, **optional_data}
386406

@@ -567,7 +587,7 @@ class Document:
567587
packages: list[Package]
568588

569589
spdx_id: str = "SPDXRef-DOCUMENT"
570-
version: str = SPDX_SPEC_VERSION
590+
version: str = SPDX_SPEC_VERSION_2_3
571591
data_license: str = "CC0-1.0"
572592
comment: str = ""
573593

@@ -585,7 +605,7 @@ def as_dict(self):
585605
"documentNamespace": self.namespace,
586606
"documentDescribes": self.describes,
587607
"creationInfo": self.creation_info.as_dict(),
588-
"packages": [package.as_dict() for package in self.packages],
608+
"packages": [package.as_dict(self.version) for package in self.packages],
589609
}
590610

591611
if self.files:
@@ -646,7 +666,7 @@ def validate(self, schema):
646666
return validate_document(document=self.as_dict(), schema=schema)
647667

648668

649-
def validate_document(document, schema=SPDX_SCHEMA_PATH):
669+
def validate_document(document, schema=SPDX_SCHEMA_2_3_PATH):
650670
"""
651671
SPDX document validation.
652672
Requires the `jsonschema` library.

scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
<a href="{% url 'project_results' project.slug 'xlsx' %}" class="dropdown-item">
1616
<strong>XLSX</strong>
1717
</a>
18-
<a href="{% url 'project_results' project.slug 'spdx' %}" class="dropdown-item">
18+
<div class="dropdown-item">
1919
<strong>SPDX</strong>
20-
</a>
20+
<div class="has-text-weight-semibold">
21+
<div class="buttons">
22+
<a href="{% url 'project_results' project.slug 'spdx' '2.3' %}" class="tag is-link">2.3</a>
23+
<a href="{% url 'project_results' project.slug 'spdx' '2.2' %}" class="tag">2.2</a>
24+
</div>
25+
</div>
26+
</div>
2127
<div class="dropdown-item">
2228
<strong>CycloneDX</strong>
2329
<div class="has-text-weight-semibold">

scanpipe/templates/scanpipe/includes/project_downloads.html

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,26 @@
77
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'xlsx' %}">
88
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>XLSX
99
</a>
10-
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'spdx' %}">
11-
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>SPDX
12-
</a>
10+
<div class="dropdown is-hoverable">
11+
<div class="dropdown-trigger">
12+
<button class="button tag is-success is-medium" aria-haspopup="true" aria-controls="dropdown-menu-spdx">
13+
<span class="icon">
14+
<i class="fa-solid fa-download" aria-hidden="true"></i>
15+
</span>
16+
<span>SPDX</span>
17+
</button>
18+
</div>
19+
<div class="dropdown-menu" id="dropdown-menu-spdx" role="menu">
20+
<div class="dropdown-content">
21+
<a href="{% url 'project_results' project.slug 'spdx' '2.3' %}" class="dropdown-item has-text-weight-semibold">
22+
Spec 2.3 <span class="tag is-link ml-1">Latest</span>
23+
</a>
24+
<a href="{% url 'project_results' project.slug 'spdx' '2.2' %}" class="dropdown-item">
25+
Spec 2.2
26+
</a>
27+
</div>
28+
</div>
29+
</div>
1330
<div class="dropdown is-hoverable">
1431
<div class="dropdown-trigger">
1532
<button class="button tag is-success is-medium" aria-haspopup="true" aria-controls="dropdown-menu-cyclonedx">

scanpipe/tests/data/asgiref/asgiref-3.3.0.spdx.json

Lines changed: 8 additions & 8 deletions
Large diffs are not rendered by default.

scanpipe/tests/data/spdx/dependencies.spdx.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@
1919
"name": "Analysis",
2020
"SPDXID": "SPDXRef-scancodeio-project-b74fe5df-e965-415e-ba65-f38421a0695d",
2121
"downloadLocation": "NOASSERTION",
22+
"licenseDeclared": "NOASSERTION",
2223
"licenseConcluded": "NOASSERTION",
2324
"copyrightText": "NOASSERTION",
24-
"filesAnalyzed": true,
25-
"licenseDeclared": "NOASSERTION"
25+
"filesAnalyzed": true
2626
},
2727
{
2828
"name": "a",
2929
"SPDXID": "SPDXRef-scancodeio-discoveredpackage-a83a60de-81bc-4bf4-b48c-dc78e0e658a9",
3030
"downloadLocation": "NOASSERTION",
31+
"licenseDeclared": "NOASSERTION",
3132
"licenseConcluded": "NOASSERTION",
3233
"copyrightText": "NOASSERTION",
3334
"filesAnalyzed": false,
@@ -43,6 +44,7 @@
4344
"name": "b",
4445
"SPDXID": "SPDXRef-scancodeio-discoveredpackage-81147701-285f-485c-ba36-9cd3742790b1",
4546
"downloadLocation": "NOASSERTION",
47+
"licenseDeclared": "NOASSERTION",
4648
"licenseConcluded": "NOASSERTION",
4749
"copyrightText": "NOASSERTION",
4850
"filesAnalyzed": false,
@@ -58,6 +60,7 @@
5860
"name": "z",
5961
"SPDXID": "SPDXRef-scancodeio-discoveredpackage-e391c33e-d7d0-4a97-a3c3-e947375c53d5",
6062
"downloadLocation": "NOASSERTION",
63+
"licenseDeclared": "NOASSERTION",
6164
"licenseConcluded": "NOASSERTION",
6265
"copyrightText": "NOASSERTION",
6366
"filesAnalyzed": false,
@@ -73,19 +76,19 @@
7376
"name": "",
7477
"SPDXID": "SPDXRef-scancodeio-discovereddependency-d0e1eab2-9b8b-449b-b9d1-12147ffdd8a8",
7578
"downloadLocation": "NOASSERTION",
79+
"licenseDeclared": "NOASSERTION",
7680
"licenseConcluded": "NOASSERTION",
7781
"copyrightText": "NOASSERTION",
78-
"filesAnalyzed": false,
79-
"licenseDeclared": "NOASSERTION"
82+
"filesAnalyzed": false
8083
},
8184
{
8285
"name": "unresolved",
8386
"SPDXID": "SPDXRef-scancodeio-discovereddependency-29fbe562-a191-44b4-88e8-a9678071ecee",
8487
"downloadLocation": "NOASSERTION",
88+
"licenseDeclared": "NOASSERTION",
8589
"licenseConcluded": "NOASSERTION",
8690
"copyrightText": "NOASSERTION",
8791
"filesAnalyzed": false,
88-
"licenseDeclared": "NOASSERTION",
8992
"externalRefs": [
9093
{
9194
"referenceCategory": "PACKAGE-MANAGER",

0 commit comments

Comments
 (0)