scanoss 1.12.2__py3-none-any.whl → 1.43.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.
Files changed (109) hide show
  1. protoc_gen_swagger/__init__.py +13 -13
  2. protoc_gen_swagger/options/__init__.py +13 -13
  3. protoc_gen_swagger/options/annotations_pb2.py +18 -12
  4. protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
  5. protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
  6. protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
  7. protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
  8. protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
  9. scanoss/__init__.py +18 -18
  10. scanoss/api/__init__.py +17 -17
  11. scanoss/api/common/__init__.py +17 -17
  12. scanoss/api/common/v2/__init__.py +17 -17
  13. scanoss/api/common/v2/scanoss_common_pb2.py +49 -20
  14. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
  15. scanoss/api/components/__init__.py +17 -17
  16. scanoss/api/components/v2/__init__.py +17 -17
  17. scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
  18. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
  19. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -21
  20. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +766 -13
  21. scanoss/api/dependencies/__init__.py +17 -17
  22. scanoss/api/dependencies/v2/__init__.py +17 -17
  23. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -29
  24. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +94 -8
  25. scanoss/api/geoprovenance/__init__.py +23 -0
  26. scanoss/api/geoprovenance/v2/__init__.py +23 -0
  27. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +92 -0
  28. scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +381 -0
  29. scanoss/api/licenses/__init__.py +23 -0
  30. scanoss/api/licenses/v2/__init__.py +23 -0
  31. scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
  32. scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
  33. scanoss/api/scanning/__init__.py +17 -17
  34. scanoss/api/scanning/v2/__init__.py +17 -17
  35. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +42 -13
  36. scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +86 -7
  37. scanoss/api/semgrep/__init__.py +17 -17
  38. scanoss/api/semgrep/v2/__init__.py +17 -17
  39. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
  40. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
  41. scanoss/api/vulnerabilities/__init__.py +17 -17
  42. scanoss/api/vulnerabilities/v2/__init__.py +17 -17
  43. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
  44. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
  45. scanoss/cli.py +2359 -370
  46. scanoss/components.py +187 -94
  47. scanoss/constants.py +22 -0
  48. scanoss/cryptography.py +308 -0
  49. scanoss/csvoutput.py +91 -58
  50. scanoss/cyclonedx.py +221 -63
  51. scanoss/data/build_date.txt +1 -1
  52. scanoss/data/osadl-copyleft.json +133 -0
  53. scanoss/data/scanoss-settings-schema.json +254 -0
  54. scanoss/delta.py +197 -0
  55. scanoss/export/__init__.py +23 -0
  56. scanoss/export/dependency_track.py +227 -0
  57. scanoss/file_filters.py +582 -0
  58. scanoss/filecount.py +75 -69
  59. scanoss/gitlabqualityreport.py +214 -0
  60. scanoss/header_filter.py +563 -0
  61. scanoss/inspection/__init__.py +23 -0
  62. scanoss/inspection/policy_check/__init__.py +0 -0
  63. scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
  64. scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
  65. scanoss/inspection/policy_check/policy_check.py +222 -0
  66. scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
  67. scanoss/inspection/policy_check/scanoss/copyleft.py +243 -0
  68. scanoss/inspection/policy_check/scanoss/undeclared_component.py +309 -0
  69. scanoss/inspection/summary/__init__.py +0 -0
  70. scanoss/inspection/summary/component_summary.py +170 -0
  71. scanoss/inspection/summary/license_summary.py +191 -0
  72. scanoss/inspection/summary/match_summary.py +341 -0
  73. scanoss/inspection/utils/file_utils.py +44 -0
  74. scanoss/inspection/utils/license_utils.py +123 -0
  75. scanoss/inspection/utils/markdown_utils.py +63 -0
  76. scanoss/inspection/utils/scan_result_processor.py +417 -0
  77. scanoss/osadl.py +125 -0
  78. scanoss/results.py +275 -0
  79. scanoss/scancodedeps.py +87 -38
  80. scanoss/scanner.py +431 -539
  81. scanoss/scanners/__init__.py +23 -0
  82. scanoss/scanners/container_scanner.py +476 -0
  83. scanoss/scanners/folder_hasher.py +358 -0
  84. scanoss/scanners/scanner_config.py +73 -0
  85. scanoss/scanners/scanner_hfh.py +252 -0
  86. scanoss/scanoss_settings.py +337 -0
  87. scanoss/scanossapi.py +140 -101
  88. scanoss/scanossbase.py +59 -22
  89. scanoss/scanossgrpc.py +799 -251
  90. scanoss/scanpostprocessor.py +294 -0
  91. scanoss/scantype.py +22 -21
  92. scanoss/services/dependency_track_service.py +132 -0
  93. scanoss/spdxlite.py +532 -174
  94. scanoss/threadeddependencies.py +148 -47
  95. scanoss/threadedscanning.py +53 -37
  96. scanoss/utils/__init__.py +23 -0
  97. scanoss/utils/abstract_presenter.py +103 -0
  98. scanoss/utils/crc64.py +96 -0
  99. scanoss/utils/file.py +84 -0
  100. scanoss/utils/scanoss_scan_results_utils.py +41 -0
  101. scanoss/utils/simhash.py +198 -0
  102. scanoss/winnowing.py +241 -63
  103. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/METADATA +18 -9
  104. scanoss-1.43.1.dist-info/RECORD +110 -0
  105. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/WHEEL +1 -1
  106. scanoss-1.12.2.dist-info/RECORD +0 -58
  107. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
  108. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info/licenses}/LICENSE +0 -0
  109. {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,417 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+
25
+ from enum import Enum
26
+ from typing import Any, Dict, TypeVar
27
+
28
+ from ...scanossbase import ScanossBase
29
+ from ..utils.file_utils import load_json_file
30
+ from ..utils.license_utils import LicenseUtil
31
+
32
+
33
+ class ComponentID(Enum):
34
+ """
35
+ Enumeration representing different types of software components.
36
+
37
+ Attributes:
38
+ FILE (str): Represents a file component (value: "file").
39
+ SNIPPET (str): Represents a code snippet component (value: "snippet").
40
+ DEPENDENCY (str): Represents a dependency component (value: "dependency").
41
+ """
42
+
43
+ FILE = 'file'
44
+ SNIPPET = 'snippet'
45
+ DEPENDENCY = 'dependency'
46
+
47
+
48
+ #
49
+ # End of ComponentID Class
50
+ #
51
+
52
+ T = TypeVar('T')
53
+ class ScanResultProcessor(ScanossBase):
54
+ """
55
+ A utility class for processing and transforming scan results.
56
+
57
+ This class provides functionality for processing scan results, including methods for
58
+ loading, parsing, extracting, and aggregating component and license data from scan results.
59
+ It serves as a shared data processing layer used by both policy checks and summary generators.
60
+
61
+ Inherits from:
62
+ ScanossBase: A base class providing common functionality for SCANOSS-related operations.
63
+ """
64
+
65
+ def __init__( # noqa: PLR0913
66
+ self,
67
+ debug: bool = False,
68
+ trace: bool = False,
69
+ quiet: bool = False,
70
+ result_file_path: str = None,
71
+ include: str = None,
72
+ exclude: str = None,
73
+ explicit: str = None,
74
+ license_sources: list = None,
75
+ ):
76
+ super().__init__(debug, trace, quiet)
77
+ self.result_file_path = result_file_path
78
+ self.license_util = LicenseUtil()
79
+ self.license_util.init(include, exclude, explicit)
80
+ self.license_sources = license_sources
81
+ self.results = self._load_input_file()
82
+
83
+ def get_results(self) -> Dict[str, Any]:
84
+ return self.results
85
+
86
+ def _append_component(self, components: Dict[str, Any], new_component: Dict[str, Any]) -> Dict[str, Any]:
87
+ """
88
+ Append a new component to the component dictionary.
89
+
90
+ This function creates a new entry in the component dictionary for the given component,
91
+ initializing all required counters:
92
+ - count: Total occurrences of this component (used by both license and component summaries)
93
+ - declared: Number of times this component is marked as 'identified' (used by component summary)
94
+ - undeclared: Number of times this component is marked as 'pending' (used by component summary)
95
+
96
+ Each component also contains a 'licenses' dictionary where each license entry tracks:
97
+ - count: Number of times this license appears for this component (used by license summary)
98
+
99
+ Args:
100
+ components: The existing dictionary of components
101
+ new_component: The new component to be added
102
+ Returns:
103
+ The updated components dictionary
104
+ """
105
+ match_id = new_component.get('id')
106
+ # Determine the component key and purl based on component type
107
+ if match_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
108
+ purl = new_component['purl'][0] # Take the first purl for these component types
109
+ else:
110
+ purl = new_component['purl']
111
+
112
+ if not purl:
113
+ self.print_debug(f'WARNING: _append_component: No purl found for new component: {new_component}')
114
+ return components
115
+
116
+ component_key = f'{purl}@{new_component["version"]}'
117
+ status = new_component.get('status')
118
+
119
+ if component_key in components:
120
+ # Component already exists, update component counters and try to append a new license
121
+ self._update_component_counters(components[component_key], status)
122
+ self._append_license_to_component(components, new_component, component_key)
123
+ # Maintain 'pending' status - takes precedence over 'identified'
124
+ if status == 'pending':
125
+ components[component_key]['status'] = "pending"
126
+ return components
127
+
128
+ # Create a new component
129
+ components[component_key] = {
130
+ 'purl': purl,
131
+ 'version': new_component['version'],
132
+ 'licenses': {},
133
+ 'status': status,
134
+ 'count': 1,
135
+ 'declared': 1 if status == 'identified' else 0,
136
+ 'undeclared': 1 if status == 'pending' else 0
137
+ }
138
+
139
+ ## Append license to component
140
+ self._append_license_to_component(components, new_component, component_key)
141
+ return components
142
+
143
+ def _append_license_to_component(self,
144
+ components: Dict[str, Any], new_component: Dict[str, Any], component_key: str) -> None:
145
+ """
146
+ Add or update licenses for an existing component.
147
+
148
+ For each license in the component:
149
+ - If the license already exists, increments its count
150
+ - If it's a new license, adds it with an initial count of 1
151
+
152
+ The license count is used by license_summary to track how many times each license appears
153
+ across all components. This count contributes to:
154
+ - Total number of licenses in the project
155
+ - Number of copyleft licenses when the license is marked as copyleft
156
+
157
+ Args:
158
+ components: Dictionary containing all components
159
+ new_component: Component whose licenses need to be processed
160
+ component_key: purl + version of the component to be updated
161
+ """
162
+ # If not licenses are present
163
+ if not new_component.get('licenses'):
164
+ self.print_debug(f'WARNING: Results missing licenses. Skipping: {new_component}')
165
+ return
166
+
167
+ # Select licenses based on configuration (filtering or priority mode)
168
+ selected_licenses = self._select_licenses(new_component['licenses'])
169
+
170
+ # Process licenses for this component
171
+ for license_item in selected_licenses:
172
+ if license_item.get('name'):
173
+ spdxid = license_item['name']
174
+ source = license_item.get('source')
175
+ if not source:
176
+ source = 'unknown'
177
+
178
+ if spdxid in components[component_key]['licenses']:
179
+ # If license exists, increment counter
180
+ components[component_key]['licenses'][spdxid]['count'] += 1 # Increment counter for license
181
+ else:
182
+ # If a license doesn't exist, create new entry
183
+ components[component_key]['licenses'][spdxid] = {
184
+ 'spdxid': spdxid,
185
+ 'copyleft': self.license_util.is_copyleft(spdxid),
186
+ 'url': self.license_util.get_spdx_url(spdxid),
187
+ 'source': source,
188
+ 'count': 1, # Set counter to 1 on new license
189
+ }
190
+
191
+ def _update_component_counters(self, component, status):
192
+ """Update component counters based on status."""
193
+ component['count'] += 1
194
+ if status == 'identified':
195
+ component['declared'] += 1
196
+ else:
197
+ component['undeclared'] += 1
198
+
199
+ def get_components_data(self, components: Dict[str, Any]) -> Dict[str, Any]:
200
+ """
201
+ Extract and process file and snippet components from results.
202
+
203
+ This method processes scan results to build or update component entries. For each component:
204
+
205
+ Component Counters (used by ComponentSummary):
206
+ - count: Incremented for each occurrence of the component
207
+ - declared: Incremented when component status is 'identified'
208
+ - undeclared: Incremented when component status is 'pending'
209
+
210
+ License Tracking:
211
+ - For new components, initializes license dictionary through _append_component
212
+ - For existing components, updates license counters through _append_license_to_component
213
+ which tracks the number of occurrences of each license
214
+
215
+ Args:
216
+ components: A dictionary containing the raw results of a component scan
217
+ Returns:
218
+ Updated components dictionary with file and snippet data
219
+ """
220
+ for component in self.results.values():
221
+ for c in component:
222
+ component_id = c.get('id')
223
+ if not component_id:
224
+ self.print_debug(f'WARNING: Result missing id. Skipping: {c}')
225
+ continue
226
+ ## Skip dependency
227
+ if component_id == ComponentID.DEPENDENCY.value:
228
+ continue
229
+ status = c.get('status')
230
+ if not status:
231
+ self.print_debug(f'WARNING: Result missing status. Skipping: {c}')
232
+ continue
233
+ if component_id in [ComponentID.FILE.value, ComponentID.SNIPPET.value]:
234
+ if not c.get('purl'):
235
+ self.print_debug(f'WARNING: Result missing purl. Skipping: {c}')
236
+ continue
237
+ if len(c.get('purl')) <= 0:
238
+ self.print_debug(f'WARNING: Result missing purls. Skipping: {c}')
239
+ continue
240
+ version = c.get('version')
241
+ if not version:
242
+ self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}')
243
+ version = 'unknown'
244
+ c['version'] = version #If no version exists. Set 'unknown' version to current component
245
+ # Append component
246
+ components = self._append_component(components, c)
247
+
248
+ # End component loop
249
+ # End components loop
250
+ return components
251
+
252
+ def get_dependencies_data(self,components: Dict[str, Any]) -> Dict[str, Any]:
253
+ """
254
+ Extract and process dependency components from results.
255
+ :param components: Existing components dictionary to update
256
+ :return: Updated components dictionary with dependency data
257
+ """
258
+ for component in self.results.values():
259
+ for c in component:
260
+ component_id = c.get('id')
261
+ if not component_id:
262
+ self.print_debug(f'WARNING: Result missing id. Skipping: {c}')
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 == ComponentID.DEPENDENCY.value:
269
+ if c.get('dependencies') is None:
270
+ continue
271
+ for dependency in c['dependencies']:
272
+ if not dependency.get('purl'):
273
+ self.print_debug(f'WARNING: Dependency result missing purl. Skipping: {dependency}')
274
+ continue
275
+ version = dependency.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 # Set an 'unknown' version to the current component
280
+
281
+ # Append component
282
+ components = self._append_component(components, dependency)
283
+
284
+ # End dependency loop
285
+ # End component loop
286
+ # End of result loop
287
+ return components
288
+
289
+ def _load_input_file(self):
290
+ """
291
+ Load the result.json file
292
+
293
+ Returns:
294
+ Dict[str, Any]: The parsed JSON data
295
+ """
296
+ try:
297
+ return load_json_file(self.result_file_path)
298
+ except Exception as e:
299
+ self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
300
+ return None
301
+
302
+ def convert_components_to_list(self, components: dict):
303
+ if components is None:
304
+ self.print_debug(f'WARNING: Components is empty {self.results}')
305
+ return None
306
+ results_list = list(components.values())
307
+ for component in results_list:
308
+ licenses = component.get('licenses')
309
+ if licenses is not None:
310
+ component['licenses'] = list(licenses.values())
311
+ else:
312
+ self.print_debug(f'WARNING: Licenses missing for: {component}')
313
+ component['licenses'] = []
314
+ return results_list
315
+
316
+ def _select_licenses(self, licenses_data):
317
+ """
318
+ Select licenses based on configuration.
319
+
320
+ Two modes:
321
+ - Filtering mode: If license_sources specified, filter to those sources
322
+ - Priority mode: Otherwise, use original priority-based selection
323
+
324
+ Args:
325
+ licenses_data: List of license dictionaries
326
+
327
+ Returns:
328
+ Filtered list of licenses based on configuration
329
+ """
330
+ # Filtering mode, when license_sources is explicitly provided
331
+ if self.license_sources:
332
+ sources_to_include = set(self.license_sources) | {'unknown'}
333
+ return [lic for lic in licenses_data
334
+ if lic.get('source') in sources_to_include or lic.get('source') is None]
335
+
336
+ # Define priority order (highest to lowest)
337
+ priority_sources = ['component_declared', 'license_file', 'file_header', 'scancode']
338
+
339
+ # Group licenses by source
340
+ licenses_by_source = {}
341
+ for license_item in licenses_data:
342
+
343
+ source = license_item.get('source', 'unknown')
344
+ if source not in licenses_by_source:
345
+ licenses_by_source[source] = {}
346
+
347
+ license_name = license_item.get('name')
348
+ if license_name:
349
+ # Use license name as key, store full license object as value
350
+ # If duplicate license names exist in same source, the last one wins
351
+ licenses_by_source[source][license_name] = license_item
352
+
353
+ # Find the highest priority source that has licenses
354
+ for priority_source in priority_sources:
355
+ if priority_source in licenses_by_source:
356
+ self.print_trace(f'Choosing {priority_source} as source')
357
+ return list(licenses_by_source[priority_source].values())
358
+
359
+ # If no priority sources found, combine all licenses into a single list
360
+ self.print_debug("No priority sources found, returning all licenses as list")
361
+ return licenses_data
362
+
363
+ def group_components_by_license(self,components):
364
+ """
365
+ Groups components by their unique component-license pairs.
366
+
367
+ This method processes a list of components and creates unique entries for each
368
+ component-license combination. If a component has multiple licenses, it will create
369
+ separate entries for each license.
370
+
371
+ Args:
372
+ components: A list of component dictionaries. Each component should have:
373
+ - purl: Package URL identifying the component
374
+ - licenses: List of license dictionaries, each containing:
375
+ - spdxid: SPDX identifier for the license (optional)
376
+
377
+ Returns:
378
+ list: A list of dictionaries, each containing:
379
+ - purl: The component's package URL
380
+ - license: The SPDX identifier of the license (or 'Unknown' if not provided)
381
+ """
382
+ component_licenses: dict = {}
383
+ for component in components:
384
+ purl = component.get('purl', '')
385
+ status = component.get('status', '')
386
+ licenses = component.get('licenses', [])
387
+
388
+ # Component without license
389
+ if not licenses:
390
+ key = f'{purl}-unknown'
391
+ component_licenses[key] = {
392
+ 'purl': purl,
393
+ 'spdxid': 'unknown',
394
+ 'status': status,
395
+ 'copyleft': False,
396
+ 'url': '-',
397
+ }
398
+ continue
399
+
400
+ # Iterate over licenses component licenses
401
+ for lic in licenses:
402
+ spdxid = lic.get('spdxid', 'unknown')
403
+ if spdxid not in component_licenses:
404
+ key = f'{purl}-{spdxid}'
405
+ component_licenses[key] = {
406
+ 'purl': purl,
407
+ 'spdxid': spdxid,
408
+ 'status': status,
409
+ 'copyleft': lic['copyleft'],
410
+ 'url': lic['url'],
411
+ }
412
+ return list(component_licenses.values())
413
+
414
+
415
+ #
416
+ # End of ScanResultProcessor Class
417
+ #
scanoss/osadl.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+
25
+ import json
26
+ import sys
27
+
28
+ import importlib_resources
29
+
30
+ from scanoss.scanossbase import ScanossBase
31
+
32
+
33
+ class Osadl(ScanossBase):
34
+ """
35
+ OSADL data accessor class.
36
+
37
+ Provides access to OSADL (Open Source Automation Development Lab) authoritative
38
+ checklist data for license analysis.
39
+
40
+ Data is loaded once at class level and shared across all instances for efficiency.
41
+
42
+ Data source: https://www.osadl.org/fileadmin/checklists/copyleft.json
43
+ License: CC-BY-4.0
44
+ """
45
+
46
+ _shared_copyleft_data = {}
47
+ _data_loaded = False
48
+
49
+ def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False):
50
+ """
51
+ Initialize the Osadl class.
52
+ Data is loaded once at class level and shared across all instances.
53
+ """
54
+ super().__init__(debug, trace, quiet)
55
+ self._load_copyleft_data()
56
+
57
+
58
+ def _load_copyleft_data(self) -> bool:
59
+ """
60
+ Load the embedded OSADL copyleft JSON file into class-level shared data.
61
+ Data is loaded only once and shared across all instances.
62
+
63
+ :return: True if successful, False otherwise
64
+ """
65
+ if Osadl._data_loaded:
66
+ return True
67
+
68
+ # OSADL copyleft license checklist from: https://www.osadl.org/Checklists
69
+ # Data source: https://www.osadl.org/fileadmin/checklists/copyleft.json
70
+ # License: CC-BY-4.0 (Creative Commons Attribution 4.0 International)
71
+ # Copyright: (C) 2017 - 2024 Open Source Automation Development Lab (OSADL) eG
72
+ try:
73
+ f_name = importlib_resources.files(__name__) / 'data/osadl-copyleft.json'
74
+ with importlib_resources.as_file(f_name) as f:
75
+ with open(f, 'r', encoding='utf-8') as file:
76
+ data = json.load(file)
77
+ except Exception as e:
78
+ self.print_stderr(f'ERROR: Problem loading OSADL copyleft data: {e}')
79
+ return False
80
+
81
+ # Process copyleft data
82
+ copyleft = data.get('copyleft', {})
83
+ if not copyleft:
84
+ self.print_stderr('ERROR: No copyleft data found in OSADL JSON')
85
+ return False
86
+
87
+ # Store in class-level shared dictionary
88
+ for lic_id, status in copyleft.items():
89
+ # Normalize license ID (lowercase) for consistent lookup
90
+ lic_id_lc = lic_id.lower()
91
+ Osadl._shared_copyleft_data[lic_id_lc] = status
92
+
93
+ Osadl._data_loaded = True
94
+ self.print_debug(f'Loaded {len(Osadl._shared_copyleft_data)} OSADL copyleft entries')
95
+ return True
96
+
97
+ def is_copyleft(self, spdx_id: str) -> bool:
98
+ """
99
+ Check if a license is copyleft according to OSADL data.
100
+
101
+ Returns True for both strong copyleft ("Yes") and weak/restricted copyleft ("Yes (restricted)").
102
+
103
+ :param spdx_id: SPDX license identifier
104
+ :return: True if copyleft, False otherwise
105
+ """
106
+ if not spdx_id:
107
+ self.print_debug('No license ID provided for copyleft check')
108
+ return False
109
+
110
+ # Normalize lookup
111
+ spdx_id_lc = spdx_id.lower()
112
+ # Use class-level shared data
113
+ status = Osadl._shared_copyleft_data.get(spdx_id_lc)
114
+
115
+ if not status:
116
+ self.print_debug(f'No OSADL copyleft data for license: {spdx_id}')
117
+ return False
118
+
119
+ # Consider both "Yes" and "Yes (restricted)" as copyleft (case-insensitive)
120
+ return status.lower().startswith('yes')
121
+
122
+
123
+ #
124
+ # End of Osadl Class
125
+ #