diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 42444ed4ce..ff1f149a49 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,8 @@ v35.4.0 (unreleased) ``scanpipe.pipes.federatedcode.push_changes``. Add ``scanpipe.pipes.federatedcode.write_data_as_yaml``. +- Add ORT ``package-list.yml`` as new downloadable output format. + https://github.com/aboutcode-org/scancode.io/pull/1852 v35.3.0 (2025-08-20) -------------------- diff --git a/scanpipe/api/views.py b/scanpipe/api/views.py index d5d91189cb..2cb48b1222 100644 --- a/scanpipe/api/views.py +++ b/scanpipe/api/views.py @@ -168,6 +168,8 @@ def results_download(self, request, *args, **kwargs): output_file = output.to_cyclonedx(project, **output_kwargs) elif format == "attribution": output_file = output.to_attribution(project) + elif format == "ort-package-list": + output_file = output.to_ort_package_list_yml(project) else: message = {"status": f"Format {format} not supported."} return Response(message, status=status.HTTP_400_BAD_REQUEST) diff --git a/scanpipe/management/commands/output.py b/scanpipe/management/commands/output.py index 5e6f3e74cc..23b69ab7ae 100644 --- a/scanpipe/management/commands/output.py +++ b/scanpipe/management/commands/output.py @@ -25,7 +25,15 @@ from scanpipe.management.commands import ProjectCommand from scanpipe.pipes import output -SUPPORTED_FORMATS = ["json", "csv", "xlsx", "attribution", "spdx", "cyclonedx"] +SUPPORTED_FORMATS = [ + "json", + "csv", + "xlsx", + "attribution", + "spdx", + "cyclonedx", + "ort-package-list", +] class Command(ProjectCommand): @@ -84,6 +92,7 @@ def handle_output(self, output_format): "spdx": output.to_spdx, "cyclonedx": output.to_cyclonedx, "attribution": output.to_attribution, + "ort-package-list": output.to_ort_package_list_yml, }.get(output_format) if not output_function: diff --git a/scanpipe/pipes/ort.py b/scanpipe/pipes/ort.py new file mode 100644 index 0000000000..036b1f1fac --- /dev/null +++ b/scanpipe/pipes/ort.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# http://nexb.com and https://github.com/aboutcode-org/scancode.io +# The ScanCode.io software is licensed under the Apache License version 2.0. +# Data generated with ScanCode.io is provided as-is without warranties. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# +# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode.io should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# +# ScanCode.io is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/aboutcode-org/scancode.io for support and download. + + +from dataclasses import asdict +from dataclasses import dataclass +from dataclasses import field +from pathlib import Path + +import saneyaml + +""" +This module provides Python dataclass models for representing a package list +in a format compatible with the OSS Review Toolkit (ORT) +`CreateAnalyzerResultFromPackageListCommand`. + +The models are simplified adaptations of the Kotlin classes from: +https://github.com/oss-review-toolkit/ort/blob/main/cli-helper/src/main/kotlin/commands/CreateAnalyzerResultFromPackageListCommand.kt + +This module is intended for generating ORT-compatible YAML package lists from Python +objects, allowing integration with ORT's analyzer workflows or manual creation of +package metadata. +""" + + +# private data class SourceArtifact( +# val url: String, +# val hash: Hash? = null +# ) +@dataclass +class SourceArtifact: + url: str + # Cannot coerce empty String ("") to `org.ossreviewtoolkit.model.Hash` value + # hash: str = None + + +# private data class Vcs( +# val type: String? = null, +# val url: String? = null, +# val revision: String? = null, +# val path: String? = null +# ) +@dataclass +class Vcs: + type: str = None + url: str = None + revision: str = None + path: str = None + + +# private data class Dependency( +# val id: Identifier, +# val purl: String? = null, +# val vcs: Vcs? = null, +# val sourceArtifact: SourceArtifact? = null, +# val declaredLicenses: Set = emptySet(), +# val concludedLicense: SpdxExpression? = null, +# val description: String? = null, +# val homepageUrl: String? = null, +# val isExcluded: Boolean = false, +# val isDynamicallyLinked: Boolean = false, +# val labels: Map = emptyMap() +# ) +@dataclass +class Dependency: + id: str + purl: str = None + vcs: Vcs = None + sourceArtifact: SourceArtifact = None + declaredLicenses: list = field(default_factory=set) + # concludedLicense: str = None + description: str = None + homepageUrl: str = None + # isExcluded: bool = False + # isDynamicallyLinked: bool = False + # labels: dict = field(default_factory=dict) + + +# private data class PackageList( +# val projectName: String? = null, +# val projectVcs: Vcs? = null, +# val dependencies: List = emptyList() +# ) +@dataclass +class PackageList: + projectName: str + projectVcs: Vcs = field(default_factory=Vcs) + dependencies: list = field(default_factory=list) + + def to_yaml(self): + """Dump the Project object back to a YAML string.""" + return saneyaml.dump(asdict(self)) + + def to_file(self, filepath): + """Write the Project object to a YAML file.""" + Path(filepath).write_text(self.to_yaml(), encoding="utf-8") + + +def get_ort_project_type(project): + """ + Determine the ORT project type based on the project's input sources. + + Currently, this function checks whether any of the project's + input download URLs start with "docker://". + If at least one Docker URL is found, it returns "docker". + """ + inputs_url = project.inputsources.values_list("download_url", flat=True) + if any(url.startswith("docker://") for url in inputs_url): + return "docker" + + +def to_ort_package_list_yml(project): + """Convert a project object into a YAML string in the ORT package list format.""" + project_type = get_ort_project_type(project) + + dependencies = [] + for package in project.discoveredpackages.all(): + dependency = Dependency( + id=f"{project_type or package.type}::{package.name}:{package.version}", + purl=package.purl, + sourceArtifact=SourceArtifact(url=package.download_url), + declaredLicenses=[package.get_declared_license_expression_spdx()], + vcs=Vcs(url=package.vcs_url), + description=package.description, + homepageUrl=package.homepage_url, + ) + dependencies.append(dependency) + + package_list = PackageList( + projectName=project.name, + dependencies=dependencies, + ) + + return package_list.to_yaml() diff --git a/scanpipe/pipes/output.py b/scanpipe/pipes/output.py index 44b77f120f..3c2ea38f9f 100644 --- a/scanpipe/pipes/output.py +++ b/scanpipe/pipes/output.py @@ -60,6 +60,7 @@ from scanpipe.models import ProjectMessage from scanpipe.pipes import docker from scanpipe.pipes import flag +from scanpipe.pipes import ort from scanpipe.pipes import spdx scanpipe_app = apps.get_app_config("scanpipe") @@ -1058,10 +1059,23 @@ def to_attribution(project): return output_file +def to_ort_package_list_yml(project): + """ + Generate an ORT compatible "package-list.yml" output. + The output file is created in the ``project`` "output/" directory. + Return the path of the generated output file. + """ + output_file = project.get_output_file_path("results", "package-list.yml") + ort_yml = ort.to_ort_package_list_yml(project) + output_file.write_text(ort_yml) + return output_file + + FORMAT_TO_FUNCTION_MAPPING = { "json": to_json, "xlsx": to_xlsx, "spdx": to_spdx, "cyclonedx": to_cyclonedx, "attribution": to_attribution, + "ort-package-list": to_ort_package_list_yml, } diff --git a/scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html b/scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html index 4fdf083c01..571d0b8ac5 100644 --- a/scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html +++ b/scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html @@ -31,6 +31,9 @@ Attribution + + ORT (package-list) + \ No newline at end of file diff --git a/scanpipe/templates/scanpipe/includes/project_downloads.html b/scanpipe/templates/scanpipe/includes/project_downloads.html index d02c747c2d..1604485767 100644 --- a/scanpipe/templates/scanpipe/includes/project_downloads.html +++ b/scanpipe/templates/scanpipe/includes/project_downloads.html @@ -12,11 +12,14 @@