scanoss 1.16.0__py3-none-any.whl → 1.17.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scanoss/__init__.py +1 -1
- scanoss/cli.py +95 -14
- scanoss/data/build_date.txt +1 -1
- scanoss/inspection/__init__.py +23 -0
- scanoss/inspection/copyleft.py +156 -0
- scanoss/inspection/policy_check.py +341 -0
- scanoss/inspection/undeclared_component.py +167 -0
- scanoss/inspection/utils/license_utils.py +115 -0
- scanoss/inspection/utils/markdown_utils.py +23 -0
- scanoss/inspection/utils/result_utils.py +79 -0
- scanoss/results.py +2 -2
- scanoss/scanossbase.py +10 -0
- scanoss/spdxlite.py +2 -2
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/METADATA +1 -1
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/RECORD +19 -12
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/WHEEL +1 -1
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/LICENSE +0 -0
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/entry_points.txt +0 -0
- {scanoss-1.16.0.dist-info → scanoss-1.17.1.dist-info}/top_level.txt +0 -0
scanoss/__init__.py
CHANGED
scanoss/cli.py
CHANGED
|
@@ -25,15 +25,13 @@ import argparse
|
|
|
25
25
|
import os
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
import sys
|
|
28
|
-
from array import array
|
|
29
|
-
|
|
30
28
|
import pypac
|
|
31
|
-
|
|
29
|
+
from scanoss.inspection.copyleft import Copyleft
|
|
30
|
+
from scanoss.inspection.undeclared_component import UndeclaredComponent
|
|
32
31
|
from .threadeddependencies import SCOPE
|
|
33
|
-
from .scanner import Scanner
|
|
34
32
|
from .scanoss_settings import ScanossSettings
|
|
35
33
|
from .scancodedeps import ScancodeDeps
|
|
36
|
-
from .scanner import
|
|
34
|
+
from .scanner import Scanner
|
|
37
35
|
from .scantype import ScanType
|
|
38
36
|
from .filecount import FileCount
|
|
39
37
|
from .cyclonedx import CycloneDx
|
|
@@ -171,19 +169,19 @@ def setup_args() -> None:
|
|
|
171
169
|
# Component Sub-command: component crypto
|
|
172
170
|
c_crypto = comp_sub.add_parser('crypto', aliases=['cr'],
|
|
173
171
|
description=f'Show Cryptographic algorithms: {__version__}',
|
|
174
|
-
help='
|
|
172
|
+
help='Retrieve cryptographic algorithms for the given components')
|
|
175
173
|
c_crypto.set_defaults(func=comp_crypto)
|
|
176
174
|
|
|
177
175
|
# Component Sub-command: component vulns
|
|
178
176
|
c_vulns = comp_sub.add_parser('vulns', aliases=['vulnerabilities', 'vu'],
|
|
179
177
|
description=f'Show Vulnerability details: {__version__}',
|
|
180
|
-
help='
|
|
178
|
+
help='Retrieve vulnerabilities for the given components')
|
|
181
179
|
c_vulns.set_defaults(func=comp_vulns)
|
|
182
180
|
|
|
183
181
|
# Component Sub-command: component semgrep
|
|
184
182
|
c_semgrep = comp_sub.add_parser('semgrep', aliases=['sp'],
|
|
185
183
|
description=f'Show Semgrep findings: {__version__}',
|
|
186
|
-
help='
|
|
184
|
+
help='Retrieve semgrep issues/findings for the given components')
|
|
187
185
|
c_semgrep.set_defaults(func=comp_semgrep)
|
|
188
186
|
|
|
189
187
|
# Component Sub-command: component search
|
|
@@ -299,6 +297,31 @@ def setup_args() -> None:
|
|
|
299
297
|
)
|
|
300
298
|
p_results.set_defaults(func=results)
|
|
301
299
|
|
|
300
|
+
|
|
301
|
+
# Sub-command: inspect
|
|
302
|
+
p_inspect = subparsers.add_parser('inspect', aliases=['insp', 'ins'],
|
|
303
|
+
description=f'Inspect results: {__version__}',
|
|
304
|
+
help='Inspect results')
|
|
305
|
+
# Sub-parser: inspect
|
|
306
|
+
p_inspect_sub = p_inspect.add_subparsers(title='Inspect Commands', dest='subparsercmd',
|
|
307
|
+
description='Inspect sub-commands', help='Inspect sub-commands')
|
|
308
|
+
# Inspect Sub-command: inspect copyleft
|
|
309
|
+
p_copyleft = p_inspect_sub.add_parser('copyleft', aliases=['cp'],description="Inspect for copyleft licenses", help='Inspect for copyleft licenses')
|
|
310
|
+
p_copyleft.add_argument('--include', help='List of Copyleft licenses to append to the default list. Provide licenses as a comma-separated list.')
|
|
311
|
+
p_copyleft.add_argument('--exclude', help='List of Copyleft licenses to remove from default list. Provide licenses as a comma-separated list.')
|
|
312
|
+
p_copyleft.add_argument('--explicit', help='Explicit list of Copyleft licenses to consider. Provide licenses as a comma-separated list.s')
|
|
313
|
+
p_copyleft.set_defaults(func=inspect_copyleft)
|
|
314
|
+
|
|
315
|
+
# Inspect Sub-command: inspect undeclared
|
|
316
|
+
p_undeclared = p_inspect_sub.add_parser('undeclared', aliases=['un'],description="Inspect for undeclared components", help='Inspect for undeclared components')
|
|
317
|
+
p_undeclared.set_defaults(func=inspect_undeclared)
|
|
318
|
+
|
|
319
|
+
for p in [p_copyleft, p_undeclared]:
|
|
320
|
+
p.add_argument('-i', '--input', nargs='?', help='Path to results file')
|
|
321
|
+
p.add_argument('-f', '--format',required=False ,choices=['json', 'md'], default='json', help='Output format (default: json)')
|
|
322
|
+
p.add_argument('-o', '--output', type=str, help='Save details into a file')
|
|
323
|
+
p.add_argument('-s', '--status', type=str, help='Save summary data into Markdown file')
|
|
324
|
+
|
|
302
325
|
# Global Scan command options
|
|
303
326
|
for p in [p_scan]:
|
|
304
327
|
p.add_argument('--apiurl', type=str,
|
|
@@ -344,7 +367,7 @@ def setup_args() -> None:
|
|
|
344
367
|
|
|
345
368
|
# Help/Trace command options
|
|
346
369
|
for p in [p_scan, p_wfp, p_dep, p_fc, p_cnv, p_c_loc, p_c_dwnld, p_p_proxy, c_crypto, c_vulns, c_search,
|
|
347
|
-
c_versions, c_semgrep, p_results]:
|
|
370
|
+
c_versions, c_semgrep, p_results, p_undeclared, p_copyleft]:
|
|
348
371
|
p.add_argument('--debug', '-d', action='store_true', help='Enable debug messages')
|
|
349
372
|
p.add_argument('--trace', '-t', action='store_true', help='Enable trace messages, including API posts')
|
|
350
373
|
p.add_argument('--quiet', '-q', action='store_true', help='Enable quiet mode')
|
|
@@ -357,9 +380,10 @@ def setup_args() -> None:
|
|
|
357
380
|
parser.print_help() # No sub command subcommand, print general help
|
|
358
381
|
exit(1)
|
|
359
382
|
else:
|
|
360
|
-
if (args.subparser == 'utils' or args.subparser == 'ut' or
|
|
361
|
-
args.subparser == 'component' or args.subparser == 'comp'
|
|
362
|
-
|
|
383
|
+
if ((args.subparser == 'utils' or args.subparser == 'ut' or
|
|
384
|
+
args.subparser == 'component' or args.subparser == 'comp' or
|
|
385
|
+
args.subparser == 'inspect' or args.subparser == 'insp' or args.subparser == 'ins')
|
|
386
|
+
and not args.subparsercmd):
|
|
363
387
|
parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
|
|
364
388
|
exit(1)
|
|
365
389
|
args.func(parser, args) # Execute the function associated with the sub-command
|
|
@@ -778,6 +802,64 @@ def convert(parser, args):
|
|
|
778
802
|
if not success:
|
|
779
803
|
exit(1)
|
|
780
804
|
|
|
805
|
+
def inspect_copyleft(parser, args):
|
|
806
|
+
"""
|
|
807
|
+
Run the "inspect" sub-command
|
|
808
|
+
Parameters
|
|
809
|
+
----------
|
|
810
|
+
parser: ArgumentParser
|
|
811
|
+
command line parser object
|
|
812
|
+
args: Namespace
|
|
813
|
+
Parsed arguments
|
|
814
|
+
"""
|
|
815
|
+
if args.input is None:
|
|
816
|
+
print_stderr('Please specify an input file to inspect')
|
|
817
|
+
parser.parse_args([args.subparser, args.subparsercmd, '-h'])
|
|
818
|
+
exit(1)
|
|
819
|
+
output: str = None
|
|
820
|
+
if args.output:
|
|
821
|
+
output = args.output
|
|
822
|
+
open(output, 'w').close()
|
|
823
|
+
|
|
824
|
+
status_output: str = None
|
|
825
|
+
if args.status:
|
|
826
|
+
status_output = args.status
|
|
827
|
+
open(status_output, 'w').close()
|
|
828
|
+
|
|
829
|
+
i_copyleft = Copyleft(debug=args.debug, trace=args.trace, quiet=args.quiet, filepath=args.input,
|
|
830
|
+
format_type=args.format, status=status_output, output=output, include=args.include,
|
|
831
|
+
exclude=args.exclude, explicit=args.explicit)
|
|
832
|
+
status, _ = i_copyleft.run()
|
|
833
|
+
sys.exit(status)
|
|
834
|
+
|
|
835
|
+
def inspect_undeclared(parser, args):
|
|
836
|
+
"""
|
|
837
|
+
Run the "inspect" sub-command
|
|
838
|
+
Parameters
|
|
839
|
+
----------
|
|
840
|
+
parser: ArgumentParser
|
|
841
|
+
command line parser object
|
|
842
|
+
args: Namespace
|
|
843
|
+
Parsed arguments
|
|
844
|
+
"""
|
|
845
|
+
if args.input is None:
|
|
846
|
+
print_stderr('Please specify an input file to inspect')
|
|
847
|
+
parser.parse_args([args.subparser, args.subparsercmd, '-h'])
|
|
848
|
+
exit(1)
|
|
849
|
+
output: str = None
|
|
850
|
+
if args.output:
|
|
851
|
+
output = args.output
|
|
852
|
+
open(output, 'w').close()
|
|
853
|
+
|
|
854
|
+
status_output: str = None
|
|
855
|
+
if args.status:
|
|
856
|
+
status_output = args.status
|
|
857
|
+
open(status_output, 'w').close()
|
|
858
|
+
i_undeclared = UndeclaredComponent(debug=args.debug, trace=args.trace, quiet=args.quiet,
|
|
859
|
+
filepath=args.input, format_type=args.format,
|
|
860
|
+
status=status_output, output=output)
|
|
861
|
+
status, _ = i_undeclared.run()
|
|
862
|
+
sys.exit(status)
|
|
781
863
|
|
|
782
864
|
def utils_certloc(*_):
|
|
783
865
|
"""
|
|
@@ -787,7 +869,6 @@ def utils_certloc(*_):
|
|
|
787
869
|
import certifi
|
|
788
870
|
print(f'CA Cert File: {certifi.where()}')
|
|
789
871
|
|
|
790
|
-
|
|
791
872
|
def utils_cert_download(_, args):
|
|
792
873
|
"""
|
|
793
874
|
Run the "utils cert-download" sub-command
|
|
@@ -820,7 +901,7 @@ def utils_cert_download(_, args):
|
|
|
820
901
|
else:
|
|
821
902
|
cn = cert_components.get('CN')
|
|
822
903
|
if not args.quiet:
|
|
823
|
-
print_stderr(f'
|
|
904
|
+
print_stderr(f'Certificate {index} - CN: {cn}')
|
|
824
905
|
if sys.version_info[0] >= 3:
|
|
825
906
|
print((crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8')).strip(), file=file) # Print the downloaded PEM certificate
|
|
826
907
|
else:
|
scanoss/data/build_date.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
date:
|
|
1
|
+
date: 20241024162611, utime: 1729787171
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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
|
+
"""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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
|
+
from typing import Dict, Any
|
|
26
|
+
from scanoss.inspection.policy_check import PolicyCheck, PolicyStatus
|
|
27
|
+
|
|
28
|
+
class Copyleft(PolicyCheck):
|
|
29
|
+
"""
|
|
30
|
+
SCANOSS Copyleft class
|
|
31
|
+
Inspects components for copyleft licenses
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False, filepath: str = None,
|
|
35
|
+
format_type: str = 'json', status: str = None, output: str = None, include: str = None,
|
|
36
|
+
exclude: str = None, explicit: str = None):
|
|
37
|
+
"""
|
|
38
|
+
Initialize the Copyleft class.
|
|
39
|
+
|
|
40
|
+
:param debug: Enable debug mode
|
|
41
|
+
:param trace: Enable trace mode (default True)
|
|
42
|
+
:param quiet: Enable quiet mode
|
|
43
|
+
:param filepath: Path to the file containing component data
|
|
44
|
+
:param format_type: Output format ('json' or 'md')
|
|
45
|
+
:param status: Path to save the status output
|
|
46
|
+
:param output: Path to save detailed output
|
|
47
|
+
:param include: Licenses to include in the analysis
|
|
48
|
+
:param exclude: Licenses to exclude from the analysis
|
|
49
|
+
:param explicit: Explicitly defined licenses
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(debug, trace, quiet, filepath, format_type, status, output, name='Copyleft Policy')
|
|
52
|
+
self.license_util.init(include, exclude, explicit)
|
|
53
|
+
self.filepath = filepath
|
|
54
|
+
self.format = format
|
|
55
|
+
self.output = output
|
|
56
|
+
self.status = status
|
|
57
|
+
self.include = include
|
|
58
|
+
self.exclude = exclude
|
|
59
|
+
self.explicit = explicit
|
|
60
|
+
|
|
61
|
+
def _json(self, components: list) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Format the components with copyleft licenses as JSON.
|
|
64
|
+
|
|
65
|
+
:param components: List of components with copyleft licenses
|
|
66
|
+
:return: Dictionary with formatted JSON details and summary
|
|
67
|
+
"""
|
|
68
|
+
details = {}
|
|
69
|
+
if len(components) > 0:
|
|
70
|
+
details = { 'components': components }
|
|
71
|
+
return {
|
|
72
|
+
'details': f'{json.dumps(details, indent=2)}\n',
|
|
73
|
+
'summary': f'{len(components)} component(s) with copyleft licenses were found.\n'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _markdown(self, components: list) -> Dict[str,Any]:
|
|
77
|
+
"""
|
|
78
|
+
Format the components with copyleft licenses as Markdown.
|
|
79
|
+
|
|
80
|
+
:param components: List of components with copyleft licenses
|
|
81
|
+
:return: Dictionary with formatted Markdown details and summary
|
|
82
|
+
"""
|
|
83
|
+
headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']
|
|
84
|
+
centered_columns = [1, 4]
|
|
85
|
+
rows: [[]]= []
|
|
86
|
+
for component in components:
|
|
87
|
+
for lic in component['licenses']:
|
|
88
|
+
row = [
|
|
89
|
+
component['purl'],
|
|
90
|
+
component['version'],
|
|
91
|
+
lic['spdxid'],
|
|
92
|
+
lic['url'],
|
|
93
|
+
'YES' if lic['copyleft'] else 'NO'
|
|
94
|
+
]
|
|
95
|
+
rows.append(row)
|
|
96
|
+
# End license loop
|
|
97
|
+
# End component loop
|
|
98
|
+
return {
|
|
99
|
+
'details': f'### Copyleft licenses\n{self.generate_table(headers,rows,centered_columns)}\n',
|
|
100
|
+
'summary' : f'{len(components)} component(s) with copyleft licenses were found.\n'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def _filter_components_with_copyleft_licenses(self, components: list) -> list:
|
|
104
|
+
"""
|
|
105
|
+
Filter the components list to include only those with copyleft licenses.
|
|
106
|
+
|
|
107
|
+
:param components: List of all components
|
|
108
|
+
:return: List of components with copyleft licenses
|
|
109
|
+
"""
|
|
110
|
+
filtered_components = []
|
|
111
|
+
for component in components:
|
|
112
|
+
copyleft_licenses = [lic for lic in component['licenses'] if lic['copyleft']]
|
|
113
|
+
if copyleft_licenses:
|
|
114
|
+
filtered_component = component
|
|
115
|
+
filtered_component['licenses'] = copyleft_licenses
|
|
116
|
+
del filtered_component['status']
|
|
117
|
+
filtered_components.append(filtered_component)
|
|
118
|
+
# End component loop
|
|
119
|
+
self.print_debug(f'Copyleft components: {filtered_components}')
|
|
120
|
+
return filtered_components
|
|
121
|
+
|
|
122
|
+
def run(self):
|
|
123
|
+
"""
|
|
124
|
+
Run the copyleft license inspection process.
|
|
125
|
+
|
|
126
|
+
This method performs the following steps:
|
|
127
|
+
1. Get all components
|
|
128
|
+
2. Filter components with copyleft licenses
|
|
129
|
+
3. Format the results
|
|
130
|
+
4. Save the output to files if required
|
|
131
|
+
|
|
132
|
+
:return: Dictionary containing the inspection results
|
|
133
|
+
"""
|
|
134
|
+
self._debug()
|
|
135
|
+
# Get the components from the results
|
|
136
|
+
components = self._get_components()
|
|
137
|
+
if components is None:
|
|
138
|
+
return PolicyStatus.ERROR.value, {}
|
|
139
|
+
# Get a list of copyleft components if they exist
|
|
140
|
+
copyleft_components = self._filter_components_with_copyleft_licenses(components)
|
|
141
|
+
# Get a formatter for the output results
|
|
142
|
+
formatter = self._get_formatter()
|
|
143
|
+
if formatter is None:
|
|
144
|
+
return PolicyStatus.ERROR.value, {}
|
|
145
|
+
# Format the results
|
|
146
|
+
results = formatter(copyleft_components)
|
|
147
|
+
## Save outputs if required
|
|
148
|
+
self.print_to_file_or_stdout(results['details'], self.output)
|
|
149
|
+
self.print_to_file_or_stderr(results['summary'], self.status)
|
|
150
|
+
# Check to see if we have policy violations
|
|
151
|
+
if len(copyleft_components) <= 0:
|
|
152
|
+
return PolicyStatus.FAIL.value, results
|
|
153
|
+
return PolicyStatus.SUCCESS.value, results
|
|
154
|
+
#
|
|
155
|
+
# End of Copyleft Class
|
|
156
|
+
#
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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
|
+
from abc import abstractmethod
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Callable, List, Dict, Any
|
|
29
|
+
from scanoss.inspection.utils.license_utils import LicenseUtil
|
|
30
|
+
from scanoss.scanossbase import ScanossBase
|
|
31
|
+
|
|
32
|
+
class PolicyStatus(Enum):
|
|
33
|
+
"""
|
|
34
|
+
Enumeration representing the status of a policy check.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
SUCCESS (int): Indicates that the policy check passed successfully (value: 0).
|
|
38
|
+
FAIL (int): Indicates that the policy check failed (value: 1).
|
|
39
|
+
ERROR (int): Indicates that an error occurred during the policy check (value: 2).
|
|
40
|
+
"""
|
|
41
|
+
SUCCESS = 0
|
|
42
|
+
FAIL = 1
|
|
43
|
+
ERROR = 2
|
|
44
|
+
#
|
|
45
|
+
# End of PolicyStatus Class
|
|
46
|
+
#
|
|
47
|
+
|
|
48
|
+
class ComponentID(Enum):
|
|
49
|
+
"""
|
|
50
|
+
Enumeration representing different types of software components.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
FILE (str): Represents a file component (value: "file").
|
|
54
|
+
SNIPPET (str): Represents a code snippet component (value: "snippet").
|
|
55
|
+
DEPENDENCY (str): Represents a dependency component (value: "dependency").
|
|
56
|
+
"""
|
|
57
|
+
FILE = "file"
|
|
58
|
+
SNIPPET = "snippet"
|
|
59
|
+
DEPENDENCY = "dependency"
|
|
60
|
+
#
|
|
61
|
+
# End of ComponentID Class
|
|
62
|
+
#
|
|
63
|
+
|
|
64
|
+
class PolicyCheck(ScanossBase):
|
|
65
|
+
"""
|
|
66
|
+
A base class for implementing various software policy checks.
|
|
67
|
+
|
|
68
|
+
This class provides a framework for policy checking, including methods for
|
|
69
|
+
processing components, generating output in different formats.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
VALID_FORMATS (set): A set of valid output formats ('md', 'json').
|
|
73
|
+
|
|
74
|
+
Inherits from:
|
|
75
|
+
ScanossBase: A base class providing common functionality for SCANOSS-related operations.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
VALID_FORMATS = {'md', 'json'}
|
|
79
|
+
|
|
80
|
+
def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False, filepath: str = None,
|
|
81
|
+
format_type: str = None, status: str = None, output: str = None, name: str = None):
|
|
82
|
+
super().__init__(debug, trace, quiet)
|
|
83
|
+
self.license_util = LicenseUtil()
|
|
84
|
+
self.filepath = filepath
|
|
85
|
+
self.name = name
|
|
86
|
+
self.output = output
|
|
87
|
+
self.format_type = format_type
|
|
88
|
+
self.status = status
|
|
89
|
+
self.results = self._load_input_file()
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def run(self):
|
|
93
|
+
"""
|
|
94
|
+
Execute the policy check process.
|
|
95
|
+
|
|
96
|
+
This abstract method should be implemented by subclasses to perform specific
|
|
97
|
+
policy checks. The general structure of this method typically includes:
|
|
98
|
+
1. Retrieving components
|
|
99
|
+
2. Filtering components based on specific criteria
|
|
100
|
+
3. Formatting the results
|
|
101
|
+
4. Saving the output to files if required
|
|
102
|
+
|
|
103
|
+
:return: A tuple containing:
|
|
104
|
+
- First element: PolicyStatus enum value (SUCCESS, FAIL, or ERROR)
|
|
105
|
+
- Second element: Dictionary containing the inspection results
|
|
106
|
+
"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def _json(self, components: list) -> Dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Format the policy checks results as JSON.
|
|
113
|
+
This method should be implemented by subclasses to create a Markdown representation
|
|
114
|
+
of the policy check results.
|
|
115
|
+
|
|
116
|
+
:param components: List of components to be formatted.
|
|
117
|
+
:return: A dictionary containing two keys:
|
|
118
|
+
- 'details': A JSON-formatted string with the full list of components
|
|
119
|
+
- 'summary': A string summarizing the number of components found
|
|
120
|
+
"""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def _markdown(self, components: list) -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Generate Markdown output for the policy check results.
|
|
127
|
+
|
|
128
|
+
This method should be implemented by subclasses to create a Markdown representation
|
|
129
|
+
of the policy check results.
|
|
130
|
+
|
|
131
|
+
:param components: List of components to be included in the output.
|
|
132
|
+
:return: A dictionary representing the Markdown output.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def _append_component(self,components: Dict[str, Any], new_component: Dict[str, Any]) -> Dict[str, Any]:
|
|
137
|
+
"""
|
|
138
|
+
Append a new component to the component's dictionary.
|
|
139
|
+
|
|
140
|
+
This function creates a new entry in the components dictionary for the given component,
|
|
141
|
+
or updates an existing entry if the component already exists. It also processes the
|
|
142
|
+
licenses associated with the component.
|
|
143
|
+
|
|
144
|
+
:param components: The existing dictionary of components
|
|
145
|
+
:param new_component: The new component to be added or updated
|
|
146
|
+
:return: The updated components dictionary
|
|
147
|
+
"""
|
|
148
|
+
component_key = f"{new_component['purl'][0]}@{new_component['version']}"
|
|
149
|
+
components[component_key] = {
|
|
150
|
+
'purl': new_component['purl'][0],
|
|
151
|
+
'version': new_component['version'],
|
|
152
|
+
'licenses': {},
|
|
153
|
+
'status': new_component['status'],
|
|
154
|
+
}
|
|
155
|
+
if not new_component.get('licenses'):
|
|
156
|
+
self.print_stderr(f'WARNING: Results missing licenses. Skipping.')
|
|
157
|
+
return components
|
|
158
|
+
# Process licenses for this component
|
|
159
|
+
for l in new_component['licenses']:
|
|
160
|
+
if l.get('name'):
|
|
161
|
+
spdxid = l['name']
|
|
162
|
+
components[component_key]['licenses'][spdxid] = {
|
|
163
|
+
'spdxid': spdxid,
|
|
164
|
+
'copyleft': self.license_util.is_copyleft(spdxid),
|
|
165
|
+
'url': self.license_util.get_spdx_url(spdxid),
|
|
166
|
+
}
|
|
167
|
+
return components
|
|
168
|
+
|
|
169
|
+
def _get_components_from_results(self,results: Dict[str, Any]) -> list or None:
|
|
170
|
+
"""
|
|
171
|
+
Process the results dictionary to extract and format component information.
|
|
172
|
+
|
|
173
|
+
This function iterates through the results dictionary, identifying components from
|
|
174
|
+
different sources (files, snippets, and dependencies). It consolidates this information
|
|
175
|
+
into a list of unique components, each with its associated licenses and other details.
|
|
176
|
+
|
|
177
|
+
:param results: A dictionary containing the raw results of a component scan
|
|
178
|
+
:return: A list of dictionaries, each representing a unique component with its details
|
|
179
|
+
"""
|
|
180
|
+
if results is None:
|
|
181
|
+
self.print_stderr(f'ERROR: Results cannot be empty')
|
|
182
|
+
return None
|
|
183
|
+
components = {}
|
|
184
|
+
for component in results.values():
|
|
185
|
+
for c in component:
|
|
186
|
+
component_id = c.get('id')
|
|
187
|
+
if not component_id:
|
|
188
|
+
self.print_stderr(f'WARNING: Result missing id. Skipping.')
|
|
189
|
+
continue
|
|
190
|
+
if component_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
|
|
191
|
+
if not c.get('purl'):
|
|
192
|
+
self.print_stderr(f'WARNING: Result missing purl. Skipping.')
|
|
193
|
+
continue
|
|
194
|
+
if len(c.get('purl')) <= 0:
|
|
195
|
+
self.print_stderr(f'WARNING: Result missing purls. Skipping.')
|
|
196
|
+
continue
|
|
197
|
+
if not c.get('version'):
|
|
198
|
+
self.print_stderr(f'WARNING: Result missing version. Skipping.')
|
|
199
|
+
continue
|
|
200
|
+
component_key = f"{c['purl'][0]}@{c['version']}"
|
|
201
|
+
# Initialize or update the component entry
|
|
202
|
+
if component_key not in components:
|
|
203
|
+
components = self._append_component(components, c)
|
|
204
|
+
if c['id'] == ComponentID.DEPENDENCY.value:
|
|
205
|
+
if c.get('dependency') is None:
|
|
206
|
+
continue
|
|
207
|
+
for d in c['dependencies']:
|
|
208
|
+
if not d.get('purl'):
|
|
209
|
+
self.print_stderr(f'WARNING: Result missing purl. Skipping.')
|
|
210
|
+
continue
|
|
211
|
+
if len(d.get('purl')) <= 0:
|
|
212
|
+
self.print_stderr(f'WARNING: Result missing purls. Skipping.')
|
|
213
|
+
continue
|
|
214
|
+
if not d.get('version'):
|
|
215
|
+
self.print_stderr(f'WARNING: Result missing version. Skipping.')
|
|
216
|
+
continue
|
|
217
|
+
component_key = f"{d['purl'][0]}@{d['version']}"
|
|
218
|
+
if component_key not in components:
|
|
219
|
+
components = self._append_component(components, d)
|
|
220
|
+
# End of dependencies loop
|
|
221
|
+
# End if
|
|
222
|
+
# End of component loop
|
|
223
|
+
# End of results loop
|
|
224
|
+
results = list(components.values())
|
|
225
|
+
for component in results:
|
|
226
|
+
component['licenses'] = list(component['licenses'].values())
|
|
227
|
+
|
|
228
|
+
return results
|
|
229
|
+
|
|
230
|
+
def generate_table(self, headers, rows, centered_columns=None):
|
|
231
|
+
"""
|
|
232
|
+
Generate a Markdown table.
|
|
233
|
+
|
|
234
|
+
:param headers: List of headers for the table.
|
|
235
|
+
:param rows: List of rows for the table.
|
|
236
|
+
:param centered_columns: List of column indices to be centered.
|
|
237
|
+
:return: A string representing the Markdown table.
|
|
238
|
+
"""
|
|
239
|
+
col_sep = ' | '
|
|
240
|
+
centered_column_set = set(centered_columns or [])
|
|
241
|
+
if headers is None:
|
|
242
|
+
self.print_stderr('ERROR: Header are no set')
|
|
243
|
+
return None
|
|
244
|
+
# Decide which separator to use
|
|
245
|
+
def create_separator(index):
|
|
246
|
+
if centered_columns is None:
|
|
247
|
+
return '-'
|
|
248
|
+
return ':-:' if index in centered_column_set else '-'
|
|
249
|
+
# Build the row separator
|
|
250
|
+
row_separator = col_sep + col_sep.join(create_separator(index) for index, _ in enumerate(headers)) + col_sep
|
|
251
|
+
# build table rows
|
|
252
|
+
table_rows = [col_sep + col_sep.join(headers) + col_sep, row_separator]
|
|
253
|
+
table_rows.extend(col_sep + col_sep.join(row) + col_sep for row in rows)
|
|
254
|
+
return '\n'.join(table_rows)
|
|
255
|
+
|
|
256
|
+
def _get_formatter(self)-> Callable[[List[dict]], Dict[str,Any]] or None:
|
|
257
|
+
"""
|
|
258
|
+
Get the appropriate formatter function based on the specified format.
|
|
259
|
+
|
|
260
|
+
:return: Formatter function (either _json or _markdown)
|
|
261
|
+
"""
|
|
262
|
+
valid_format = self._is_valid_format()
|
|
263
|
+
if not valid_format:
|
|
264
|
+
return None
|
|
265
|
+
# a map of which format function to return
|
|
266
|
+
function_map = {
|
|
267
|
+
'json': self._json,
|
|
268
|
+
'md': self._markdown
|
|
269
|
+
}
|
|
270
|
+
return function_map[self.format_type]
|
|
271
|
+
|
|
272
|
+
def _debug(self):
|
|
273
|
+
"""
|
|
274
|
+
Print debug information about the policy check.
|
|
275
|
+
|
|
276
|
+
This method prints various attributes of the PolicyCheck instance for debugging purposes.
|
|
277
|
+
"""
|
|
278
|
+
if self.debug:
|
|
279
|
+
self.print_stderr(f'Policy: {self.name}')
|
|
280
|
+
self.print_stderr(f'Format: {self.format_type}')
|
|
281
|
+
self.print_stderr(f'Status: {self.status}')
|
|
282
|
+
self.print_stderr(f'Output: {self.output}')
|
|
283
|
+
self.print_stderr(f'Input: {self.filepath}')
|
|
284
|
+
|
|
285
|
+
def _is_valid_format(self) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Validate if the format specified is supported.
|
|
288
|
+
|
|
289
|
+
This method checks if the format stored in format is one of the
|
|
290
|
+
valid formats defined in self.VALID_FORMATS.
|
|
291
|
+
|
|
292
|
+
:return: bool: True if the format is valid, False otherwise.
|
|
293
|
+
"""
|
|
294
|
+
if self.format_type not in self.VALID_FORMATS:
|
|
295
|
+
valid_formats_str = ', '.join(self.VALID_FORMATS)
|
|
296
|
+
self.print_stderr(f'ERROR: Invalid format "{self.format_type}". Valid formats are: {valid_formats_str}')
|
|
297
|
+
return False
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
def _load_input_file(self):
|
|
301
|
+
"""
|
|
302
|
+
Load the result.json file
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Dict[str, Any]: The parsed JSON data
|
|
306
|
+
"""
|
|
307
|
+
if not os.path.exists(self.filepath):
|
|
308
|
+
self.print_stderr(f'ERROR: The file "{self.filepath}" does not exist.')
|
|
309
|
+
return None
|
|
310
|
+
with open(self.filepath, "r") as jsonfile:
|
|
311
|
+
try:
|
|
312
|
+
return json.load(jsonfile)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def _get_components(self):
|
|
318
|
+
"""
|
|
319
|
+
Retrieve and process components from the preloaded results.
|
|
320
|
+
|
|
321
|
+
This method performs the following steps:
|
|
322
|
+
1. Checks if the results have been previously loaded (self.results).
|
|
323
|
+
2. Extracts and processes components from the loaded results.
|
|
324
|
+
|
|
325
|
+
:return: A list of processed components, or None if an error occurred during any step.
|
|
326
|
+
Possible reasons for returning None include:
|
|
327
|
+
- Results not loaded (self.results is None)
|
|
328
|
+
- Failure to extract components from the results
|
|
329
|
+
|
|
330
|
+
Note:
|
|
331
|
+
- This method assumes that the results have been previously loaded and stored in self.results.
|
|
332
|
+
- If results is None, the method returns None without performing any further operations.
|
|
333
|
+
- The actual processing of components is delegated to the _get_components_from_results method.
|
|
334
|
+
"""
|
|
335
|
+
if self.results is None:
|
|
336
|
+
return None
|
|
337
|
+
components = self._get_components_from_results(self.results)
|
|
338
|
+
return components
|
|
339
|
+
#
|
|
340
|
+
# End of PolicyCheck Class
|
|
341
|
+
#
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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
|
+
from typing import Dict, Any
|
|
26
|
+
from scanoss.inspection.policy_check import PolicyCheck, PolicyStatus
|
|
27
|
+
|
|
28
|
+
class UndeclaredComponent(PolicyCheck):
|
|
29
|
+
"""
|
|
30
|
+
SCANOSS UndeclaredComponent class
|
|
31
|
+
Inspects for undeclared components
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False, filepath: str = None,
|
|
35
|
+
format_type: str = 'json', status: str = None, output: str = None):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the UndeclaredComponent class.
|
|
38
|
+
|
|
39
|
+
:param debug: Enable debug mode
|
|
40
|
+
:param trace: Enable trace mode (default True)
|
|
41
|
+
:param quiet: Enable quiet mode
|
|
42
|
+
:param filepath: Path to the file containing component data
|
|
43
|
+
:param format_type: Output format ('json' or 'md')
|
|
44
|
+
:param status: Path to save status output
|
|
45
|
+
:param output: Path to save detailed output
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(debug, trace, quiet, filepath, format_type, status, output,
|
|
48
|
+
name='Undeclared Components Policy')
|
|
49
|
+
self.filepath = filepath
|
|
50
|
+
self.format = format
|
|
51
|
+
self.output = output
|
|
52
|
+
self.status = status
|
|
53
|
+
|
|
54
|
+
def _get_undeclared_component(self, components: list)-> list or None:
|
|
55
|
+
"""
|
|
56
|
+
Filter the components list to include only undeclared components.
|
|
57
|
+
|
|
58
|
+
:param components: List of all components
|
|
59
|
+
:return: List of undeclared components
|
|
60
|
+
"""
|
|
61
|
+
if components is None:
|
|
62
|
+
self.print_stderr(f'WARNING: No components provided!')
|
|
63
|
+
return None
|
|
64
|
+
undeclared_components = []
|
|
65
|
+
for component in components:
|
|
66
|
+
if component['status'] == 'pending':
|
|
67
|
+
del component['status']
|
|
68
|
+
undeclared_components.append(component)
|
|
69
|
+
# end component loop
|
|
70
|
+
return undeclared_components
|
|
71
|
+
|
|
72
|
+
def _get_summary(self, components: list) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Get a summary of the undeclared components.
|
|
75
|
+
|
|
76
|
+
:param components: List of all components
|
|
77
|
+
:return: Component summary markdown
|
|
78
|
+
"""
|
|
79
|
+
summary = f'{len(components)} undeclared component(s) were found.\n'
|
|
80
|
+
if len(components) > 0:
|
|
81
|
+
summary += (f'Add the following snippet into your `sbom.json` file\n'
|
|
82
|
+
f'\n```json\n{json.dumps(self._generate_sbom_file(components), indent=2)}\n```\n')
|
|
83
|
+
return summary
|
|
84
|
+
|
|
85
|
+
def _json(self, components: list) -> Dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Format the undeclared components as JSON.
|
|
88
|
+
|
|
89
|
+
:param components: List of undeclared components
|
|
90
|
+
:return: Dictionary with formatted JSON details and summary
|
|
91
|
+
"""
|
|
92
|
+
details = {}
|
|
93
|
+
if len(components) > 0:
|
|
94
|
+
details = {'components': components}
|
|
95
|
+
return {
|
|
96
|
+
'details': f'{json.dumps(details, indent=2)}\n',
|
|
97
|
+
'summary': self._get_summary(components),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def _markdown(self, components: list) -> Dict[str,Any]:
|
|
101
|
+
"""
|
|
102
|
+
Format the undeclared components as Markdown.
|
|
103
|
+
|
|
104
|
+
:param components: List of undeclared components
|
|
105
|
+
:return: Dictionary with formatted Markdown details and summary
|
|
106
|
+
"""
|
|
107
|
+
headers = ['Component', 'Version', 'License']
|
|
108
|
+
rows: [[]]= []
|
|
109
|
+
# TODO look at using SpdxLite license name lookup method
|
|
110
|
+
for component in components:
|
|
111
|
+
licenses = " - ".join(lic.get('spdxid', 'Unknown') for lic in component['licenses'])
|
|
112
|
+
rows.append([component['purl'], component['version'], licenses])
|
|
113
|
+
return {
|
|
114
|
+
'details': f'### Undeclared components\n{self.generate_table(headers,rows)}\n',
|
|
115
|
+
'summary': self._get_summary(components),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def _generate_sbom_file(self, components: list) -> list:
|
|
119
|
+
"""
|
|
120
|
+
Generate a list of PURLs for the SBOM file.
|
|
121
|
+
|
|
122
|
+
:param components: List of undeclared components
|
|
123
|
+
:return: List of dictionaries containing PURLs
|
|
124
|
+
"""
|
|
125
|
+
sbom = {}
|
|
126
|
+
if components is None:
|
|
127
|
+
self.print_stderr(f'WARNING: No components provided!')
|
|
128
|
+
else:
|
|
129
|
+
for component in components:
|
|
130
|
+
sbom[component['purl']] = { 'purl': component['purl'] }
|
|
131
|
+
return list(sbom.values())
|
|
132
|
+
|
|
133
|
+
def run(self):
|
|
134
|
+
"""
|
|
135
|
+
Run the undeclared component inspection process.
|
|
136
|
+
|
|
137
|
+
This method performs the following steps:
|
|
138
|
+
1. Get all components
|
|
139
|
+
2. Filter undeclared components
|
|
140
|
+
3. Format the results
|
|
141
|
+
4. Save the output to files if required
|
|
142
|
+
|
|
143
|
+
:return: Dictionary containing the inspection results
|
|
144
|
+
"""
|
|
145
|
+
self._debug()
|
|
146
|
+
components = self._get_components()
|
|
147
|
+
if components is None:
|
|
148
|
+
return PolicyStatus.ERROR.value, {}
|
|
149
|
+
# Get undeclared component summary (if any)
|
|
150
|
+
undeclared_components = self._get_undeclared_component(components)
|
|
151
|
+
if undeclared_components is None:
|
|
152
|
+
return PolicyStatus.ERROR.value, {}
|
|
153
|
+
self.print_debug(f'Undeclared components: {undeclared_components}')
|
|
154
|
+
formatter = self._get_formatter()
|
|
155
|
+
if formatter is None:
|
|
156
|
+
return PolicyStatus.ERROR.value, {}
|
|
157
|
+
results = formatter(undeclared_components)
|
|
158
|
+
# Output the results
|
|
159
|
+
self.print_to_file_or_stdout(results['details'], self.output)
|
|
160
|
+
self.print_to_file_or_stderr(results['summary'], self.status)
|
|
161
|
+
# Determine if the filter found results or not
|
|
162
|
+
if len(undeclared_components) <= 0:
|
|
163
|
+
return PolicyStatus.FAIL.value, results
|
|
164
|
+
return PolicyStatus.SUCCESS.value, results
|
|
165
|
+
#
|
|
166
|
+
# End of UndeclaredComponent Class
|
|
167
|
+
#
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, 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
|
+
from scanoss.scanossbase import ScanossBase
|
|
25
|
+
|
|
26
|
+
DEFAULT_COPYLEFT_LICENSES = {
|
|
27
|
+
'agpl-3.0-only', 'artistic-1.0', 'artistic-2.0', 'cc-by-sa-4.0', 'cddl-1.0', 'cddl-1.1', 'cecill-2.1',
|
|
28
|
+
'epl-1.0', 'epl-2.0', 'gfdl-1.1-only', 'gfdl-1.2-only', 'gfdl-1.3-only', 'gpl-1.0-only', 'gpl-2.0-only',
|
|
29
|
+
'gpl-3.0-only', 'lgpl-2.1-only', 'lgpl-3.0-only', 'mpl-1.1', 'mpl-2.0', 'sleepycat', 'watcom-1.0'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class LicenseUtil(ScanossBase):
|
|
33
|
+
"""
|
|
34
|
+
A utility class for handling software licenses, particularly copyleft licenses.
|
|
35
|
+
|
|
36
|
+
This class provides functionality to initialize, manage, and query a set of
|
|
37
|
+
copyleft licenses. It also offers a method to generate URLs for license information.
|
|
38
|
+
"""
|
|
39
|
+
BASE_SPDX_ORG_URL = 'https://spdx.org/licenses'
|
|
40
|
+
BASE_OSADL_URL = 'https://www.osadl.org/fileadmin/checklists/unreflicenses'
|
|
41
|
+
|
|
42
|
+
def __init__(self,debug: bool = False, trace: bool = True, quiet: bool = False):
|
|
43
|
+
super().__init__(debug, trace, quiet)
|
|
44
|
+
self.default_copyleft_licenses = set(DEFAULT_COPYLEFT_LICENSES)
|
|
45
|
+
self.copyleft_licenses = set()
|
|
46
|
+
|
|
47
|
+
def init(self, include: str = None, exclude: str = None, explicit: str = None):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the set of copyleft licenses based on user input.
|
|
50
|
+
|
|
51
|
+
This method allows for customization of the copyleft license set by:
|
|
52
|
+
- Setting an explicit list of licenses
|
|
53
|
+
- Including additional licenses to the default set
|
|
54
|
+
- Excluding specific licenses from the default set
|
|
55
|
+
|
|
56
|
+
:param include: Comma-separated string of licenses to include
|
|
57
|
+
:param exclude: Comma-separated string of licenses to exclude
|
|
58
|
+
:param explicit: Comma-separated string of licenses to use exclusively
|
|
59
|
+
"""
|
|
60
|
+
if self.debug:
|
|
61
|
+
self.print_stderr(f'Include Copyleft licenses: ${include}')
|
|
62
|
+
self.print_stderr(f'Exclude Copyleft licenses: ${exclude}')
|
|
63
|
+
self.print_stderr(f'Explicit Copyleft licenses: ${explicit}')
|
|
64
|
+
if explicit:
|
|
65
|
+
explicit = explicit.strip()
|
|
66
|
+
if explicit:
|
|
67
|
+
exp = [item.strip().lower() for item in explicit.split(',')]
|
|
68
|
+
self.copyleft_licenses = set(exp)
|
|
69
|
+
self.print_debug(f'Copyleft licenses: ${self.copyleft_licenses}')
|
|
70
|
+
return
|
|
71
|
+
# If no explicit licenses were set, set default ones
|
|
72
|
+
self.copyleft_licenses = self.default_copyleft_licenses.copy()
|
|
73
|
+
if include:
|
|
74
|
+
include = include.strip()
|
|
75
|
+
if include:
|
|
76
|
+
inc =[item.strip().lower() for item in include.split(',')]
|
|
77
|
+
self.copyleft_licenses.update(inc)
|
|
78
|
+
if exclude:
|
|
79
|
+
exclude = exclude.strip()
|
|
80
|
+
if exclude:
|
|
81
|
+
inc = [item.strip().lower() for item in exclude.split(',')]
|
|
82
|
+
for lic in inc:
|
|
83
|
+
self.copyleft_licenses.discard(lic)
|
|
84
|
+
self.print_debug(f'Copyleft licenses: ${self.copyleft_licenses}')
|
|
85
|
+
|
|
86
|
+
def is_copyleft(self, spdxid: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if a given license is considered copyleft.
|
|
89
|
+
|
|
90
|
+
:param spdxid: The SPDX identifier of the license to check
|
|
91
|
+
:return: True if the license is copyleft, False otherwise
|
|
92
|
+
"""
|
|
93
|
+
return spdxid.lower() in self.copyleft_licenses
|
|
94
|
+
|
|
95
|
+
def get_spdx_url(self, spdxid: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Generate the URL for the SPDX page of a license.
|
|
98
|
+
|
|
99
|
+
:param spdxid: The SPDX identifier of the license
|
|
100
|
+
:return: The URL of the SPDX page for the given license
|
|
101
|
+
"""
|
|
102
|
+
return f'{self.BASE_SPDX_ORG_URL}/{spdxid}.html'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_osadl_url(self, spdxid: str) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Generate the URL for the OSADL (Open Source Automation Development Lab) page of a license.
|
|
108
|
+
|
|
109
|
+
:param spdxid: The SPDX identifier of the license
|
|
110
|
+
:return: The URL of the OSADL page for the given license
|
|
111
|
+
"""
|
|
112
|
+
return f'{self.BASE_OSADL_URL}/{spdxid}.txt'
|
|
113
|
+
#
|
|
114
|
+
# End of LicenseUtil Class
|
|
115
|
+
#
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
def generate_table(headers, rows, centered_columns=None):
|
|
2
|
+
"""
|
|
3
|
+
Generate Markdown table
|
|
4
|
+
:param headers: List of headers
|
|
5
|
+
:param rows: Rows
|
|
6
|
+
:param centered_columns: List with centered columns
|
|
7
|
+
"""
|
|
8
|
+
COL_SEP = ' | '
|
|
9
|
+
centered_column_set = set(centered_columns or [])
|
|
10
|
+
def create_separator(header, index):
|
|
11
|
+
if centered_columns is None:
|
|
12
|
+
return '-'
|
|
13
|
+
return ':-:' if index in centered_column_set else '-'
|
|
14
|
+
|
|
15
|
+
row_separator = COL_SEP + COL_SEP.join(
|
|
16
|
+
create_separator(header, index) for index, header in enumerate(headers)
|
|
17
|
+
) + COL_SEP
|
|
18
|
+
|
|
19
|
+
table_rows = [COL_SEP + COL_SEP.join(headers) + COL_SEP]
|
|
20
|
+
table_rows.append(row_separator)
|
|
21
|
+
table_rows.extend(COL_SEP + COL_SEP.join(row) + COL_SEP for row in rows)
|
|
22
|
+
|
|
23
|
+
return '\n'.join(table_rows)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
from scanoss.inspection.utils.license_utils import license_util
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ComponentID(Enum):
|
|
8
|
+
FILE = "file"
|
|
9
|
+
SNIPPET = "snippet"
|
|
10
|
+
DEPENDENCY = "dependency"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _append_component(components: Dict[str, Any], new_component: Dict[str, Any]) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Append a new component to the components dictionary.
|
|
16
|
+
|
|
17
|
+
This function creates a new entry in the components dictionary for the given component,
|
|
18
|
+
or updates an existing entry if the component already exists. It also processes the
|
|
19
|
+
licenses associated with the component.
|
|
20
|
+
|
|
21
|
+
:param components: The existing dictionary of components
|
|
22
|
+
:param new_component: The new component to be added or updated
|
|
23
|
+
:return: The updated components dictionary
|
|
24
|
+
"""
|
|
25
|
+
component_key = f"{new_component['purl'][0]}@{new_component['version']}"
|
|
26
|
+
components[component_key] = {
|
|
27
|
+
'purl': new_component['purl'][0],
|
|
28
|
+
'version': new_component['version'],
|
|
29
|
+
'licenses': {},
|
|
30
|
+
'status': new_component['status'],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Process licenses for this component
|
|
34
|
+
for l in new_component['licenses']:
|
|
35
|
+
spdxid = l['name']
|
|
36
|
+
components[component_key]['licenses'][spdxid] = {
|
|
37
|
+
'spdxid': spdxid,
|
|
38
|
+
'copyleft': license_util.is_copyleft(spdxid),
|
|
39
|
+
'url': l.get('url')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return components
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_components(results: Dict[str, Any]) -> list:
|
|
46
|
+
"""
|
|
47
|
+
Process the results dictionary to extract and format component information.
|
|
48
|
+
|
|
49
|
+
This function iterates through the results dictionary, identifying components from
|
|
50
|
+
different sources (files, snippets, and dependencies). It consolidates this information
|
|
51
|
+
into a list of unique components, each with its associated licenses and other details.
|
|
52
|
+
|
|
53
|
+
:param results: A dictionary containing the raw results of a component scan
|
|
54
|
+
:return: A list of dictionaries, each representing a unique component with its details
|
|
55
|
+
"""
|
|
56
|
+
components = {}
|
|
57
|
+
for component in results.values():
|
|
58
|
+
for c in component:
|
|
59
|
+
if c['id'] in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
|
|
60
|
+
component_key = f"{c['purl'][0]}@{c['version']}"
|
|
61
|
+
|
|
62
|
+
# Initialize or update the component entry
|
|
63
|
+
if component_key not in components:
|
|
64
|
+
components = _append_component(components, c)
|
|
65
|
+
|
|
66
|
+
if c['id'] == ComponentID.DEPENDENCY.value:
|
|
67
|
+
for d in c['dependencies']:
|
|
68
|
+
component_key = f"{d['purl'][0]}@{d['version']}"
|
|
69
|
+
|
|
70
|
+
if component_key not in components:
|
|
71
|
+
components = _append_component(components, d)
|
|
72
|
+
# End of for loop
|
|
73
|
+
# End if
|
|
74
|
+
# End if
|
|
75
|
+
results = list(components.values())
|
|
76
|
+
for component in results:
|
|
77
|
+
component['licenses'] = list(component['licenses'].values())
|
|
78
|
+
|
|
79
|
+
return results
|
scanoss/results.py
CHANGED
|
@@ -86,7 +86,7 @@ class Results(ScanossBase):
|
|
|
86
86
|
self.output_file = output_file
|
|
87
87
|
self.output_format = output_format
|
|
88
88
|
|
|
89
|
-
def
|
|
89
|
+
def load_file(self, file: str) -> Dict[str, Any]:
|
|
90
90
|
"""Load the JSON file
|
|
91
91
|
|
|
92
92
|
Args:
|
|
@@ -106,7 +106,7 @@ class Results(ScanossBase):
|
|
|
106
106
|
Load the file and transform the data into a list of dictionaries with the filename and the file data
|
|
107
107
|
"""
|
|
108
108
|
|
|
109
|
-
raw_data = self.
|
|
109
|
+
raw_data = self.load_file(file)
|
|
110
110
|
return self._transform_data(raw_data)
|
|
111
111
|
|
|
112
112
|
@staticmethod
|
scanoss/scanossbase.py
CHANGED
|
@@ -89,3 +89,13 @@ class ScanossBase:
|
|
|
89
89
|
f.write(msg)
|
|
90
90
|
else:
|
|
91
91
|
self.print_stdout(msg)
|
|
92
|
+
|
|
93
|
+
def print_to_file_or_stderr(self, msg: str, file: str = None):
|
|
94
|
+
"""
|
|
95
|
+
Print message to file if provided or stderr
|
|
96
|
+
"""
|
|
97
|
+
if file:
|
|
98
|
+
with open(file, "w") as f:
|
|
99
|
+
f.write(msg)
|
|
100
|
+
else:
|
|
101
|
+
self.print_stderr(msg)
|
scanoss/spdxlite.py
CHANGED
|
@@ -175,7 +175,7 @@ class SpdxLite:
|
|
|
175
175
|
# pip3 install jsonschema
|
|
176
176
|
# jsonschema -i spdxlite.json <(curl https://raw.githubusercontent.com/spdx/spdx-spec/v2.2/schemas/spdx-schema.json)
|
|
177
177
|
# Validation can also be done online here: https://tools.spdx.org/app/validate/
|
|
178
|
-
now = datetime.datetime.utcnow()
|
|
178
|
+
now = datetime.datetime.utcnow() # TODO replace with recommended format
|
|
179
179
|
md5hex = hashlib.md5(f'{raw_data}-{now}'.encode('utf-8')).hexdigest()
|
|
180
180
|
data = {
|
|
181
181
|
'spdxVersion': 'SPDX-2.2',
|
|
@@ -183,7 +183,7 @@ class SpdxLite:
|
|
|
183
183
|
'SPDXID': f'SPDXRef-{md5hex}',
|
|
184
184
|
'name': 'SCANOSS-SBOM',
|
|
185
185
|
'creationInfo': {
|
|
186
|
-
'created': now.strftime('%Y-%m-%dT%H:%M:%
|
|
186
|
+
'created': now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
187
187
|
'creators': [f'Tool: SCANOSS-PY: {__version__}', f'Person: {getpass.getuser()}']
|
|
188
188
|
},
|
|
189
189
|
'documentNamespace': f'https://spdx.org/spdxdocs/scanoss-py-{__version__}-{md5hex}',
|
|
@@ -4,22 +4,22 @@ protoc_gen_swagger/options/annotations_pb2.py,sha256=b25EDD6gssUWnFby9gxgcpLIROT
|
|
|
4
4
|
protoc_gen_swagger/options/annotations_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
|
5
5
|
protoc_gen_swagger/options/openapiv2_pb2.py,sha256=vYElGp8E1vGHszvWqX97zNG9GFJ7u2QcdK9ouq0XdyI,14939
|
|
6
6
|
protoc_gen_swagger/options/openapiv2_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
|
7
|
-
scanoss/__init__.py,sha256=
|
|
8
|
-
scanoss/cli.py,sha256=
|
|
7
|
+
scanoss/__init__.py,sha256=NqMUgZAgLLsgxOynmMFGMJ8NVk9h-smhQ0yEnPqj-WE,1163
|
|
8
|
+
scanoss/cli.py,sha256=6NlNf1PHlaWNpsDhXFXvrwdLUFiCHbPypj4eFl4rJ5U,50665
|
|
9
9
|
scanoss/components.py,sha256=ZHZ1KA69shxOASZK7USD9yPTITpAc_RXL5q5zpDK23o,12590
|
|
10
10
|
scanoss/csvoutput.py,sha256=hBwr_Fc6mBdOdXgyQcdFrockYH-PJ0jblowlExJ6OPg,9925
|
|
11
11
|
scanoss/cyclonedx.py,sha256=JVBYeR3D-i4yP9cVSyWvm0_7Y8Kr2MC5GxMgRGAf8R0,12585
|
|
12
12
|
scanoss/filecount.py,sha256=o7xb6m387ucnsU4H1OXGzf_AdWsudhAHe49T8uX4Ieo,6660
|
|
13
|
-
scanoss/results.py,sha256=
|
|
13
|
+
scanoss/results.py,sha256=7G33QAYYI9qI61TCzXjSLYXMmg5CDtZS5e2QhnQfE74,9883
|
|
14
14
|
scanoss/scancodedeps.py,sha256=_9d7MAV20-FrET7mF7gW-BZiz2eHrtwudgrEcSX0oZQ,11321
|
|
15
15
|
scanoss/scanner.py,sha256=Boxk0A-AuS0DMB4UYArU0PWZ0yJlK4v1YgdeVnKmJck,52023
|
|
16
16
|
scanoss/scanoss_settings.py,sha256=NpNZ2aCpRG2EqfJc9_BK6SnODqkOwVBEq3u-9s0KxPI,5986
|
|
17
17
|
scanoss/scanossapi.py,sha256=TJxPctr-0DTn_26LfM__OAMfntaXzvheFTbdmU-5pnM,11953
|
|
18
|
-
scanoss/scanossbase.py,sha256=
|
|
18
|
+
scanoss/scanossbase.py,sha256=zMDRCLbrcoRvYEKQRuZXnBiVY4_Vsplmg_APbB65oaU,3084
|
|
19
19
|
scanoss/scanossgrpc.py,sha256=ythZkr6F0P0hl_KPYoHkos_IL97TxLKeYfAouX_CUnM,20491
|
|
20
20
|
scanoss/scanpostprocessor.py,sha256=tfQk6GBmW1Yd2rqHHp6QKiYVdmTkBAcpoE4HHN__oKo,5899
|
|
21
21
|
scanoss/scantype.py,sha256=R2-ExLGOrYxaJFtIK2AEo2caD0XrN1zpF5q1qT9Zsyc,1326
|
|
22
|
-
scanoss/spdxlite.py,sha256=
|
|
22
|
+
scanoss/spdxlite.py,sha256=ZRcqMHutagRLX1GY7mqrzCsZernTVi5mCp6QZtGoZfY,15646
|
|
23
23
|
scanoss/threadeddependencies.py,sha256=sOIAjiPTmxybKz2yhT4-ixXBeC4K8UQVq6JQj4e8mLc,9906
|
|
24
24
|
scanoss/threadedscanning.py,sha256=T0tL8W1IEX_hLY5ksrAl_iQqtxT_KbyDhTDHo6a7xFE,9387
|
|
25
25
|
scanoss/winnowing.py,sha256=HzMWRYh1XB4so71br-DUPpV6OlmymDfsnU-EOCCObJM,18734
|
|
@@ -50,12 +50,19 @@ scanoss/api/vulnerabilities/__init__.py,sha256=FLQtiDiv85Q1Chk-sJ9ky9WOV1mulZhEK
|
|
|
50
50
|
scanoss/api/vulnerabilities/v2/__init__.py,sha256=FLQtiDiv85Q1Chk-sJ9ky9WOV1mulZhEKjiBihlwiaM,1139
|
|
51
51
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=CFhF80av8tenGvn9AIsGEtRJPuV2dC_syA5JLZb2lDw,5464
|
|
52
52
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=HlS4k4Zmx6RIAqaO9I96jD-eyF5yU6Xx04pVm7pdqOg,6864
|
|
53
|
-
scanoss/data/build_date.txt,sha256=
|
|
53
|
+
scanoss/data/build_date.txt,sha256=PiBVnmfpEupnCtyNYoAIaej2BfQh7qbXTnoD-oofinE,40
|
|
54
54
|
scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
|
|
55
55
|
scanoss/data/spdx-licenses.json,sha256=A6Z0q82gaTLtnopBfzeIVZjJFxkdRW1g2TuumQc-lII,228794
|
|
56
|
-
scanoss
|
|
57
|
-
scanoss
|
|
58
|
-
scanoss
|
|
59
|
-
scanoss
|
|
60
|
-
scanoss
|
|
61
|
-
scanoss
|
|
56
|
+
scanoss/inspection/__init__.py,sha256=z62680zKq4OmBOugSODvgpSwdsloZL7bvcaMbnx3xgU,1139
|
|
57
|
+
scanoss/inspection/copyleft.py,sha256=B150gqqEvXZXq5toYtmNCKY7M0WkooBUWYiIeu48aek,6581
|
|
58
|
+
scanoss/inspection/policy_check.py,sha256=lbbHhdcLGWgRKKahX_ljqZmMgztHStGkFZltSHjPT8Y,14156
|
|
59
|
+
scanoss/inspection/undeclared_component.py,sha256=_c5P7bsOS6X5v4tPeiewHfppHuHKlAEYc6WzvZ1K90o,6732
|
|
60
|
+
scanoss/inspection/utils/license_utils.py,sha256=iln0414t-cfmVktmAd7ANK7oOqpfRUvwRwIgrj-6GJA,5098
|
|
61
|
+
scanoss/inspection/utils/markdown_utils.py,sha256=hCa7rqBvtRoAziz3wj0gbpUOrPJIEi3pTvIUrsZf6qc,808
|
|
62
|
+
scanoss/inspection/utils/result_utils.py,sha256=OJRFznK4WCBMNvgX9kTus5WI5eTKjTXp_kWya7ixCyQ,2938
|
|
63
|
+
scanoss-1.17.1.dist-info/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
|
|
64
|
+
scanoss-1.17.1.dist-info/METADATA,sha256=_xvrlJbpBy5wMqqKIuMnwcHhtuiW5g12sTs4XYDHRWg,5936
|
|
65
|
+
scanoss-1.17.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
66
|
+
scanoss-1.17.1.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
|
|
67
|
+
scanoss-1.17.1.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
|
|
68
|
+
scanoss-1.17.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|