2525import io
2626import json
2727import re
28+ import uuid
2829from operator import attrgetter
2930from pathlib import Path
3031
@@ -694,27 +695,83 @@ def get_dependency_as_spdx_relationship(dependency, document_spdx_id, packages_a
694695 return spdx_relationship
695696
696697
697- def to_spdx (project , include_files = False ):
698+ def get_inputs_as_spdx_packages (project ):
699+ """Return the Project's inputs as SPDX package to be used as root elements."""
700+ inputs_as_spdx_packages = []
701+
702+ for input_source in project .get_inputs_with_source ():
703+ input_uuid = input_source .get ("uuid" ) or uuid .uuid4 ()
704+
705+ input_as_spdx_package = spdx .Package (
706+ spdx_id = f"SPDXRef-scancodeio-input-{ input_uuid } " ,
707+ name = input_source .get ("filename" ),
708+ filename = input_source .get ("filename" ),
709+ download_location = input_source .get ("download_url" ),
710+ files_analyzed = True ,
711+ )
712+ inputs_as_spdx_packages .append (input_as_spdx_package )
713+
714+ return inputs_as_spdx_packages
715+
716+
717+ def to_spdx (project , version = spdx .SPDX_SPEC_VERSION_2_3 , include_files = False ):
698718 """
699719 Generate output for the provided ``project`` in SPDX document format.
700720 The output file is created in the ``project`` "output/" directory.
701721 Return the path of the generated output file.
702722 """
723+ if version not in [spdx .SPDX_SPEC_VERSION_2_2 , spdx .SPDX_SPEC_VERSION_2_3 ]:
724+ raise ValueError (f"SPDX { version } is not supported." )
725+
703726 output_file = project .get_output_file_path ("results" , "spdx.json" )
727+ document_spdx_id = f"SPDXRef-DOCUMENT-{ project .uuid } "
704728
705729 discoveredpackage_qs = get_queryset (project , "discoveredpackage" )
706730 discovereddependency_qs = get_queryset (project , "discovereddependency" )
707731
708- document_spdx_id = f"SPDXRef-DOCUMENT-{ project .uuid } "
709732 packages_as_spdx = []
710733 license_expressions = []
711734 relationships = []
712735
736+ project_inputs_as_spdx_packages = get_inputs_as_spdx_packages (project )
737+
738+ if project_inputs_as_spdx_packages :
739+ packages_as_spdx .extend (project_inputs_as_spdx_packages )
740+
741+ # Use the Project's input as the root element that the SPDX document describes.
742+ # This ensures "documentDescribes" points only to the main subject of the SBOM,
743+ # not to every dependency or file in the project.
744+ # See https://github.com/spdx/spdx-spec/issues/395 and
745+ # https://github.com/aboutcode-org/scancode.io/issues/564#issuecomment-3269296563
746+ # for detailed context.
747+ if len (project_inputs_as_spdx_packages ) == 1 :
748+ describe_spdx_id = project_inputs_as_spdx_packages [0 ].spdx_id
749+
750+ # Fallback to the Project as the SPDX root element for the "documentDescribes",
751+ # if more than one input, or if no inputs, are available.
752+ else :
753+ project_as_root_package = spdx .Package (
754+ spdx_id = f"SPDXRef-scancodeio-project-{ project .uuid } " ,
755+ name = project .name ,
756+ files_analyzed = True ,
757+ )
758+ packages_as_spdx .append (project_as_root_package )
759+ describe_spdx_id = project_as_root_package .spdx_id
760+
713761 for package in discoveredpackage_qs :
714- packages_as_spdx .append (package .as_spdx ())
762+ spdx_package = package .as_spdx ()
763+ packages_as_spdx .append (spdx_package )
764+
715765 if license_expression := package .declared_license_expression :
716766 license_expressions .append (license_expression )
717767
768+ spdx_relationship = spdx .Relationship (
769+ spdx_id = describe_spdx_id ,
770+ related_spdx_id = spdx_package .spdx_id ,
771+ relationship = "DEPENDS_ON" ,
772+ )
773+ relationships .append (spdx_relationship )
774+
718775 for dependency in discovereddependency_qs :
719776 spdx_relationship = get_dependency_as_spdx_relationship (
720777 dependency ,
@@ -731,9 +788,11 @@ def to_spdx(project, include_files=False):
731788 ]
732789
733790 document = spdx .Document (
791+ version = version ,
734792 spdx_id = document_spdx_id ,
735793 name = f"scancodeio_{ project .name } " ,
736794 namespace = f"https://scancode.io/spdxdocs/{ project .uuid } " ,
795+ describes = [describe_spdx_id ],
737796 creation_info = spdx .CreationInfo (tool = f"ScanCode.io-{ scancodeio_version } " ),
738797 packages = packages_as_spdx ,
739798 files = files_as_spdx ,
0 commit comments