scanoss 1.25.2__py3-none-any.whl → 1.26.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.
@@ -1,7 +1,7 @@
1
1
  """
2
2
  SPDX-License-Identifier: MIT
3
3
 
4
- Copyright (c) 2024, SCANOSS
4
+ Copyright (c) 2025, SCANOSS
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,13 +22,11 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- import json
26
- import os.path
27
25
  from abc import abstractmethod
28
26
  from enum import Enum
29
27
  from typing import Any, Callable, Dict, List
30
28
 
31
- from ..scanossbase import ScanossBase
29
+ from .inspect_base import InspectBase
32
30
  from .utils.license_utils import LicenseUtil
33
31
 
34
32
 
@@ -45,34 +43,11 @@ class PolicyStatus(Enum):
45
43
  SUCCESS = 0
46
44
  FAIL = 1
47
45
  ERROR = 2
48
-
49
-
50
46
  #
51
47
  # End of PolicyStatus Class
52
48
  #
53
49
 
54
-
55
- class ComponentID(Enum):
56
- """
57
- Enumeration representing different types of software components.
58
-
59
- Attributes:
60
- FILE (str): Represents a file component (value: "file").
61
- SNIPPET (str): Represents a code snippet component (value: "snippet").
62
- DEPENDENCY (str): Represents a dependency component (value: "dependency").
63
- """
64
-
65
- FILE = 'file'
66
- SNIPPET = 'snippet'
67
- DEPENDENCY = 'dependency'
68
-
69
-
70
- #
71
- # End of ComponentID Class
72
- #
73
-
74
-
75
- class PolicyCheck(ScanossBase):
50
+ class PolicyCheck(InspectBase):
76
51
  """
77
52
  A base class for implementing various software policy checks.
78
53
 
@@ -83,27 +58,24 @@ class PolicyCheck(ScanossBase):
83
58
  VALID_FORMATS (set): A set of valid output formats ('md', 'json').
84
59
 
85
60
  Inherits from:
86
- ScanossBase: A base class providing common functionality for SCANOSS-related operations.
61
+ InspectBase: A base class providing common functionality for SCANOSS-related operations.
87
62
  """
88
-
89
63
  VALID_FORMATS = {'md', 'json', 'jira_md'}
90
-
91
- def __init__( # noqa: PLR0913
92
- self,
93
- debug: bool = False,
94
- trace: bool = True,
95
- quiet: bool = False,
96
- filepath: str = None,
97
- format_type: str = None,
98
- status: str = None,
99
- output: str = None,
100
- name: str = None,
64
+ def __init__( # noqa: PLR0913
65
+ self,
66
+ debug: bool = False,
67
+ trace: bool = True,
68
+ quiet: bool = False,
69
+ filepath: str = None,
70
+ format_type: str = None,
71
+ status: str = None,
72
+ output: str = None,
73
+ name: str = None,
101
74
  ):
102
- super().__init__(debug, trace, quiet)
75
+ super().__init__(debug, trace, quiet, filepath, output)
103
76
  self.license_util = LicenseUtil()
104
77
  self.filepath = filepath
105
78
  self.name = name
106
- self.output = output
107
79
  self.format_type = format_type
108
80
  self.status = status
109
81
  self.results = self._load_input_file()
@@ -166,162 +138,6 @@ class PolicyCheck(ScanossBase):
166
138
  """
167
139
  pass
168
140
 
