1+ #!/usr/bin/env python
2+ # -*- coding: utf-8 -*-
3+ # Copyright (c) 2024 LG Electronics Inc.
4+ # Copyright (c) OWASP Foundation.
5+ # SPDX-License-Identifier: Apache-2.0
6+
7+ import os
8+ import sys
9+ import logging
10+ import re
11+ import json
12+ from pathlib import Path
13+ from datetime import datetime
14+ from fosslight_util .spdx_licenses import get_spdx_licenses_json , get_license_from_nick
15+ from fosslight_util .constant import (LOGGER_NAME , FOSSLIGHT_DEPENDENCY , FOSSLIGHT_SCANNER ,
16+ FOSSLIGHT_BINARY , FOSSLIGHT_SOURCE )
17+ from fosslight_util .oss_item import CHECKSUM_NULL , get_checksum_sha1
18+ from packageurl import PackageURL
19+ import traceback
20+ try :
21+ from cyclonedx .builder .this import this_component as cdx_lib_component
22+ from cyclonedx .exception import MissingOptionalDependencyException
23+ from cyclonedx .factory .license import LicenseFactory
24+ from cyclonedx .model import XsUri , ExternalReferenceType
25+ from cyclonedx .model .bom import Bom
26+ from cyclonedx .model .component import Component , ComponentType , HashAlgorithm , HashType , ExternalReference
27+ from cyclonedx .model .contact import OrganizationalEntity
28+ from cyclonedx .output import make_outputter , BaseOutput
29+ from cyclonedx .output .json import JsonV1Dot6
30+ from cyclonedx .schema import OutputFormat , SchemaVersion
31+ from cyclonedx .validation import make_schemabased_validator
32+ from cyclonedx .validation .json import JsonStrictValidator
33+ from cyclonedx .output .json import Json as JsonOutputter
34+ from cyclonedx .output .xml import Xml as XmlOutputter
35+ from cyclonedx .validation .xml import XmlValidator
36+ except Exception :
37+ logger .info ('No import cyclonedx-python-lib' )
38+ logger = logging .getLogger (LOGGER_NAME )
39+
40+
41+ def write_cyclonedx (output_file_without_ext , output_extension , scan_item ):
42+ success = True
43+ error_msg = ''
44+
45+ bom = Bom ()
46+ if scan_item :
47+ try :
48+ cover_name = scan_item .cover .get_print_json ()["Tool information" ].split ('(' ).pop (0 ).strip ()
49+ match = re .search (r"(.+) v([0-9.]+)" , cover_name )
50+ if match :
51+ scanner_name = match .group (1 )
52+ else :
53+ scanner_name = FOSSLIGHT_SCANNER
54+ except Exception :
55+ cover_name = FOSSLIGHT_SCANNER
56+ scanner_name = FOSSLIGHT_SCANNER
57+
58+ lc_factory = LicenseFactory ()
59+ bom .metadata .tools .components .add (cdx_lib_component ())
60+ bom .metadata .tools .components .add (Component (name = scanner_name .upper (),
61+ type = ComponentType .APPLICATION ))
62+ comp_id = 0
63+ bom .metadata .component = root_component = Component (name = 'Root Component' ,
64+ type = ComponentType .APPLICATION ,
65+ bom_ref = str (comp_id ))
66+ relation_tree = {}
67+ bom_ref_packages = []
68+
69+ output_dir = os .path .dirname (output_file_without_ext )
70+ Path (output_dir ).mkdir (parents = True , exist_ok = True )
71+ try :
72+ root_package = False
73+ for scanner_name , file_items in scan_item .file_items .items ():
74+ for file_item in file_items :
75+ if file_item .exclude :
76+ continue
77+ if scanner_name == FOSSLIGHT_SOURCE :
78+ comp_type = ComponentType .FILE
79+ else :
80+ comp_type = ComponentType .LIBRARY
81+
82+ for oss_item in file_item .oss_items :
83+ if oss_item .name == '' :
84+ if scanner_name == FOSSLIGHT_DEPENDENCY :
85+ continue
86+ else :
87+ comp_name = file_item .source_name_or_path
88+ else :
89+ comp_name = oss_item .name
90+
91+ comp_id += 1
92+ comp = Component (type = comp_type ,
93+ name = comp_name ,
94+ bom_ref = str (comp_id ))
95+
96+ if oss_item .version != '' :
97+ comp .version = oss_item .version
98+ if oss_item .copyright != '' :
99+ comp .copyright = oss_item .copyright
100+ if scanner_name == FOSSLIGHT_DEPENDENCY and file_item .purl :
101+ comp .purl = PackageURL .from_string (file_item .purl )
102+ if scanner_name != FOSSLIGHT_DEPENDENCY :
103+ comp .hashes = [HashType (alg = HashAlgorithm .SHA_1 , content = file_item .checksum )]
104+
105+ if oss_item .download_location != '' :
106+ comp .external_references = [ExternalReference (url = XsUri (oss_item .download_location ),
107+ type = ExternalReferenceType .WEBSITE )]
108+
109+ oss_licenses = []
110+ for ol in oss_item .license :
111+ try :
112+ oss_licenses .append (lc_factory .make_from_string (ol ))
113+ except Exception :
114+ logger .info (f'No spdx license name: { oi } ' )
115+ if oss_licenses :
116+ comp .licenses = oss_licenses
117+
118+ root_package = False
119+ if scanner_name == FOSSLIGHT_DEPENDENCY :
120+ if oss_item .comment :
121+ oss_comment = oss_item .comment .split ('/' )
122+ for oc in oss_comment :
123+ if oc in ['direct' , 'transitive' , 'root package' ]:
124+ if oc == 'direct' :
125+ bom .register_dependency (root_component , [comp ])
126+ elif oc == 'root package' :
127+ root_package = True
128+ root_component .name = comp_name
129+ root_component .type = comp_type
130+ comp_id -= 1
131+ else :
132+ bom .register_dependency (root_component , [comp ])
133+ if len (file_item .depends_on ) > 0 :
134+ purl = file_item .purl
135+ relation_tree [purl ] = []
136+ relation_tree [purl ].extend (file_item .depends_on )
137+
138+ if not root_package :
139+ bom .components .add (comp )
140+
141+ if len (bom .components ) > 0 :
142+ for comp_purl in relation_tree :
143+ comp = bom .get_component_by_purl (PackageURL .from_string (comp_purl ))
144+ if comp :
145+ dep_comp_list = []
146+ for dep_comp_purl in relation_tree [comp_purl ]:
147+ dep_comp = bom .get_component_by_purl (PackageURL .from_string (dep_comp_purl ))
148+ if dep_comp :
149+ dep_comp_list .append (dep_comp )
150+ bom .register_dependency (comp , dep_comp_list )
151+
152+ except Exception as e :
153+ success = False
154+ error_msg = f'Failed to create CycloneDX document object:{ e } , { traceback .format_exc ()} '
155+ else :
156+ success = False
157+ error_msg = 'No item to write in output file.'
158+
159+ result_file = ''
160+ if success :
161+ result_file = output_file_without_ext + output_extension
162+ try :
163+ if output_extension == '.json' :
164+ write_cyclonedx_json (bom , result_file )
165+ elif output_extension == '.xml' :
166+ write_cyclonedx_xml (bom , result_file )
167+ else :
168+ success = False
169+ error_msg = f'Not supported output_extension({ output_extension } )'
170+ except Exception as e :
171+ success = False
172+ error_msg = f'Failed to write CycloneDX document: { e } '
173+ if os .path .exists (result_file ):
174+ os .remove (result_file )
175+
176+ return success , error_msg , result_file
177+
178+
179+ def write_cyclonedx_json (bom , result_file ):
180+ success = True
181+ try :
182+ my_json_outputter : 'JsonOutputter' = JsonV1Dot6 (bom )
183+ my_json_outputter .output_to_file (result_file )
184+ serialized_json = my_json_outputter .output_as_string (indent = 2 )
185+ my_json_validator = JsonStrictValidator (SchemaVersion .V1_6 )
186+ try :
187+ validation_errors = my_json_validator .validate_str (serialized_json )
188+ if validation_errors :
189+ logger .warning (f'JSON invalid, ValidationError: { repr (validation_errors )} ' )
190+ except MissingOptionalDependencyException as error :
191+ logger .debug (f'JSON-validation was skipped due to { error } ' )
192+ except Exception as e :
193+ success = False
194+ return success
195+
196+
197+
198+ def write_cyclonedx_xml (bom , result_file ):
199+ success = True
200+ try :
201+ my_xml_outputter : BaseOutput = make_outputter (bom = bom ,
202+ output_format = OutputFormat .XML ,
203+ schema_version = SchemaVersion .V1_6 )
204+ my_xml_outputter .output_to_file (filename = result_file )
205+ serialized_xml = my_xml_outputter .output_as_string (indent = 2 )
206+ my_xml_validator = XmlValidator (SchemaVersion .V1_6 )
207+ try :
208+ validation_errors = my_xml_validator .validate_str (serialized_xml )
209+ if validation_errors :
210+ logger .warning (f'XML invalid, ValidationError: { repr (validation_errors )} ' )
211+ except MissingOptionalDependencyException as error :
212+ logger .debug (f'XML-validation was skipped due to { error } ' )
213+ except Exception as e :
214+ success = False
215+ return success
0 commit comments