1+ """
2+ SPDX-License-Identifier: MIT
3+
4+ Copyright (c) 2021, SCANOSS
5+
6+ Permission is hereby granted, free of charge, to any person obtaining a copy
7+ of this software and associated documentation files (the "Software"), to deal
8+ in the Software without restriction, including without limitation the rights
9+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+ copies of the Software, and to permit persons to whom the Software is
11+ furnished to do so, subject to the following conditions:
12+
13+ The above copyright notice and this permission notice shall be included in
14+ all copies or substantial portions of the Software.
15+
16+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+ THE SOFTWARE.
23+ """
24+ import json
25+ import os .path
26+ import sys
27+ import hashlib
28+ import time
29+ import datetime
30+
31+
32+ class SpdxLite :
33+ """
34+ SPDX Lite management class
35+ Handle all interaction with SPDX Lite formatting
36+ """
37+ def __init__ (self , debug : bool = False , output_file : str = None ):
38+ """
39+ Initialise the SpdxLite class
40+ """
41+ self .output_file = output_file
42+ self .debug = debug
43+
44+ @staticmethod
45+ def print_stderr (* args , ** kwargs ):
46+ """
47+ Print the given message to STDERR
48+ """
49+ print (* args , file = sys .stderr , ** kwargs )
50+
51+ def print_msg (self , * args , ** kwargs ):
52+ """
53+ Print message if quite mode is not enabled
54+ """
55+ if not self .quiet :
56+ self .print_stderr (* args , ** kwargs )
57+
58+ def print_debug (self , * args , ** kwargs ):
59+ """
60+ Print debug message if enabled
61+ """
62+ if self .debug :
63+ self .print_stderr (* args , ** kwargs )
64+
65+ def parse (self , data : json ):
66+ """
67+ Parse the given input (raw/plain) JSON string and return a summary
68+
69+ :param data: json - JSON object
70+ :return: summary dictionary
71+ """
72+ if not data :
73+ self .print_stderr ('ERROR: No JSON data provided to parse.' )
74+ return None
75+ self .print_debug (f'Processing raw results into summary format...' )
76+ summary = {}
77+ for f in data :
78+ file_details = data .get (f )
79+ # print(f'File: {f}: {file_details}')
80+ for d in file_details :
81+ id_details = d .get ("id" )
82+ if not id_details or id_details == 'none' : # Ignore files with no ids
83+ continue
84+ purl = None
85+ purls = d .get ('purl' )
86+ if not purls :
87+ self .print_stderr (f'Purl block missing for { f } : { file_details } ' )
88+ continue
89+ for p in purls :
90+ self .print_debug (f'Purl: { p } ' )
91+ purl = p
92+ break
93+ if not purl :
94+ self .print_stderr (f'Warning: No PURL found for { f } : { file_details } ' )
95+ continue
96+ if summary .get (purl ):
97+ self .print_debug (f'Component { purl } already stored: { summary .get (purl )} ' )
98+ continue
99+ fd = {}
100+ for field in ['id' , 'vendor' , 'component' , 'version' , 'latest' , 'url' ]:
101+ fd [field ] = d .get (field )
102+ licenses = d .get ('licenses' )
103+ fdl = []
104+ dc = []
105+ for lic in licenses :
106+ name = lic .get ("name" )
107+ if not name in dc : # Only save the license name once
108+ fdl .append ({'id' :name })
109+ dc .append (name )
110+ fd ['licenses' ] = fdl
111+ summary [p ] = fd
112+ return summary
113+
114+ def produce_from_file (self , json_file : str , output_file : str = None ) -> bool :
115+ """
116+ Parse plain/raw input JSON file and produce SPDX Lite output
117+ :param json_file:
118+ :param output_file:
119+ :return: True if successful, False otherwise
120+ """
121+ if not json_file :
122+ self .print_stderr ('ERROR: No JSON file provided to parse.' )
123+ return False
124+ if not os .path .isfile (json_file ):
125+ self .print_stderr (f'ERROR: JSON file does not exist or is not a file: { json_file } ' )
126+ return False
127+ success = True
128+ with open (json_file , 'r' ) as f :
129+ success = self .produce_from_str (f .read (), output_file )
130+ return success
131+
132+ def produce_from_json (self , data : json , output_file : str = None ) -> bool :
133+ """
134+ Produce the SPDX Lite output from the input JSON object
135+ :param data: JSON object
136+ :param output_file: Output file (optional)
137+ :return: True if successful, False otherwise
138+ """
139+ raw_data = self .parse (data )
140+ if not raw_data :
141+ self .print_stderr ('ERROR: No SPDX data returned for the JSON string provided.' )
142+ return False
143+ now = datetime .datetime .utcnow ()
144+ md5hex = hashlib .md5 (f'{ time .time ()} ' .encode ('utf-8' )).hexdigest ()
145+ data = {}
146+ data ['spdxVersion' ] = 'SPDX-2.2'
147+ data ['dataLicense' ] = 'CC0-1.0'
148+ data ['SPDXIdentifier' ] = f'SCANOSS-SPDX-{ md5hex } '
149+ data ['DocumentName' ] = 'SCANOSS-SBOM'
150+ data ['creator' ] = 'Tool: SCANOSS-PY'
151+ data ['created' ] = now .strftime ('%Y-%m-%dT%H:%M:%S' ) + now .strftime ('.%f' )[:4 ] + 'Z'
152+ data ['Packages' ] = []
153+ for purl in raw_data :
154+ comp = raw_data .get (purl )
155+ lic = []
156+ licenses = comp .get ('licenses' )
157+ if licenses :
158+ for l in licenses :
159+ lic .append (l .get ('id' ))
160+ data ['Packages' ].append ({
161+ 'PackageName' : comp .get ('component' ),
162+ 'PackageSPDXID' : purl ,
163+ 'PackageVersion' : comp .get ('version' ),
164+ 'PackageDownloadLocation' : comp .get ('url' ),
165+ 'DeclaredLicense' : f'({ " AND " .join (lic )} )' if len (lic ) > 0 else ''
166+ })
167+ # End for loop
168+ file = sys .stdout
169+ if not output_file and self .output_file :
170+ output_file = self .output_file
171+ if output_file :
172+ file = open (output_file , 'w' )
173+ print (json .dumps (data , indent = 2 ), file = file )
174+ if output_file :
175+ file .close ()
176+ return True
177+
178+ def produce_from_str (self , json_str : str , output_file : str = None ) -> bool :
179+ """
180+ Produce SPDX Lite output from input JSON string
181+ :param json_str: input JSON string
182+ :param output_file: Output file (optional)
183+ :return: True if successful, False otherwise
184+ """
185+ if not json_str :
186+ self .print_stderr ('ERROR: No JSON string provided to parse.' )
187+ return False
188+ data = None
189+ try :
190+ data = json .loads (json_str )
191+ except Exception as e :
192+ self .print_stderr (f'ERROR: Problem parsing input JSON: { e } ' )
193+ return False
194+ else :
195+ return self .produce_from_json (data , output_file )
196+ return False
197+ #
198+ # End of SpdxLite Class
199+ #
0 commit comments