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 CHANGED
@@ -22,4 +22,4 @@
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- __version__ = "1.16.0"
25
+ __version__ = "1.17.1"
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 FAST_WINNOWING, Scanner
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='Retreive cryptographic algorithms for the given components')
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='Retreive vulnerabilities for the given components')
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='Retreive semgrep issues/findings for the given components')
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
- and not args.subparsercmd:
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'Centificate {index} - CN: {cn}')
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:
@@ -1 +1 @@
1
- date: 20241010135530, utime: 1728568530
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 _load_file(self, file: str) -> Dict[str, Any]:
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._load_file(file)
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:%S') + now.strftime('.%f')[:4] + 'Z',
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}',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scanoss
3
- Version: 1.16.0
3
+ Version: 1.17.1
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -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=0h0EuhMJeA8cpYqOnCxwfM-gtnXaB0BFmm8FwkEwnKI,1163
8
- scanoss/cli.py,sha256=k7RJVh36CwxUwDqOVHkXdArWp4QpzJVCSQ0haJWenKA,46686
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=8AJgXeZRc7Ugf4iM_NvFD8zPD2Y4NLePwNzZKUmmFi4,9885
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=ucG85doysZT3KLgApg2CiVm3-YLcdBQ2HmkZ9YnJSxA,2806
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=IsWP9o1D8ryT1_5LeobIEhWJXNFbffoWCy1yaeZY2X0,15638
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=Y087LKNPYOukJKz1qnrj5bG17xeBavkMK7KMr_uNZZk,40
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-1.16.0.dist-info/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
57
- scanoss-1.16.0.dist-info/METADATA,sha256=b9mSpB5aYU5kLfcWSjYwiAwAd1uBGI1Stw30NU0rKsY,5936
58
- scanoss-1.16.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
59
- scanoss-1.16.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
60
- scanoss-1.16.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
61
- scanoss-1.16.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5