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.
- protoc_gen_swagger/__init__.py +13 -13
- protoc_gen_swagger/options/__init__.py +13 -13
- protoc_gen_swagger/options/annotations_pb2.py +18 -12
- protoc_gen_swagger/options/annotations_pb2.pyi +48 -0
- protoc_gen_swagger/options/annotations_pb2_grpc.py +20 -0
- protoc_gen_swagger/options/openapiv2_pb2.py +110 -99
- protoc_gen_swagger/options/openapiv2_pb2.pyi +1317 -0
- protoc_gen_swagger/options/openapiv2_pb2_grpc.py +20 -0
- scanoss/__init__.py +18 -18
- scanoss/api/__init__.py +17 -17
- scanoss/api/common/__init__.py +17 -17
- scanoss/api/common/v2/__init__.py +17 -17
- scanoss/api/common/v2/scanoss_common_pb2.py +49 -20
- scanoss/api/common/v2/scanoss_common_pb2_grpc.py +25 -0
- scanoss/api/components/__init__.py +17 -17
- scanoss/api/components/v2/__init__.py +17 -17
- scanoss/api/components/v2/scanoss_components_pb2.py +68 -43
- scanoss/api/components/v2/scanoss_components_pb2_grpc.py +83 -22
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +136 -21
- scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +766 -13
- scanoss/api/dependencies/__init__.py +17 -17
- scanoss/api/dependencies/v2/__init__.py +17 -17
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +56 -29
- scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +94 -8
- scanoss/api/geoprovenance/__init__.py +23 -0
- scanoss/api/geoprovenance/v2/__init__.py +23 -0
- scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2.py +92 -0
- scanoss/api/geoprovenance/v2/scanoss_geoprovenance_pb2_grpc.py +381 -0
- scanoss/api/licenses/__init__.py +23 -0
- scanoss/api/licenses/v2/__init__.py +23 -0
- scanoss/api/licenses/v2/scanoss_licenses_pb2.py +84 -0
- scanoss/api/licenses/v2/scanoss_licenses_pb2_grpc.py +302 -0
- scanoss/api/scanning/__init__.py +17 -17
- scanoss/api/scanning/v2/__init__.py +17 -17
- scanoss/api/scanning/v2/scanoss_scanning_pb2.py +42 -13
- scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +86 -7
- scanoss/api/semgrep/__init__.py +17 -17
- scanoss/api/semgrep/v2/__init__.py +17 -17
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +50 -23
- scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +151 -16
- scanoss/api/vulnerabilities/__init__.py +17 -17
- scanoss/api/vulnerabilities/v2/__init__.py +17 -17
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +78 -31
- scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +282 -18
- scanoss/cli.py +2359 -370
- scanoss/components.py +187 -94
- scanoss/constants.py +22 -0
- scanoss/cryptography.py +308 -0
- scanoss/csvoutput.py +91 -58
- scanoss/cyclonedx.py +221 -63
- scanoss/data/build_date.txt +1 -1
- scanoss/data/osadl-copyleft.json +133 -0
- scanoss/data/scanoss-settings-schema.json +254 -0
- scanoss/delta.py +197 -0
- scanoss/export/__init__.py +23 -0
- scanoss/export/dependency_track.py +227 -0
- scanoss/file_filters.py +582 -0
- scanoss/filecount.py +75 -69
- scanoss/gitlabqualityreport.py +214 -0
- scanoss/header_filter.py +563 -0
- scanoss/inspection/__init__.py +23 -0
- scanoss/inspection/policy_check/__init__.py +0 -0
- scanoss/inspection/policy_check/dependency_track/__init__.py +0 -0
- scanoss/inspection/policy_check/dependency_track/project_violation.py +479 -0
- scanoss/inspection/policy_check/policy_check.py +222 -0
- scanoss/inspection/policy_check/scanoss/__init__.py +0 -0
- scanoss/inspection/policy_check/scanoss/copyleft.py +243 -0
- scanoss/inspection/policy_check/scanoss/undeclared_component.py +309 -0
- scanoss/inspection/summary/__init__.py +0 -0
- scanoss/inspection/summary/component_summary.py +170 -0
- scanoss/inspection/summary/license_summary.py +191 -0
- scanoss/inspection/summary/match_summary.py +341 -0
- scanoss/inspection/utils/file_utils.py +44 -0
- scanoss/inspection/utils/license_utils.py +123 -0
- scanoss/inspection/utils/markdown_utils.py +63 -0
- scanoss/inspection/utils/scan_result_processor.py +417 -0
- scanoss/osadl.py +125 -0
- scanoss/results.py +275 -0
- scanoss/scancodedeps.py +87 -38
- scanoss/scanner.py +431 -539
- scanoss/scanners/__init__.py +23 -0
- scanoss/scanners/container_scanner.py +476 -0
- scanoss/scanners/folder_hasher.py +358 -0
- scanoss/scanners/scanner_config.py +73 -0
- scanoss/scanners/scanner_hfh.py +252 -0
- scanoss/scanoss_settings.py +337 -0
- scanoss/scanossapi.py +140 -101
- scanoss/scanossbase.py +59 -22
- scanoss/scanossgrpc.py +799 -251
- scanoss/scanpostprocessor.py +294 -0
- scanoss/scantype.py +22 -21
- scanoss/services/dependency_track_service.py +132 -0
- scanoss/spdxlite.py +532 -174
- scanoss/threadeddependencies.py +148 -47
- scanoss/threadedscanning.py +53 -37
- scanoss/utils/__init__.py +23 -0
- scanoss/utils/abstract_presenter.py +103 -0
- scanoss/utils/crc64.py +96 -0
- scanoss/utils/file.py +84 -0
- scanoss/utils/scanoss_scan_results_utils.py +41 -0
- scanoss/utils/simhash.py +198 -0
- scanoss/winnowing.py +241 -63
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/METADATA +18 -9
- scanoss-1.43.1.dist-info/RECORD +110 -0
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/WHEEL +1 -1
- scanoss-1.12.2.dist-info/RECORD +0 -58
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info}/entry_points.txt +0 -0
- {scanoss-1.12.2.dist-info → scanoss-1.43.1.dist-info/licenses}/LICENSE +0 -0
- {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
|
+
#
|