• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright (c) 2025 Northeastern University
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19from collections import OrderedDict
20from typing import Dict, Any
21
22from ohos.sbom.common.utils import remove_empty
23from ohos.sbom.converters.api import SBOMConverter
24from ohos.sbom.converters.base import SBOMFormat, ISBOMConverter
25from ohos.sbom.sbom.metadata.sbom_meta_data import Document, SBOMMetaData, Package, File, Hash, RelationshipType
26
27
28class SPDX23Converter(ISBOMConverter):
29    """
30    Converter for SPDX 2.3 format (strictly follows input model and field mapping).
31
32    Implements conversion from OpenHarmony SBOM metadata to SPDX 2.3 JSON format.
33    """
34
35    def convert(self, sbom_meta: SBOMMetaData) -> Dict[str, Any]:
36        """
37        Convert intermediate SBOM data to SPDX 2.3 format.
38
39        Args:
40            sbom_meta: Intermediate SBOM data containing document, packages,
41                      files and relationships
42
43        Returns:
44            OrderedDict: SPDX 2.3 compliant dictionary with empty values removed
45        """
46        spdx = OrderedDict()
47
48        # Document information
49        spdx.update(self._convert_document(sbom_meta.document))
50
51        # Package information
52        if sbom_meta.packages:
53            spdx["packages"] = [self._convert_package(pkg) for pkg in sbom_meta.packages]
54
55        # File information
56        if sbom_meta.files:
57            spdx["files"] = [self._convert_file(file) for file in sbom_meta.files]
58
59        # Relationship information
60        if sbom_meta.relationships:
61            spdx["relationships"] = [
62                self._convert_single_relationship(rel.bom_ref, target, rel.relationship_type)
63                for rel in sbom_meta.relationships
64                for target in rel.depends_on
65            ]
66
67        return remove_empty(spdx)
68
69    def _convert_document(self, doc: Document) -> Dict[str, Any]:
70        """
71        Convert document metadata to SPDX format.
72
73        Args:
74            doc: Document metadata object
75
76        Returns:
77            OrderedDict: SPDX document section
78        """
79        return OrderedDict([
80            ("SPDXID", doc.doc_id or "SPDXRef-DOCUMENT"),
81            ("spdxVersion", "2.3"),
82            ("creationInfo", self._build_creation_info(doc)),
83            ("name", doc.name or "Unnamed SBOM"),
84            ("dataLicense", doc.data_license),
85            ("documentNamespace", doc.document_namespace or self._generate_namespace(doc)),
86            ("comment", doc.doc_comments)
87        ])
88
89    def _build_creation_info(self, doc: Document) -> Dict[str, Any]:
90        """
91        Build SPDX creationInfo section.
92
93        Args:
94            doc: Document metadata object
95
96        Returns:
97            OrderedDict: creationInfo section with creators and timestamps
98        """
99        creators = []
100        if doc.tools:
101            creators.extend([
102                f"Tool: {tool.get('name', '')}-{tool.get('version', '')}"
103                for tool in doc.tools
104            ])
105        creators.append("Organization: OpenHarmony")
106
107        return OrderedDict([
108            ("created", doc.timestamp),
109            ("creators", creators),
110            ("licenseListVersion", doc.license_list_version),
111            ("comment", getattr(doc, 'doc_comments', None))
112        ])
113
114    def _generate_namespace(self, doc: Document) -> str:
115        """
116        Generate default document namespace URI.
117
118        Args:
119            doc: Document metadata object
120
121        Returns:
122            str: Generated namespace URI
123        """
124        return f"http://spdx.org/spdxdocs/{doc.name}-{doc.serial_number}"
125
126    def _convert_package(self, pkg: Package) -> Dict[str, Any]:
127        """
128        Convert package metadata to SPDX format.
129
130        Args:
131            pkg: Package metadata object
132
133        Returns:
134            OrderedDict: SPDX package entry with all required fields
135
136        Note:
137            Automatically handles package purpose validation and fallback
138        """
139        # SPDX 2.3 valid package purposes
140        valid_purposes = {
141            "SOURCE", "BINARY", "ARCHIVE", "APPLICATION", "FRAMEWORK",
142            "LIBRARY", "MODULE", "OPERATING-SYSTEM", "DEVICE", "FIRMWARE",
143            "CONTAINER", "FILE", "INSTALL", "OTHER"
144        }
145
146        purpose = (pkg.type or "OTHER").upper()
147        if purpose not in valid_purposes:
148            purpose = "OTHER"
149
150        pkg_data = OrderedDict([
151            ("SPDXID", pkg.purl or f"SPDXRef-Package-{pkg.name}"),
152            ("name", pkg.name),
153            ("versionInfo", pkg.version),
154            ("supplier", self._format_supplier(pkg.supplier)),
155            ("originator", self._format_supplier(pkg.group) if pkg.group else None),
156            ("downloadLocation", pkg.download_location or "NOASSERTION"),
157            ("filesAnalyzed", False),  # SPDX recommendation for SBOMs
158            ("licenseConcluded", pkg.license_concluded),
159            ("licenseDeclared", pkg.license_declared),
160            ("copyrightText", pkg.com_copyright or "NOASSERTION"),
161            ("externalRefs", [self._build_purl_ref(pkg.purl)] if pkg.purl else []),
162            ("checksums", [self._convert_hash(h) for h in pkg.hashes]),
163            ("primaryPackagePurpose", purpose)
164        ])
165        return remove_empty(pkg_data)
166
167    def _convert_file(self, file: File) -> Dict[str, Any]:
168        """
169        Convert file metadata to SPDX format.
170
171        Args:
172            file: File metadata object
173
174        Returns:
175            OrderedDict: SPDX file entry with all required fields
176        """
177        file_data = OrderedDict([
178            ("SPDXID", file.file_id),
179            ("fileName", file.file_name),
180            ("fileTypes", [t.upper() for t in file.file_types]),
181            ("checksums", [self._convert_hash(h) for h in file.checksums]),
182            ("licenseConcluded", file.license_concluded),
183            ("copyrightText", file.copyright_text or "NOASSERTION"),
184        ])
185        return remove_empty(file_data)
186
187    def _convert_single_relationship(self, subject_id: str, target_id: str, rel_type: RelationshipType) -> Dict[
188        str, Any]:
189        """
190        Convert relationship to SPDX format.
191
192        Args:
193            subject_id: Source component ID
194            target_id: Target component ID
195            rel_type: Relationship type enum
196
197        Returns:
198            OrderedDict: SPDX relationship entry
199        """
200        return OrderedDict([
201            ("spdxElementId", subject_id),
202            ("relatedSpdxElement", target_id),
203            ("relationshipType", rel_type.value)
204        ])
205
206    def _format_supplier(self, supplier: str) -> str:
207        """
208        Format supplier information according to SPDX spec.
209
210        Args:
211            supplier: Raw supplier string
212
213        Returns:
214            str: Formatted supplier string (with Person: prefix if contains @)
215        """
216        if not supplier:
217            return "NOASSERTION"
218        return f"Person: {supplier}" if "@" in supplier else supplier
219
220    def _build_purl_ref(self, purl: str) -> Dict[str, str]:
221        """
222        Build SPDX external reference for Package URL.
223
224        Args:
225            purl: Package URL string
226
227        Returns:
228            dict: SPDX external reference structure
229        """
230        return {
231            "referenceType": "purl",
232            "referenceLocator": purl
233        }
234
235    def _convert_hash(self, hash_obj: Hash) -> Dict[str, str]:
236        """
237        Convert hash object to SPDX format.
238
239        Args:
240            hash_obj: Hash object with algorithm and value
241
242        Returns:
243            dict: SPDX checksum structure
244        """
245        return {
246            "algorithm": hash_obj.alg.upper(),
247            "checksumValue": hash_obj.content
248        }
249
250
251# Register the converter with the factory
252SBOMConverter.register_format(SBOMFormat.SPDX, SPDX23Converter)
253