169
- @abstractmethod
170
- def _get_components(self):
171
- """
172
- Retrieve and process components from the preloaded results.
173
-
174
- This method performs the following steps:
175
- 1. Checks if the results have been previously loaded (self.results).
176
- 2. Extracts and processes components from the loaded results.
177
-
178
- :return: A list of processed components, or None if an error occurred during any step.
179
-
180
- Possible reasons for returning None include:
181
- - Results not loaded (self.results is None)
182
- - Failure to extract components from the results
183
-
184
- Note:
185
- - This method assumes that the results have been previously loaded and stored in self.results.
186
- - Implementations must extract components (e.g. via `_get_components_data`,
187
- `_get_dependencies_data`, or other helpers).
188
- - If `self.results` is `None`, simply return `None`.
189
- """
190
- pass
191
-
192
-
193
- def _append_component(
194
- self, components: Dict[str, Any], new_component: Dict[str, Any], id: str, status: str
195
- ) -> Dict[str, Any]:
196
- """
197
- Append a new component to the component's dictionary.
198
-
199
- This function creates a new entry in the components dictionary for the given component,
200
- or updates an existing entry if the component already exists. It also processes the
201
- licenses associated with the component.
202
-
203
- :param components: The existing dictionary of components
204
- :param new_component: The new component to be added or updated
205
- :param id: The new component ID
206
- :param status: The new component status
207
- :return: The updated components dictionary
208
- """
209
- # Determine the component key and purl based on component type
210
- if id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
211
- purl = new_component['purl'][0] # Take the first purl for these component types
212
- else:
213
- purl = new_component['purl']
214
-
215
- if not purl:
216
- self.print_debug(f'WARNING: _append_component: No purl found for new component: {new_component}')
217
- return components
218
-
219
- component_key = f'{purl}@{new_component["version"]}'
220
- components[component_key] = {
221
- 'purl': purl,
222
- 'version': new_component['version'],
223
- 'licenses': {},
224
- 'status': status,
225
- }
226
- if not new_component.get('licenses'):
227
- self.print_debug(f'WARNING: Results missing licenses. Skipping: {new_component}')
228
- return components
229
-
230
-
231
- licenses_order_by_source_priority = self._get_licenses_order_by_source_priority(new_component['licenses'])
232
- # Process licenses for this component
233
- for license_item in licenses_order_by_source_priority:
234
- if license_item.get('name'):
235
- spdxid = license_item['name']
236
- source = license_item.get('source')
237
- if not source:
238
- source = 'unknown'
239
- components[component_key]['licenses'][spdxid] = {
240
- 'spdxid': spdxid,
241
- 'copyleft': self.license_util.is_copyleft(spdxid),
242
- 'url': self.license_util.get_spdx_url(spdxid),
243
- 'source': source,
244
- }
245
- return components
246
-
247
- def _get_components_data(self, results: Dict[str, Any], components: Dict[str, Any]) -> Dict[str, Any]:
248
- """
249
- Extract and process file and snippet components from results.
250
-
251
- :param results: A dictionary containing the raw results of a component scan
252
- :param components: Existing components dictionary to update
253
- :return: Updated components dictionary with file and snippet data
254
- """
255
- for component in results.values():
256
- for c in component:
257
- component_id = c.get('id')
258
- if not component_id:
259
- self.print_debug(f'WARNING: Result missing id. Skipping: {c}')
260
- continue
261
- ## Skip dependency
262
- if component_id == ComponentID.DEPENDENCY.value:
263
- continue
264
- status = c.get('status')
265
- if not status:
266
- self.print_debug(f'WARNING: Result missing status. Skipping: {c}')
267
- continue
268
- if component_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
269
- if not c.get('purl'):
270
- self.print_debug(f'WARNING: Result missing purl. Skipping: {c}')
271
- continue
272
- if len(c.get('purl')) <= 0:
273
- self.print_debug(f'WARNING: Result missing purls. Skipping: {c}')
274
- continue
275
- version = c.get('version')
276
- if not version:
277
- self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}')
278
- version = 'unknown'
279
- c['version'] = version #If no version exists. Set 'unknown' version to current component
280
- component_key = f'{c["purl"][0]}@{version}'
281
- if component_key not in components:
282
- components = self._append_component(components, c, component_id, status)
283
- # End component loop
284
- # End components loop
285
- return components
286
-
287
- def _get_dependencies_data(self, results: Dict[str, Any], components: Dict[str, Any]) -> Dict[str, Any]:
288
- """
289
- Extract and process dependency components from results.
290
-
291
- :param results: A dictionary containing the raw results of a component scan
292
- :param components: Existing components dictionary to update
293
- :return: Updated components dictionary with dependency data
294
- """
295
- for component in results.values():
296
- for c in component:
297
- component_id = c.get('id')
298
- if not component_id:
299
- self.print_debug(f'WARNING: Result missing id. Skipping: {c}')
300
- continue
301
- status = c.get('status')
302
- if not status:
303
- self.print_debug(f'WARNING: Result missing status. Skipping: {c}')
304
- continue
305
- if component_id == ComponentID.DEPENDENCY.value:
306
- if c.get('dependencies') is None:
307
- continue
308
- for dependency in c['dependencies']:
309
- if not dependency.get('purl'):
310
- self.print_debug(f'WARNING: Dependency result missing purl. Skipping: {dependency}')
311
- continue
312
- version = c.get('version')
313
- if not version:
314
- self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}')
315
- version = 'unknown'
316
- c['version'] = version # If no version exists. Set 'unknown' version to current component
317
- component_key = f'{dependency["purl"]}@{version}'
318
- if component_key not in components:
319
- components = self._append_component(components, dependency, component_id, status)
320
- # End dependency loop
321
- # End component loop
322
- # End of result loop
323
- return components
324
-
325
141
  def generate_table(self, headers, rows, centered_columns=None):
