Skip to content

Commit b25021c

Browse files
committed
Add ORT package-list.yml output format
Signed-off-by: tdruez <[email protected]>
1 parent 0067725 commit b25021c

File tree

7 files changed

+117
-1
lines changed

7 files changed

+117
-1
lines changed

scanpipe/api/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ def results_download(self, request, *args, **kwargs):
168168
output_file = output.to_cyclonedx(project, **output_kwargs)
169169
elif format == "attribution":
170170
output_file = output.to_attribution(project)
171+
elif format == "ort":
172+
output_file = output.to_ort(project)
171173
else:
172174
message = {"status": f"Format {format} not supported."}
173175
return Response(message, status=status.HTTP_400_BAD_REQUEST)

scanpipe/management/commands/output.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from scanpipe.management.commands import ProjectCommand
2626
from scanpipe.pipes import output
2727

28-
SUPPORTED_FORMATS = ["json", "csv", "xlsx", "attribution", "spdx", "cyclonedx"]
28+
SUPPORTED_FORMATS = ["json", "csv", "xlsx", "attribution", "spdx", "cyclonedx", "ort"]
2929

3030

3131
class Command(ProjectCommand):
@@ -84,6 +84,7 @@ def handle_output(self, output_format):
8484
"spdx": output.to_spdx,
8585
"cyclonedx": output.to_cyclonedx,
8686
"attribution": output.to_attribution,
87+
"ort": output.to_ort,
8788
}.get(output_format)
8889

8990
if not output_function:

scanpipe/pipes/ort.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
23+
24+
from dataclasses import asdict
25+
from dataclasses import dataclass
26+
from dataclasses import field
27+
from pathlib import Path
28+
29+
import yaml
30+
31+
32+
@dataclass
33+
class SourceArtifact:
34+
url: str
35+
36+
37+
@dataclass
38+
class Dependency:
39+
id: str
40+
purl: str
41+
sourceArtifact: SourceArtifact
42+
declaredLicenses: list = field(default_factory=list)
43+
44+
45+
@dataclass
46+
class Project:
47+
projectName: str
48+
dependencies: list[Dependency] = field(default_factory=list)
49+
50+
@classmethod
51+
def from_yaml(cls, yaml_str: str):
52+
"""Create a Project object from a YAML string."""
53+
data = yaml.safe_load(yaml_str)
54+
deps = [
55+
Dependency(
56+
id=dependency["id"],
57+
purl=dependency["purl"],
58+
sourceArtifact=SourceArtifact(**dependency["sourceArtifact"]),
59+
declaredLicenses=dependency.get("declaredLicenses", []),
60+
)
61+
for dependency in data.get("dependencies", [])
62+
]
63+
return cls(projectName=data["projectName"], dependencies=deps)
64+
65+
@classmethod
66+
def from_file(cls, filepath: str | Path):
67+
"""Create a Project object by loading a YAML file."""
68+
return cls.from_yaml(Path(filepath).read_text(encoding="utf-8"))
69+
70+
def to_yaml(self) -> str:
71+
"""Dump the Project object back to a YAML string."""
72+
return yaml.safe_dump(asdict(self), sort_keys=False, allow_unicode=True)
73+
74+
75+
def to_ort(project):
76+
dependencies = []
77+
for package in project.discoveredpackages.all():
78+
dependency = Dependency(
79+
id=f"{package.type}::{package.name}:{package.version}",
80+
purl=package.purl,
81+
sourceArtifact=SourceArtifact(url=package.download_url),
82+
declaredLicenses=[package.get_declared_license_expression_spdx()],
83+
)
84+
dependencies.append(dependency)
85+
86+
project = Project(
87+
projectName=project.name,
88+
dependencies=dependencies,
89+
)
90+
91+
return project.to_yaml()

scanpipe/pipes/output.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from scanpipe.models import ProjectMessage
6161
from scanpipe.pipes import docker
6262
from scanpipe.pipes import flag
63+
from scanpipe.pipes import ort
6364
from scanpipe.pipes import spdx
6465

6566
scanpipe_app = apps.get_app_config("scanpipe")
@@ -1058,10 +1059,23 @@ def to_attribution(project):
10581059
return output_file
10591060

10601061

1062+
def to_ort(project):
1063+
"""
1064+
Generate a ORT compatible "package-list.yml" output.
1065+
The output file is created in the ``project`` "output/" directory.
1066+
Return the path of the generated output file.
1067+
"""
1068+
output_file = project.get_output_file_path("results", "package-list.yml")
1069+
ort_yml = ort.to_ort(project)
1070+
output_file.write_text(ort_yml)
1071+
return output_file
1072+
1073+
10611074
FORMAT_TO_FUNCTION_MAPPING = {
10621075
"json": to_json,
10631076
"xlsx": to_xlsx,
10641077
"spdx": to_spdx,
10651078
"cyclonedx": to_cyclonedx,
10661079
"attribution": to_attribution,
1080+
"ort": to_ort,
10671081
}

scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<a href="{% url 'project_results' project.slug 'attribution' %}" class="dropdown-item">
3232
<strong>Attribution</strong>
3333
</a>
34+
<a href="{% url 'project_results' project.slug 'ort' %}" class="dropdown-item">
35+
<strong>ORT (package-list)</strong>
36+
</a>
3437
</div>
3538
</div>
3639
</div>

scanpipe/templates/scanpipe/includes/project_downloads.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@
3636
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'attribution' %}">
3737
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>Attribution
3838
</a>
39+
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'ort' %}">
40+
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>ORT
41+
</a>
3942
</div>
4043
</article>

scanpipe/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,8 @@ def get(self, request, *args, **kwargs):
16301630
output_file = output.to_cyclonedx(project, **output_kwargs)
16311631
elif format == "attribution":
16321632
output_file = output.to_attribution(project)
1633+
elif format == "ort":
1634+
output_file = output.to_ort(project)
16331635
else:
16341636
raise Http404("Format not supported.")
16351637

0 commit comments

Comments
 (0)