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
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
--------------------
Expand Down
2 changes: 2 additions & 0 deletions scanpipe/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion scanpipe/management/commands/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
153 changes: 153 additions & 0 deletions scanpipe/pipes/ort.py
Original file line number Diff line number Diff line change
@@ -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<String> = emptySet(),
# val concludedLicense: SpdxExpression? = null,
# val description: String? = null,
# val homepageUrl: String? = null,
# val isExcluded: Boolean = false,
# val isDynamicallyLinked: Boolean = false,
# val labels: Map<String, String> = 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<Dependency> = 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()
14 changes: 14 additions & 0 deletions scanpipe/pipes/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
<a href="{% url 'project_results' project.slug 'attribution' %}" class="dropdown-item">
<strong>Attribution</strong>
</a>
<a href="{% url 'project_results' project.slug 'ort-package-list' %}" class="dropdown-item">
<strong>ORT (package-list)</strong>
</a>
</div>
</div>
</div>
25 changes: 24 additions & 1 deletion scanpipe/templates/scanpipe/includes/project_downloads.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
</a>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button tag is-success is-medium" aria-haspopup="true" aria-controls="dropdown-menu-cyclonedx">
<button class="button tag is-success is-medium has-text-weight-normal" aria-haspopup="true" aria-controls="dropdown-menu-cyclonedx">
<span class="icon">
<i class="fa-solid fa-download" aria-hidden="true"></i>
</span>
<span>CycloneDX</span>
<span class="icon is-small">
<i class="fa-solid fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu-cyclonedx" role="menu">
Expand All @@ -36,5 +39,25 @@
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'attribution' %}">
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>Attribution
</a>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button tag is-success is-medium has-text-weight-normal" aria-haspopup="true" aria-controls="dropdown-menu-ort">
<span class="icon">
<i class="fa-solid fa-download" aria-hidden="true"></i>
</span>
<span>Tools formats</span>
<span class="icon is-small">
<i class="fa-solid fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu-ort" role="menu">
<div class="dropdown-content">
<a href="{% url 'project_results' project.slug 'ort-package-list' %}" class="dropdown-item has-text-weight-semibold">
ORT package-list.yml
</a>
</div>
</div>
</div>
</div>
</article>
Loading