326
142
  """
327
143
  Generate a Markdown table.
@@ -408,79 +224,6 @@ class PolicyCheck(ScanossBase):
408
224
  self.print_stderr(f'ERROR: Invalid format "{self.format_type}". Valid formats are: {valid_formats_str}')
409
225
  return False
410
226
  return True
411
-
412
- def _load_input_file(self):
413
- """
414
- Load the result.json file
415
-
416
- Returns:
417
- Dict[str, Any]: The parsed JSON data
418
- """
419
- if not os.path.exists(self.filepath):
420
- self.print_stderr(f'ERROR: The file "{self.filepath}" does not exist.')
421
- return None
422
- with open(self.filepath, 'r') as jsonfile:
423
- try:
424
- return json.load(jsonfile)
425
- except Exception as e:
426
- self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
427
- return None
428
-
429
- def _convert_components_to_list(self, components: dict):
430
- if components is None:
431
- self.print_debug(f'WARNING: Components is empty {self.results}')
432
- return None
433
- results_list = list(components.values())
434
- for component in results_list:
435
- licenses = component.get('licenses')
436
- if licenses is not None:
437
- component['licenses'] = list(licenses.values())
438
- else:
439
- self.print_debug(f'WARNING: Licenses missing for: {component}')
440
- component['licenses'] = []
441
- return results_list
442
-
443
- def _get_licenses_order_by_source_priority(self,licenses_data):
444
- """
445
- Select licenses based on source priority:
446
- 1. component_declared (highest priority)
447
- 2. license_file
448
- 3. file_header
449
- 4. scancode (lowest priority)
450
-
451
- If any high-priority source is found, return only licenses from that source.
452
- If none found, return all licenses.
453
-
454
- Returns: list with ordered licenses by source.
455
- """
456
- # Define priority order (highest to lowest)
457
- priority_sources = ['component_declared', 'license_file', 'file_header', 'scancode']
458
-
459
- # Group licenses by source
460
- licenses_by_source = {}
461
- for license_item in licenses_data:
462
-
463
- source = license_item.get('source', 'unknown')
464
- if source not in licenses_by_source:
465
- licenses_by_source[source] = {}
466
-
467
- license_name = license_item.get('name')
468
- if license_name:
469
- # Use license name as key, store full license object as value
470
- # If duplicate license names exist in same source, the last one wins
471
- licenses_by_source[source][license_name] = license_item
472
-
473
- # Find the highest priority source that has licenses
474
- for priority_source in priority_sources:
475
- if priority_source in licenses_by_source:
476
- self.print_trace(f'Choosing {priority_source} as source')
477
- return list(licenses_by_source[priority_source].values())
478
-
479
- # If no priority sources found, combine all licenses into a single list
480
- self.print_debug("No priority sources found, returning all licenses as list")
481
- return licenses_data
482
-
483
-
484
227
  #
485
228
  # End of PolicyCheck Class
486
- #
229
+ #
@@ -1,7 +1,7 @@
1
1
  """
2
2
  SPDX-License-Identifier: MIT
3
3
 
4
- Copyright (c) 2024, SCANOSS
4
+ Copyright (c) 2025, SCANOSS
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -79,7 +79,13 @@ class UndeclaredComponent(PolicyCheck):
79
79
  undeclared_components = []
80
80
  for component in components:
81
81
  if component['status'] == 'pending':
82
- del component['status']
82
+ # Remove unused keys
83
+ del component['count']
84
+ del component['declared']
85
+ del component['undeclared']
86
+ for lic in component['licenses']:
87
+ lic.pop('count', None) # None is default value if key doesn't exist
88
+ lic.pop('source', None) # None is default value if key doesn't exist
83
89
  undeclared_components.append(component)
84
90
  # end component loop
85
91
  return undeclared_components
@@ -148,12 +154,14 @@ class UndeclaredComponent(PolicyCheck):
148
154
  :param components: List of undeclared components
