• 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 dataclasses import dataclass, field
21from enum import Enum
22from typing import List, Dict, Optional, Any
23
24from ohos.sbom.common.utils import remove_empty
25
26# SPDX standard value indicating no license assertion
27NOASSERTION = "NOASSERTION"
28
29
30# ---------------------------
31# Core Value Objects
32# ---------------------------
33@dataclass(frozen=True)
34class Hash:
35    """
36    Represents a cryptographic hash value for file/package verification.
37
38    Attributes:
39        alg: Hash algorithm (e.g. "SHA256", "MD5")
40        content: Hexadecimal hash value
41    """
42    alg: str
43    content: str
44
45    def __json__(self) -> Dict[str, str]:
46        """Support JSON module serialization."""
47        return self.to_dict()
48
49    @classmethod
50    def from_dict(cls, data: Dict[str, str]) -> 'Hash':
51        """Create Hash from dictionary."""
52        return cls(
53            alg=data.get('alg', ''),
54            content=data.get('content', '')
55        )
56
57    def to_dict(self) -> Dict[str, str]:
58        """Convert to dictionary representation."""
59        return {
60            "alg": self.alg,
61            "content": self.content
62        }
63
64
65class RelationshipType(Enum):
66    """
67    Enumeration of standard SBOM relationship types between components.
68
69    Values:
70        CONTAINS: The source contains the target
71        CONTAINED_BY: The source is contained by the target
72        DEPENDS_ON: The source depends on the target
73        DEPENDENCY_OF: The source is a dependency of the target
74        GENERATES: The source generates the target
75        GENERATED_FROM: The source was generated from the target
76        VARIANT_OF: The source is a variant of the target
77        COPY_OF: The source is a copy of the target
78        DYNAMIC_LINK: The source dynamically links to the target
79        STATIC_LINK: The source statically links to the target
80        OTHER: Other relationship type
81    """
82    CONTAINS = "CONTAINS"
83    CONTAINED_BY = "CONTAINED_BY"
84    DEPENDS_ON = "DEPENDS_ON"
85    DEPENDENCY_OF = "DEPENDENCY_OF"
86    GENERATES = "GENERATES"
87    GENERATED_FROM = "GENERATED_FROM"
88    VARIANT_OF = "VARIANT_OF"
89    COPY_OF = "COPY_OF"
90    DYNAMIC_LINK = "DYNAMIC_LINK"
91    STATIC_LINK = "STATIC_LINK"
92    OTHER = "OTHER"
93
94    @classmethod
95    def from_str(cls, value: str) -> 'RelationshipType':
96        """Create from string value."""
97        try:
98            return cls(value)
99        except ValueError:
100            return cls.OTHER
101
102
103# ---------------------------
104# Document Creation Information
105# ---------------------------
106@dataclass(frozen=True)
107class Document:
108    """
109    Represents SBOM document metadata and creation information.
110
111    Required fields:
112        serial_number: Unique document identifier (typically UUID)
113        version: Document version
114        bom_format: BOM format specification
115        spec_version: Specification version
116        data_license: License for the document metadata
117        timestamp: Creation timestamp (ISO 8601 format)
118        authors: List of document authors
119
120    Optional fields:
121        doc_id: SPDX document identifier
122        name: Document name
123        document_namespace: Unique document namespace URI
124        license_list_version: SPDX license list version
125        lifecycles: List of lifecycle phases
126        properties: Custom key-value properties
127        tools: List of tools used to generate the SBOM
128        doc_comments: Free-form comments
129    """
130    # Required fields
131    serial_number: str
132    version: str
133    bom_format: str
134    spec_version: str
135    data_license: str
136    timestamp: str
137    authors: List[str]
138
139    # SPDX-specific fields
140    doc_id: Optional[str]
141    name: Optional[str]
142    document_namespace: Optional[str]
143    license_list_version: Optional[str]
144
145    # Optional fields
146    lifecycles: List[str] = field(default_factory=list)
147    properties: Dict[str, str] = field(default_factory=dict)
148    tools: List[Dict[str, str]] = field(default_factory=list)
149    doc_comments: Optional[str] = None
150
151    @classmethod
152    def from_dict(cls, data: Dict[str, Any]) -> 'Document':
153        """Create Document from dictionary."""
154        return cls(
155            serial_number=data.get('serialNumber', ''),
156            version=data.get('version', ''),
157            bom_format=data.get('bomFormat', ''),
158            spec_version=data.get('specVersion', ''),
159            data_license=data.get('dataLicense', ''),
160            timestamp=data.get('timestamp', ''),
161            authors=data.get('authors', []),
162            doc_id=data.get('docId'),
163            name=data.get('name'),
164            document_namespace=data.get('documentNamespace'),
165            license_list_version=data.get('licenseListVersion'),
166            lifecycles=data.get('lifecycles', []),
167            properties=data.get('properties', {}),
168            tools=data.get('tools', []),
169            doc_comments=data.get('docComments')
170        )
171
172    def to_dict(self) -> Dict[str, Any]:
173        """
174        Convert to ordered dictionary following standard field order.
175
176        Returns:
177            OrderedDict: Document data with empty values removed
178        """
179        data = OrderedDict([
180            ("serialNumber", self.serial_number),
181            ("docId", self.doc_id),
182            ("name", self.name),
183            ("documentNamespace", self.document_namespace),
184            ("version", self.version),
185            ("bomFormat", self.bom_format),
186            ("specVersion", self.spec_version),
187            ("dataLicense", self.data_license),
188            ("licenseListVersion", self.license_list_version),
189            ("timestamp", self.timestamp),
190            ("authors", self.authors),
191            ("lifecycles", self.lifecycles),
192            ("properties", self.properties),
193            ("tools", self.tools),
194            ("docComments", self.doc_comments)
195        ])
196        return remove_empty(data)
197
198
199# ---------------------------
200# Package Information
201# ---------------------------
202@dataclass(frozen=True)
203class Package:
204    """
205    Represents a software package in the SBOM.
206
207    Required fields:
208        type: Package type (e.g. "library", "application")
209        supplier: Package supplier/originator
210        group: Package group/namespace
211        name: Package name
212        version: Package version
213        purl: Package URL identifier
214        license_concluded: Concluded license
215        license_declared: Declared license
216        bom_ref: Unique package reference
217        comp_platform: Target platform
218
219    Optional fields:
220        com_copyright: Copyright notice
221        download_location: Package download URL
222        hashes: List of cryptographic hashes
223    """
224    type: str
225    supplier: str
226    group: str
227    name: str
228    version: str
229    purl: str
230    license_concluded: str
231    license_declared: str
232    bom_ref: str
233    comp_platform: str
234
235    # Optional fields
236    com_copyright: Optional[str] = None
237    download_location: Optional[str] = None
238    hashes: List[Hash] = field(default_factory=list)
239
240    @classmethod
241    def from_dict(cls, data: Dict[str, Any]) -> 'Package':
242        """Create Package from dictionary."""
243        return cls(
244            type=data.get('type', ''),
245            supplier=data.get('supplier', ''),
246            group=data.get('group', ''),
247            name=data.get('name', ''),
248            version=data.get('version', ''),
249            purl=data.get('purl', ''),
250            license_concluded=data.get('licenseConcluded', ''),
251            license_declared=data.get('licenseDeclared', ''),
252            bom_ref=data.get('bom-ref', ''),
253            comp_platform=data.get('compPlatform', ''),
254            com_copyright=data.get('comCopyright'),
255            download_location=data.get('downloadLocation'),
256            hashes=[Hash.from_dict(h) for h in data.get('hashes', [])]
257        )
258
259    def to_dict(self) -> Dict[str, Any]:
260        """
261        Convert to ordered dictionary following standard field order.
262
263        Returns:
264            OrderedDict: Package data with empty values removed
265        """
266        data = OrderedDict([
267            ("type", self.type),
268            ("supplier", self.supplier),
269            ("group", self.group),
270            ("name", self.name),
271            ("version", self.version),
272            ("purl", self.purl),
273            ("licenseConcluded", self.license_concluded),
274            ("licenseDeclared", self.license_declared),
275            ("comCopyright", self.com_copyright),
276            ("bom-ref", self.bom_ref),
277            ("downloadLocation", self.download_location),
278            ("hashes", [h.to_dict() for h in self.hashes]),
279            ("compPlatform", self.comp_platform)
280        ])
281        return remove_empty(data)
282
283
284# ---------------------------
285# File Information
286# ---------------------------
287@dataclass(frozen=True)
288class File:
289    """
290    Represents a file in the SBOM.
291
292    Required fields:
293        file_name: File name with path
294        file_id: Unique file identifier
295        checksums: List of cryptographic hashes
296        license_concluded: Concluded license
297
298    Optional fields:
299        file_types: List of file types
300        file_author: File author
301        license_info_in_files: Licenses found in file
302        copyright_text: Copyright notice
303    """
304    file_name: str
305    file_id: str
306    checksums: List['Hash']
307    license_concluded: str
308    file_types: List[str] = field(default_factory=list)
309    file_author: Optional[str] = None
310    license_info_in_files: List[str] = field(default_factory=list)
311    copyright_text: Optional[str] = None
312
313    @classmethod
314    def from_dict(cls, data: Dict[str, Any]) -> 'File':
315        """Create File from dictionary."""
316        return cls(
317            file_name=data.get('fileName', ''),
318            file_id=data.get('fileId', ''),
319            checksums=[Hash.from_dict(h) for h in data.get('checksums', [])],
320            license_concluded=data.get('licenseConcluded', ''),
321            file_types=data.get('fileTypes', []),
322            file_author=data.get('fileAuthor'),
323            license_info_in_files=data.get('licenseInfoInFiles', []),
324            copyright_text=data.get('copyrightText')
325        )
326
327    def to_dict(self) -> Dict[str, Any]:
328        """
329        Convert to ordered dictionary following standard field order.
330
331        Returns:
332            OrderedDict: File data with empty values removed
333        """
334        data = OrderedDict([
335            ("fileName", self.file_name),
336            ("fileAuthor", self.file_author),
337            ("fileId", self.file_id),
338            ("fileTypes", self.file_types),
339            ("checksums", [h.to_dict() for h in self.checksums]),
340            ("licenseConcluded", self.license_concluded),
341            ("licenseInfoInFiles", self.license_info_in_files),
342            ("copyrightText", self.copyright_text)
343        ])
344        return remove_empty(data)
345
346
347# ---------------------------
348# Relationship Information
349# ---------------------------
350@dataclass(frozen=True)
351class Relationship:
352    """
353    Represents a relationship between SBOM components.
354
355    Attributes:
356        bom_ref: Source component reference
357        depends_on: List of target component references
358        relationship_type: Type of relationship
359    """
360    bom_ref: str
361    depends_on: List[str]
362    relationship_type: RelationshipType
363
364    @classmethod
365    def from_dict(cls, data: Dict[str, Any]) -> 'Relationship':
366        """Create Relationship from dictionary."""
367        return cls(
368            bom_ref=data.get('bom-ref', ''),
369            depends_on=data.get('dependsOn', []),
370            relationship_type=RelationshipType.from_str(data.get('relationshipType', 'OTHER'))
371        )
372
373    def to_dict(self) -> Dict[str, Any]:
374        """
375        Convert to ordered dictionary following standard field order.
376
377        Returns:
378            OrderedDict: Relationship data with empty values removed
379        """
380        data = OrderedDict([
381            ("bom-ref", self.bom_ref),
382            ("dependsOn", self.depends_on),
383            ("relationshipType", self.relationship_type.value)
384        ])
385        return remove_empty(data)
386
387
388# ---------------------------
389# SBOM Metadata
390# ---------------------------
391@dataclass(frozen=True)
392class SBOMMetaData:
393    """
394    Complete SBOM document containing all metadata components.
395
396    Attributes:
397        document: Document creation information
398        packages: List of software packages
399        files: List of files
400        relationships: List of component relationships
401    """
402    document: Document
403    packages: List[Package] = field(default_factory=list)
404    files: List[File] = field(default_factory=list)
405    relationships: List[Relationship] = field(default_factory=list)
406
407    @classmethod
408    def from_dict(cls, data: Dict[str, Any]) -> 'SBOMMetaData':
409        """Create SBOMMetaData from dictionary."""
410        return cls(
411            document=Document.from_dict(data.get('document', {})),
412            packages=[Package.from_dict(p) for p in data.get('packages', [])],
413            files=[File.from_dict(f) for f in data.get('files', [])],
414            relationships=[Relationship.from_dict(r) for r in data.get('relationships', [])]
415        )
416
417    def to_dict(self) -> Dict[str, Any]:
418        """
419        Convert complete SBOM to ordered dictionary.
420
421        Returns:
422            OrderedDict: SBOM data with empty values removed
423        """
424        data = OrderedDict([
425            ("document", self.document.to_dict()),
426            ("packages", [pkg.to_dict() for pkg in self.packages]),
427            ("files", [file.to_dict() for file in self.files]),
428            ("relationships", [rel.to_dict() for rel in self.relationships])
429        ])
430        return remove_empty(data)
431