Skip to content

Commit 1307ca6

Browse files
authored
"Add to generate spdx format result" (#113)
Signed-off-by: Jiyeong Seok <[email protected]>
1 parent 2632144 commit 1307ca6

File tree

4 files changed

+297
-1
lines changed

4 files changed

+297
-1
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ coloredlogs
99
python3-wget
1010
beautifulsoup4
1111
jsonmerge
12+
spdx-tools

src/fosslight_util/output_format.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from fosslight_util.write_opossum import write_opossum
88
from fosslight_util.write_yaml import write_yaml
99

10-
SUPPORT_FORMAT = {'excel': '.xlsx', 'csv': '.csv', 'opossum': '.json', 'yaml': '.yaml'}
10+
SUPPORT_FORMAT = {'excel': '.xlsx', 'csv': '.csv', 'opossum': '.json', 'yaml': '.yaml',
11+
'spdx-yaml': '.yaml', 'spdx-json': '.json', 'spdx-xml': '.xml',
12+
'spdx-tag': '.tag'}
1113

1214

1315
def check_output_format(output='', format='', customized_format={}):

src/fosslight_util/resources/frequent_license_nick_list.json

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
"gpl2.0": "GPL-2.0-only",
44
"gpl 2.0": "GPL-2.0-only",
55
"gpl-2.0-only": "GPL-2.0-only",
6+
"gplv2": "GPL-2.0-only",
7+
"gpl v2": "GPL-2.0-only",
8+
"gpl version 2": "GPL-2.0-only",
9+
"gnu general public license v2 (gplv2)": "GPL-2.0-only",
10+
"gnu general public license v2.0 only": "GPL-2.0-only",
11+
"gpl-2+": "GPL-2.0-or-later",
12+
"gpl-2.0+": "GPL-2.0-or-later",
13+
"gplv2+": "GPL-2.0-or-later",
14+
"gplv2.0+": "GPL-2.0-or-later",
15+
"gnu general public license (gpl) version 2 or any later version": "GPL-2.0-or-later",
16+
"gnu general public license v2 or later (gplv2+)": "GPL-2.0-or-later",
17+
"gnu general public license v2.0 or later": "GPL-2.0-or-later",
618
"gpl-3.0": "GPL-3.0-only",
719
"gpl3.0": "GPL-3.0-only",
820
"gpl 3.0": "GPL-3.0-only",
@@ -14,24 +26,99 @@
1426
"lgpl2.0": "LGPL-2.1-only",
1527
"lgpl2.1": "LGPL-2.1-only",
1628
"lgpl-2.1-only": "LGPL-2.1-only",
29+
"lgpl-2.0-only": "LGPL-2.1-only",
30+
"lgpl v2.1": "LGPL-2.1-only",
31+
"lgpl version 2.1": "LGPL-2.1-only",
32+
"lgpl2": "LGPL-2.1-only",
33+
"lgplv2": "LGPL-2.1-only",
34+
"lgplv2.1": "LGPL-2.1-only",
35+
"gnu lesser general public license (lgpl) version 2.1": "LGPL-2.1-only",
36+
"gnu lesser general public license version 2.1": "LGPL-2.1-only",
37+
"lgpl v2.1+": "LGPL-2.1-or-later",
38+
"lgplv2+": "LGPL-2.1-or-later",
39+
"lgplv2.0+": "LGPL-2.1-or-later",
40+
"lgpl-2.1+": "LGPL-2.1-or-later",
41+
"lgplv2.1+": "LGPL-2.1-or-later",
42+
"gnu library general public license v2 or later": "LGPL-2.1-or-later",
43+
"gnu library general public license v2.1 or later": "LGPL-2.1-or-later",
44+
"gnu lesser general public license v2.1 or later": "LGPL-2.1-or-later",
1745
"apache-2.0": "Apache-2.0",
1846
"apache2.0": "Apache-2.0",
1947
"apache 2.0": "Apache-2.0",
48+
"apache 2": "Apache-2.0",
49+
"apache2": "Apache-2.0",
50+
"asf 2.0": "Apache-2.0",
51+
"apache 2.0 license": "Apache-2.0",
52+
"apache license (v2.0)": "Apache-2.0",
53+
"apache license v2": "Apache-2.0",
54+
"apache license v2.0": "Apache-2.0",
55+
"apache v2": "Apache-2.0",
56+
"apache version 2.0": "Apache-2.0",
57+
"the apache software license version 2.0": "Apache-2.0",
58+
"apache license version 2.0": "Apache-2.0",
59+
"apache license 2.0": "Apache-2.0",
60+
"apache-2.0 license": "Apache-2.0",
61+
"http://www.apache.org/licenses/license-2.0.txt": "Apache-2.0",
62+
"https://www.apache.org/licenses/license-2.0.txt": "Apache-2.0",
2063
"bsd-3-clause": "BSD-3-clause",
2164
"bsd 3 clause": "BSD-3-clause",
2265
"bsd-3 clause": "BSD-3-clause",
2366
"bsd 3-clause": "BSD-3-clause",
67+
"new bsd license": "BSD-3-clause",
68+
"revised bsd": "BSD-3-clause",
69+
"modified bsd": "BSD-3-clause",
70+
"the 3-clause bsd license": "BSD-3-clause",
71+
"the bsd 3-clause license": "BSD-3-clause",
72+
"the new bsd license": "BSD-3-clause",
73+
"3-clause bsd license": "BSD-3-clause",
74+
"bsd 3-clause license": "BSD-3-clause",
75+
"bsd 3-clause new license": "BSD-3-clause",
76+
"bsd Licence 3": "BSD-3-clause",
77+
"bsd License 3": "BSD-3-clause",
78+
"bsd-3-clause license": "BSD-3-clause",
79+
"bsd3": "BSD-3-clause",
80+
"bsd3_clause": "BSD-3-clause",
81+
"eclipse distribution license (new bsd license)": "BSD-3-clause",
82+
"eclipse distribution license - 1.0": "BSD-3-clause",
83+
"eclipse distribution license - v 1.0": "BSD-3-clause",
84+
"eclipse distribution license v. 1.0": "BSD-3-clause",
85+
"edl 1.0": "BSD-3-clause",
86+
"edl-1.0": "BSD-3-clause",
87+
"bsd 2-clause \"simplified\" license": "BSD-2-Clause",
88+
"bsd 2 clause": "BSD-2-Clause",
89+
"bsd 2-clause": "BSD-2-Clause",
90+
"bsd 2-clause license": "BSD-2-Clause",
91+
"bsd2": "BSD-2-Clause",
92+
"bsd2_clause": "BSD-2-Clause",
93+
"simplified bsd license": "BSD-2-Clause",
2494
"agpl 3.0": "AGPL-3.0",
2595
"apgl-3.0": "APGL-3.0",
2696
"apgl3.0": "APGL-3.0",
2797
"agplv3": "AGPL-3.0",
2898
"mit": "MIT",
99+
"mit license": "MIT",
100+
"the mit license": "MIT",
101+
"the mit license (mit)": "MIT",
29102
"epl-1.0": "EPL-1.0",
30103
"epl 1.0": "EPL-1.0",
104+
"eclipse public license - v 1.0": "EPL-1.0",
31105
"epl1.0": "EPL-1.0",
32106
"epl-2.0": "EPL-2.0",
33107
"epl 2.0": "EPL-2.0",
34108
"epl2.0": "EPL-2.0",
109+
"mpl 1.1": "MPL-1.1",
110+
"mplv1.1": "MPL-1.1",
111+
"mozilla public license 1.1": "MPL-1.1",
112+
"mozilla public license version 1.1": "MPL-1.1",
113+
"mpl 2.0": "MPL-2.0",
114+
"mpl2": "MPL-2.0",
115+
"mpl2.0": "MPL-2.0",
116+
"mplv2": "MPL-2.0",
117+
"mpl v2": "MPL-2.0",
118+
"mozilla public license 2.0": "MPL-2.0",
119+
"mozilla public license version 2.0": "MPL-2.0",
120+
"mozilla public license 2.0 (mpl 2.0)": "MPL-2.0",
121+
"json License": "JSON",
35122
"lge": "LicenseRef-LGE_Proprietary_License",
36123
"lge license": "LicenseRef-LGE_Proprietary_License",
37124
"lge proprietary": "LicenseRef-LGE_Proprietary_License",

src/fosslight_util/write_spdx.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (c) 2022 LG Electronics Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import os
7+
import uuid
8+
import logging
9+
import re
10+
from pathlib import Path
11+
from spdx.creationinfo import Tool
12+
from spdx.document import Document
13+
from spdx.package import Package
14+
from spdx.relationship import Relationship
15+
from spdx.license import License
16+
from spdx.utils import SPDXNone
17+
from spdx.utils import NoAssert
18+
from spdx.version import Version
19+
from spdx.writers.write_anything import write_file
20+
from fosslight_util.spdx_licenses import get_spdx_licenses_json, get_license_from_nick
21+
import fosslight_util.constant as constant
22+
import traceback
23+
24+
logger = logging.getLogger(constant.LOGGER_NAME)
25+
26+
27+
def get_license_list_version():
28+
version = 'N/A'
29+
success, error_msg, licenses = get_spdx_licenses_json()
30+
if success:
31+
version = licenses['licenseListVersion']
32+
else:
33+
logger.info(f'Fail to get spdx license list version:{error_msg}')
34+
return version
35+
36+
37+
def write_spdx(output_file_without_ext, output_extension, sheet_list,
38+
scanner_name, scanner_version, spdx_version=(2, 3)):
39+
success = True
40+
error_msg = ''
41+
if sheet_list:
42+
doc = Document(version=Version(*spdx_version),
43+
data_license=License.from_identifier('CC0-1.0'),
44+
namespace=f'http://spdx.org/spdxdocs/{scanner_name.lower()}-{uuid.uuid4()}',
45+
name=f'SPDX Document by {scanner_name.upper()}',
46+
spdx_id='SPDXRef-DOCUMENT')
47+
48+
doc.creation_info.set_created_now()
49+
doc.creation_info.add_creator(Tool(f'{scanner_name.upper()} {scanner_version}'))
50+
doc.creation_info.license_list_version = Version(*tuple(get_license_list_version().split('.')))
51+
52+
relation_tree = {}
53+
spdx_id_packages = []
54+
55+
output_dir = os.path.dirname(output_file_without_ext)
56+
Path(output_dir).mkdir(parents=True, exist_ok=True)
57+
try:
58+
package_id = 0
59+
root_package = False
60+
for sheet_name, sheet_contents in sheet_list.items():
61+
if sheet_name not in constant.supported_sheet_and_scanner.keys():
62+
continue
63+
scanner = constant.supported_sheet_and_scanner.get(sheet_name)
64+
for oss_item in sheet_contents:
65+
if len(oss_item) < 9:
66+
logger.warning(f"sheet list is too short ({len(oss_item)}): {oss_item}")
67+
continue
68+
package_id += 1
69+
package = Package(spdx_id=f'SPDXRef-{package_id}')
70+
71+
if oss_item[1] != '':
72+
package.name = oss_item[1] # required
73+
else:
74+
package.name = SPDXNone()
75+
76+
if oss_item[2] != '':
77+
package.version = oss_item[2] # no required
78+
79+
if oss_item[4] != '':
80+
package.download_location = oss_item[4] # required
81+
else:
82+
package.download_location = SPDXNone()
83+
84+
if scanner == constant.FL_DEPENDENCY:
85+
package.files_analyzed = False # If omitted, the default value of true is assumed.
86+
else:
87+
package.files_analyzed = True
88+
89+
if oss_item[5] != '':
90+
package.homepage = oss_item[5] # no required
91+
92+
if oss_item[6] != '':
93+
package.cr_text = oss_item[3] # required
94+
else:
95+
package.cr_text = SPDXNone()
96+
if oss_item[3] != '':
97+
lic_list = [check_input_license_format(lic.strip()) for lic in oss_item[3].split(',')]
98+
package.license_declared = ','.join(lic_list)
99+
else:
100+
package.license_declared = NoAssert() # required
101+
102+
doc.add_package(package)
103+
104+
if scanner == constant.FL_DEPENDENCY:
105+
spdx_id_packages.append([package.name, package.spdx_id])
106+
comment = oss_item[8]
107+
relation_tree[package.name] = {}
108+
relation_tree[package.name]['id'] = package.spdx_id
109+
relation_tree[package.name]['dep'] = []
110+
111+
if 'root package' in comment.split(','):
112+
root_package = True
113+
relationship = Relationship(f"{doc.spdx_id} DESCRIBES {package.spdx_id}")
114+
doc.add_relationship(relationship)
115+
if len(oss_item) > 9:
116+
deps = oss_item[9]
117+
relation_tree[package.name]['dep'].extend([di.strip().split('(')[0] for di in deps.split(',')])
118+
if scanner == constant.FL_DEPENDENCY and len(relation_tree) > 0:
119+
for pkg in relation_tree:
120+
if len(relation_tree[pkg]['dep']) > 0:
121+
pkg_spdx_id = relation_tree[pkg]['id']
122+
if len(relation_tree[pkg]['dep']) > 0:
123+
for pname in relation_tree[pkg]['dep']:
124+
ans = next(filter(lambda x: x[0] == pname, spdx_id_packages), None)
125+
if ans is None:
126+
continue
127+
rel_pkg_spdx_id = ans[1]
128+
relationship = Relationship(f'{pkg_spdx_id} DEPENDS_ON {rel_pkg_spdx_id}')
129+
doc.add_relationship(relationship)
130+
if not root_package:
131+
root_package = Package(spdx_id='SPDXRef-ROOT-PACKAGE')
132+
root_package.name = 'root package'
133+
root_package.download_location = NoAssert()
134+
root_package.files_analyzed = False
135+
root_package.cr_text = SPDXNone()
136+
root_package.license_declared = NoAssert()
137+
doc.add_package(root_package)
138+
relationship = Relationship(f"{doc.spdx_id} DESCRIBES {root_package.spdx_id}")
139+
doc.add_relationship(relationship)
140+
except Exception as e:
141+
success = False
142+
error_msg = f'Failed to create spdx document object:{e}, {traceback.format_exc()}'
143+
else:
144+
success = False
145+
error_msg = 'No item to write in output file.'
146+
147+
result_file = ''
148+
if success:
149+
result_file = output_file_without_ext + output_extension
150+
try:
151+
write_file(doc, result_file, validate=False, encoding='utf-8')
152+
except Exception as e:
153+
success = False
154+
error_msg = f'Failed to write spdx document: {e}'
155+
if os.path.exists(result_file):
156+
os.remove(result_file)
157+
158+
return success, error_msg, result_file
159+
160+
161+
def convert_to_spdx_style(input_string):
162+
input_string = re.sub(r'[^\w\s\.\-]', '', input_string)
163+
input_string = re.sub(r'[\s\_]', '-', input_string)
164+
input_converted = f"LicenseRef-{input_string}"
165+
return input_converted
166+
167+
168+
def check_input_license_format(input_license):
169+
spdx_licenses = get_spdx_licensename()
170+
for spdx in spdx_licenses:
171+
if input_license.casefold() == spdx.casefold():
172+
return spdx
173+
174+
if input_license.startswith('LicenseRef-'):
175+
return input_license
176+
177+
licensesfromJson = get_license_from_nick()
178+
if licensesfromJson == "":
179+
logger.warning(" Error - Return Value to get license from Json is none")
180+
181+
try:
182+
converted_license = licensesfromJson.get(input_license.casefold())
183+
if converted_license is None:
184+
converted_license = convert_to_spdx_style(input_license)
185+
except Exception as ex:
186+
logger.warning(f"Error - Get frequetly used license : {ex}")
187+
188+
return converted_license
189+
190+
191+
def get_spdx_licensename():
192+
spdx_licenses = []
193+
try:
194+
success, error_msg, licenses = get_spdx_licenses_json()
195+
if success is False:
196+
logger.warning(f"Error to get SPDX Licesens : {error_msg}")
197+
198+
licenseInfo = licenses.get("licenses")
199+
for info in licenseInfo:
200+
shortID = info.get("licenseId")
201+
isDeprecated = info.get("isDeprecatedLicenseId")
202+
if isDeprecated is False:
203+
spdx_licenses.append(shortID)
204+
except Exception as ex:
205+
logger.warning(f"Error access to get_spdx_licenses_json : {ex}")
206+
return spdx_licenses

0 commit comments

Comments
 (0)