149
155
  :return: Dictionary with formatted JSON details and summary
150
156
  """
157
+ # Use component grouped by licenses to generate the summary
158
+ component_licenses = self._group_components_by_license(components)
151
159
  details = {}
152
160
  if len(components) > 0:
153
161
  details = {'components': components}
154
162
  return {
155
163
  'details': f'{json.dumps(details, indent=2)}\n',
156
- 'summary': self._get_summary(components),
164
+ 'summary': self._get_summary(component_licenses),
157
165
  }
158
166
 
159
167
  def _markdown(self, components: list) -> Dict[str, Any]:
@@ -163,15 +171,15 @@ class UndeclaredComponent(PolicyCheck):
163
171
  :param components: List of undeclared components
164
172
  :return: Dictionary with formatted Markdown details and summary
165
173
  """
166
- headers = ['Component', 'Version', 'License']
174
+ headers = ['Component', 'License']
167
175
  rows: [[]] = []
168
176
  # TODO look at using SpdxLite license name lookup method
169
- for component in components:
170
- licenses = ' - '.join(lic.get('spdxid', 'Unknown') for lic in component['licenses'])
171
- rows.append([component['purl'], component['version'], licenses])
177
+ component_licenses = self._group_components_by_license(components)
178
+ for component in component_licenses:
179
+ rows.append([component.get('purl'), component.get('spdxid')])
172
180
  return {
173
181
  'details': f'### Undeclared components\n{self.generate_table(headers, rows)}\n',
174
- 'summary': self._get_summary(components),
182
+ 'summary': self._get_summary(component_licenses),
175
183
  }
176
184
 
177
185
  def _jira_markdown(self, components: list) -> Dict[str, Any]:
@@ -181,15 +189,15 @@ class UndeclaredComponent(PolicyCheck):
181
189
  :param components: List of undeclared components
182
190
  :return: Dictionary with formatted Markdown details and summary
