Skip to content
Merged
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
7 changes: 4 additions & 3 deletions scanpipe/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,19 @@ def results_download(self, request, *args, **kwargs):
"""Return the results in the provided `output_format` as an attachment."""
project = self.get_object()
format = request.query_params.get("output_format", "json")

version = request.query_params.get("version")
output_kwargs = {}
if version:
output_kwargs["version"] = version

if format == "json":
return project_results_json_response(project, as_attachment=True)
elif format == "xlsx":
output_file = output.to_xlsx(project)
elif format == "spdx":
output_file = output.to_spdx(project)
output_file = output.to_spdx(project, **output_kwargs)
elif format == "cyclonedx":
if version:
output_kwargs["version"] = version
output_file = output.to_cyclonedx(project, **output_kwargs)
elif format == "attribution":
output_file = output.to_attribution(project)
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/management/commands/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def handle_output(self, output_format):
output_kwargs = {}
if ":" in output_format:
output_format, version = output_format.split(":", maxsplit=1)
if output_format != "cyclonedx":
if output_format not in ["cyclonedx", "spdx"]:
raise CommandError(
'The ":" version syntax is only supported for the cyclonedx format.'
)
Expand Down
7 changes: 6 additions & 1 deletion scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,12 @@ def add_upload(self, uploaded_file, tag=""):
adds the `input_source`.
"""
self.write_input_file(uploaded_file)
self.add_input_source(filename=uploaded_file.name, is_uploaded=True, tag=tag)
input_source = self.add_input_source(
filename=uploaded_file.name,
is_uploaded=True,
tag=tag,
)
return input_source

def add_uploads(self, uploads):
"""
Expand Down
65 changes: 62 additions & 3 deletions scanpipe/pipes/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io
import json
import re
import uuid
from operator import attrgetter
from pathlib import Path

Expand Down Expand Up @@ -694,27 +695,83 @@ def get_dependency_as_spdx_relationship(dependency, document_spdx_id, packages_a
return spdx_relationship


def to_spdx(project, include_files=False):
def get_inputs_as_spdx_packages(project):
"""Return the Project's inputs as SPDX package to be used as root elements."""
inputs_as_spdx_packages = []

for input_source in project.get_inputs_with_source():
input_uuid = input_source.get("uuid") or uuid.uuid4()

input_as_spdx_package = spdx.Package(
spdx_id=f"SPDXRef-scancodeio-input-{input_uuid}",
name=input_source.get("filename"),
filename=input_source.get("filename"),
download_location=input_source.get("download_url"),
files_analyzed=True,
)
inputs_as_spdx_packages.append(input_as_spdx_package)

return inputs_as_spdx_packages


def to_spdx(project, version=spdx.SPDX_SPEC_VERSION_2_3, include_files=False):
"""
Generate output for the provided ``project`` in SPDX document format.
The output file is created in the ``project`` "output/" directory.
Return the path of the generated output file.
"""
if version not in [spdx.SPDX_SPEC_VERSION_2_2, spdx.SPDX_SPEC_VERSION_2_3]:
raise ValueError(f"SPDX {version} is not supported.")

output_file = project.get_output_file_path("results", "spdx.json")
document_spdx_id = f"SPDXRef-DOCUMENT-{project.uuid}"

discoveredpackage_qs = get_queryset(project, "discoveredpackage")
discovereddependency_qs = get_queryset(project, "discovereddependency")

document_spdx_id = f"SPDXRef-DOCUMENT-{project.uuid}"
packages_as_spdx = []
license_expressions = []
relationships = []

project_inputs_as_spdx_packages = get_inputs_as_spdx_packages(project)

if project_inputs_as_spdx_packages:
packages_as_spdx.extend(project_inputs_as_spdx_packages)

# Use the Project's input as the root element that the SPDX document describes.
# This ensures "documentDescribes" points only to the main subject of the SBOM,
# not to every dependency or file in the project.
# See https://github.com/spdx/spdx-spec/issues/395 and
# https://github.com/aboutcode-org/scancode.io/issues/564#issuecomment-3269296563
# for detailed context.
if len(project_inputs_as_spdx_packages) == 1:
describe_spdx_id = project_inputs_as_spdx_packages[0].spdx_id

# Fallback to the Project as the SPDX root element for the "documentDescribes",
# if more than one input, or if no inputs, are available.
else:
project_as_root_package = spdx.Package(
spdx_id=f"SPDXRef-scancodeio-project-{project.uuid}",
name=project.name,
files_analyzed=True,
)
packages_as_spdx.append(project_as_root_package)
describe_spdx_id = project_as_root_package.spdx_id

for package in discoveredpackage_qs:
packages_as_spdx.append(package.as_spdx())
spdx_package = package.as_spdx()
packages_as_spdx.append(spdx_package)

if license_expression := package.declared_license_expression:
license_expressions.append(license_expression)

spdx_relationship = spdx.Relationship(
spdx_id=describe_spdx_id,
related_spdx_id=spdx_package.spdx_id,
relationship="DEPENDS_ON",
)
relationships.append(spdx_relationship)

for dependency in discovereddependency_qs:
spdx_relationship = get_dependency_as_spdx_relationship(
dependency,
Expand All @@ -731,9 +788,11 @@ def to_spdx(project, include_files=False):
]

document = spdx.Document(
version=version,
spdx_id=document_spdx_id,
name=f"scancodeio_{project.name}",
namespace=f"https://scancode.io/spdxdocs/{project.uuid}",
describes=[describe_spdx_id],
creation_info=spdx.CreationInfo(tool=f"ScanCode.io-{scancodeio_version}"),
packages=packages_as_spdx,
files=files_as_spdx,
Expand Down
Loading