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