183
191
  """
184
- headers = ['Component', 'Version', 'License']
192
+ headers = ['Component', 'License']
185
193
  rows: [[]] = []
186
194
  # TODO look at using SpdxLite license name lookup method
187
- for component in components:
188
- licenses = ' - '.join(lic.get('spdxid', 'Unknown') for lic in component['licenses'])
189
- rows.append([component['purl'], component['version'], licenses])
195
+ component_licenses = self._group_components_by_license(components)
196
+ for component in component_licenses:
197
+ rows.append([component.get('purl'), component.get('spdxid')])
190
198
  return {
191
199
  'details': f'{self.generate_jira_table(headers, rows)}',
192
- 'summary': self._get_jira_summary(components),
200
+ 'summary': self._get_jira_summary(component_licenses),
193
201
  }
194
202
 
195
203
  def _get_unique_components(self, components: list) -> list:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanoss
3
- Version: 1.25.2
3
+ Version: 1.26.1
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -4,8 +4,8 @@ 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=9K1SKldg4taEpZ7t4H-Z45hI8SIqMlSmjp6TFz3Km2w,1146
8
- scanoss/cli.py,sha256=SAB0xuHjEEw20YtYAPSZwrQaVf4JUsm8NRcVArMzd4U,69099
7
+ scanoss/__init__.py,sha256=qgYL2puYvasR_9Z1kUia9Cf_aPv7lHwJ923ZunoX9tc,1146
8
+ scanoss/cli.py,sha256=yjK4oawNzecarQYYlkElOiHFDDAZx_zKdSXf_gQvqXk,72678
9
9
  scanoss/components.py,sha256=b0R9DdKuXqyQiw5nZZwjQ6NJXBr1U9gyx1RI2FP9ozA,14511
10
10
  scanoss/constants.py,sha256=FWCZG8gQputKwV7XwvW1GuwDXL4wDLQyVRGdwygg578,320
11
11
  scanoss/cryptography.py,sha256=Q39MOCscP-OFvrnPXaPOMFFkc8OKnf3mC3SgZYEtCog,9407
@@ -57,14 +57,17 @@ scanoss/api/vulnerabilities/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSC
57
57
  scanoss/api/vulnerabilities/v2/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSCHhIDMJT4r0,1122
58
58
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=CFhF80av8tenGvn9AIsGEtRJPuV2dC_syA5JLZb2lDw,5464
59
59
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=HlS4k4Zmx6RIAqaO9I96jD-eyF5yU6Xx04pVm7pdqOg,6864
60
- scanoss/data/build_date.txt,sha256=3-u7TU-KPaW2lJLuHTSTYQJlq8aqKlVQv7MRd_B-xvQ,40
60
+ scanoss/data/build_date.txt,sha256=T_n0TdmNDPNRIny3af0dDhOvzk8oLLMPv0gC4sFQngs,40
61
61
  scanoss/data/scanoss-settings-schema.json,sha256=ClkRYAkjAN0Sk704G8BE_Ok006oQ6YnIGmX84CF8h9w,8798
62
62
  scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
63
63
  scanoss/data/spdx-licenses.json,sha256=A6Z0q82gaTLtnopBfzeIVZjJFxkdRW1g2TuumQc-lII,228794
64
- scanoss/inspection/__init__.py,sha256=0hjb5ktavp7utJzFhGMPImPaZiHWgilM2HwvTp5lXJE,1122
65
- scanoss/inspection/copyleft.py,sha256=MqkixiGnOs7IEfhSmX5zRkJmmouHc76q8LP2Rqdu4AQ,8495
66
- scanoss/inspection/policy_check.py,sha256=k8v7ei3oZBERt4l8S3AWEVn-qA2TsGkfYJT_i21M0s4,19076
67
- scanoss/inspection/undeclared_component.py,sha256=my-KPEeFx7P2VG2dsYm5-7y83DDMGzf0MnBQdPExIHk,11026
64
+ scanoss/inspection/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
65
+ scanoss/inspection/component_summary.py,sha256=h1l3rF6NnoK0wMkS4ib6rDfcza2aqunyoMDbN2lw2G4,4049
66
+ scanoss/inspection/copyleft.py,sha256=iCArNWZo5TOX7K68e-YD_6mQNRDOE9V35UoSMs23_qY,9236
67
+ scanoss/inspection/inspect_base.py,sha256=KEyYSkAhx4lBjZk-NeGivj2UCToxdcJc1OmGSbk0MZc,17638
68
+ scanoss/inspection/license_summary.py,sha256=T3I8E6ljqYF0ngIcY3Ke2WaNeCzrdpN0RQ02RG_3Thk,5763
69
+ scanoss/inspection/policy_check.py,sha256=NS39dvePZbpusGRnEUKN9JFiMdSyva0msFE6On6bK8Q,8329
70
+ scanoss/inspection/undeclared_component.py,sha256=qjc30agrIXU_07CJCgLvvFT-sQvJa1Hb0lS1UjM6aj8,11495
68
71
  scanoss/inspection/utils/license_utils.py,sha256=Zb6QLmVJb86lKCwZyBsmwakyAtY1SXa54kUyyKmWMqA,5093
69
72
  scanoss/scanners/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
70
73
  scanoss/scanners/container_scanner.py,sha256=leP4roes6B9B95F49mJ0P_F8WcKCQkvJgk9azWyJrjg,16294
@@ -76,9 +79,9 @@ scanoss/utils/abstract_presenter.py,sha256=teiDTxBj5jBMCk2T8i4l1BJPf_u4zBLWrtCTF
76
79
  scanoss/utils/crc64.py,sha256=TMrwQimSdE6imhFOUL7oAG6Kxu-8qMpGWMuMg8QpSVs,3169
77
80
  scanoss/utils/file.py,sha256=62cA9a17TU9ZvfA3FY5HY4-QOajJeSrc8S6xLA_f-3M,2980
78
81
  scanoss/utils/simhash.py,sha256=6iu8DOcecPAY36SZjCOzrrLMT9oIE7-gI6QuYwUQ7B0,5793
79
- scanoss-1.25.2.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
80
- scanoss-1.25.2.dist-info/METADATA,sha256=tLuNgHCCKBeUfqk0EWRkr5iv35GBIybi0nMBSBi1huw,6060
81
- scanoss-1.25.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- scanoss-1.25.2.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
83
- scanoss-1.25.2.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
84
- scanoss-1.25.2.dist-info/RECORD,,
82
+ scanoss-1.26.1.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
83
+ scanoss-1.26.1.dist-info/METADATA,sha256=55vVk3xfgQlKqi7B7WnaUJCbhmbyBmf1zrQ2YzBsVmo,6060
84
+ scanoss-1.26.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ scanoss-1.26.1.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
86
+ scanoss-1.26.1.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
87
+ scanoss-1.26.1.dist-info/RECORD,,