Skip to content

Commit 1a72651

Browse files
committed
Added SPDX Lite output option
1 parent 51b8d71 commit 1a72651

File tree

5 files changed

+228
-2
lines changed

5 files changed

+228
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [0.7.3] - 2021-12-11
13+
### Added
14+
- Added support for SPDX Lite report output (--format spdxlite)
15+
1216
## [0.7.2] - 2021-12-10
1317
### Added
1418
- Added option to process all file extensions while scanning (--all-extensions)
@@ -75,3 +79,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7579
[0.7.0]: https://github.com/scanoss/scanoss.py/compare/v0.6.11...v0.7.0
7680
[0.7.1]: https://github.com/scanoss/scanoss.py/compare/v0.7.0...v0.7.1
7781
[0.7.2]: https://github.com/scanoss/scanoss.py/compare/v0.7.1...v0.7.2
82+
[0.7.3]: https://github.com/scanoss/scanoss.py/compare/v0.7.2...v0.7.3

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '0.7.2'
25+
__version__ = '0.7.3'

src/scanoss/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def setup_args() -> None:
6262
p_scan.add_argument('--identify', '-i', type=str, help='Scan and identify components in SBOM file' )
6363
p_scan.add_argument('--ignore', '-n', type=str, help='Ignore components specified in the SBOM file' )
6464
p_scan.add_argument('--output', '-o', type=str, help='Output result file name (optional - default stdout).' )
65-
p_scan.add_argument('--format', '-f', type=str, choices=['plain', 'cyclonedx'],
65+
p_scan.add_argument('--format', '-f', type=str, choices=['plain', 'cyclonedx', 'spdxlite'],
6666
help='Result output format (optional - default: plain)'
6767
)
6868
p_scan.add_argument('--threads', '-T', type=int, default=10,

src/scanoss/scanner.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .scanossapi import ScanossApi
3232
from .winnowing import Winnowing
3333
from .cyclonedx import CycloneDx
34+
from .spdxlite import SpdxLite
3435
from .threadedscanning import ThreadedScanning
3536

3637
FILTERED_DIRS = { # Folders to skip
@@ -388,6 +389,15 @@ def __finish_scan_threaded(self, scan_started: bool, file_count: int) -> bool:
388389
success = cdx.produce_from_json(parsed_json)
389390
else:
390391
success = cdx.produce_from_str(raw_output)
392+
elif self.output_format == 'spdxlite':
393+
spdxlite = SpdxLite(self.debug, self.scan_output)
394+
if parsed_json:
395+
success = spdxlite.produce_from_json(parsed_json)
396+
else:
397+
success = spdxlite.produce_from_str(raw_output)
398+
else:
399+
self.print_stderr(f'ERROR: Unknown output format: {self.output_format}')
400+
success = False
391401
return success
392402

393403

@@ -508,6 +518,12 @@ def scan_wfp_file(self, file: str = None) -> bool:
508518
elif self.output_format == 'cyclonedx':
509519
cdx = CycloneDx(self.debug, self.scan_output)
510520
cdx.produce_from_str(raw_output)
521+
elif self.output_format == 'spdxlite':
522+
spdxlite = SpdxLite(self.debug, self.scan_output)
523+
success = spdxlite.produce_from_str(raw_output)
524+
else:
525+
self.print_stderr(f'ERROR: Unknown output format: {self.output_format}')
526+
success = False
511527

512528
return success
513529

@@ -590,6 +606,12 @@ def scan_wfp(self, wfp: str) -> bool:
590606
elif self.output_format == 'cyclonedx':
591607
cdx = CycloneDx(self.debug, self.scan_output)
592608
cdx.produce_from_str(raw_output)
609+
elif self.output_format == 'spdxlite':
610+
spdxlite = SpdxLite(self.debug, self.scan_output)
611+
success = spdxlite.produce_from_str(raw_output)
612+
else:
613+
self.print_stderr(f'ERROR: Unknown output format: {self.output_format}')
614+
success = False
593615

594616
return success
595617

src/scanoss/spdxlite.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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

Comments
 (0)