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,309 @@
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
+ from dataclasses import dataclass
27
+ from typing import List
28
+
29
+ from ...policy_check.policy_check import PolicyCheck, PolicyOutput, PolicyStatus
30
+ from ...utils.markdown_utils import generate_jira_table, generate_table
31
+ from ...utils.scan_result_processor import ScanResultProcessor
32
+
33
+
34
+ @dataclass
35
+ class License:
36
+ spdxid: str
37
+ copyleft: bool
38
+ url: str
39
+
40
+ @dataclass
41
+ class Component:
42
+ purl: str
43
+ version: str
44
+ licenses: List[License]
45
+ status: str
46
+
47
+ class UndeclaredComponent(PolicyCheck[Component]):
48
+ """
49
+ SCANOSS UndeclaredComponent class
50
+ Inspects for undeclared components
51
+ """
52
+
53
+ def __init__( # noqa: PLR0913
54
+ self,
55
+ debug: bool = False,
56
+ trace: bool = False,
57
+ quiet: bool = False,
58
+ filepath: str = None,
59
+ format_type: str = 'json',
60
+ status: str = None,
61
+ output: str = None,
62
+ sbom_format: str = 'settings'
63
+ ):
64
+ """
65
+ Initialize the UndeclaredComponent class.
66
+
67
+ :param debug: Enable debug mode
68
+ :param trace: Enable trace mode (default True)
69
+ :param quiet: Enable quiet mode
70
+ :param filepath: Path to the file containing component data
71
+ :param format_type: Output format ('json' or 'md')
72
+ :param status: Path to save status output
73
+ :param output: Path to save detailed output
74
+ :param sbom_format: Sbom format for status output (default 'settings')
75
+ """
76
+ super().__init__(
77
+ debug, trace, quiet, format_type, status, name='Undeclared Components Policy', output=output
78
+ )
79
+ self.filepath = filepath
80
+ self.output = output
81
+ self.status = status
82
+ self.sbom_format = sbom_format
83
+ self.results_processor = ScanResultProcessor(self.debug, self.trace, self.quiet, self.filepath)
84
+
85
+
86
+ def _get_undeclared_components(self, components: list[Component]) -> list or None:
87
+ """
88
+ Filter the components list to include only undeclared components.
89
+
90
+ :param components: List of all components
91
+ :return: List of undeclared components
92
+ """
93
+ if components is None:
94
+ self.print_debug('WARNING: No components provided!')
95
+ return None
96
+ undeclared_components = []
97
+ for component in components:
98
+ if component['status'] == 'pending':
99
+ # Remove unused keys
100
+ del component['count']
101
+ del component['declared']
102
+ del component['undeclared']
103
+ for lic in component['licenses']:
104
+ lic.pop('count', None) # None is default value if key doesn't exist
105
+ lic.pop('source', None) # None is default value if key doesn't exist
106
+ undeclared_components.append(component)
107
+ # end component loop
108
+ return undeclared_components
109
+
110
+ def _get_jira_summary(self, components: list[Component]) -> str:
111
+ """
112
+ Get a summary of the undeclared components.
113
+
114
+ :param components: List of all components
115
+ :return: Component summary markdown
116
+ """
117
+
118
+ """
119
+ Get a summary of the undeclared components.
120
+
121
+ :param components: List of all components
122
+ :return: Component summary markdown
123
+ """
124
+ if len(components) > 0:
125
+ json_content = json.dumps(self._generate_scanoss_file(components), indent=2)
126
+
127
+ if self.sbom_format == 'settings':
128
+ return (
129
+ f'{len(components)} undeclared component(s) were found.\n'
130
+ f'Add the following snippet into your `scanoss.json` file\n'
131
+ f'{{code:json}}\n'
132
+ f'{json_content}\n'
133
+ f'{{code}}\n'
134
+ )
135
+ else:
136
+ return (
137
+ f'{len(components)} undeclared component(s) were found.\n'
138
+ f'Add the following snippet into your `sbom.json` file\n'
139
+ f'{{code:json}}\n'
140
+ f'{json_content}\n'
141
+ f'{{code}}\n'
142
+ )
143
+ return f'{len(components)} undeclared component(s) were found.\\n'
144
+
145
+ def _get_summary(self, components: list) -> str:
146
+ """
147
+ Get a summary of the undeclared components.
148
+
149
+ :param components: List of all components
150
+ :return: Component summary markdown
151
+ """
152
+ summary = f'{len(components)} undeclared component(s) were found.\n'
153
+ if len(components) > 0:
154
+ if self.sbom_format == 'settings':
155
+ summary += (
156
+ f'Add the following snippet into your `scanoss.json` file\n'
157
+ f'\n```json\n{json.dumps(self._generate_scanoss_file(components), indent=2)}\n```\n'
158
+ )
159
+ else:
160
+ summary += (
161
+ f'Add the following snippet into your `sbom.json` file\n'
162
+ f'\n```json\n{json.dumps(self._generate_sbom_file(components), indent=2)}\n```\n'
163
+ )
164
+
165
+ return summary
166
+
167
+ def _json(self, components: list[Component]) -> PolicyOutput:
168
+ """
169
+ Format the undeclared components as JSON.
170
+
171
+ :param components: List of undeclared components
172
+ :return: Dictionary with formatted JSON details and summary
173
+ """
174
+ # Use component grouped by licenses to generate the summary
175
+ component_licenses = self.results_processor.group_components_by_license(components)
176
+ details = {}
177
+ if len(components) > 0:
178
+ details = {'components': components}
179
+ return PolicyOutput(
180
+ details=f'{json.dumps(details, indent=2)}\n',
181
+ summary=self._get_summary(component_licenses)
182
+ )
183
+
184
+ def _markdown(self, components: list[Component]) -> PolicyOutput:
185
+ """
186
+ Format the undeclared components as Markdown.
187
+
188
+ :param components: List of undeclared components
189
+ :return: Dictionary with formatted Markdown details and summary
190
+ """
191
+ headers = ['Component', 'License']
192
+ rows = []
193
+ # TODO look at using SpdxLite license name lookup method
194
+ component_licenses = self.results_processor.group_components_by_license(components)
195
+ for component in component_licenses:
196
+ rows.append([component.get('purl'), component.get('spdxid')])
197
+ return PolicyOutput(
198
+ details= f'### Undeclared components\n{generate_table(headers, rows)}\n',
199
+ summary= self._get_summary(component_licenses),
200
+ )
201
+
202
+ def _jira_markdown(self, components: list) -> PolicyOutput:
203
+ """
204
+ Format the undeclared components as Markdown.
205
+
206
+ :param components: List of undeclared components
207
+ :return: Dictionary with formatted Markdown details and summary
208
+ """
209
+ headers = ['Component', 'License']
210
+ rows = []
211
+ # TODO look at using SpdxLite license name lookup method
212
+ component_licenses = self.results_processor.group_components_by_license(components)
213
+ for component in component_licenses:
214
+ rows.append([component.get('purl'), component.get('spdxid')])
215
+ return PolicyOutput(
216
+ details= f'{generate_jira_table(headers, rows)}',
217
+ summary= self._get_jira_summary(component_licenses),
218
+ )
219
+
220
+ def _get_unique_components(self, components: list) -> list:
221
+ """
222
+ Generate a list of unique components.
223
+
224
+ :param components: List of undeclared components
225
+ :return: list of unique components
226
+ """
227
+ unique_components = {}
228
+ if components is None:
229
+ self.print_stderr('WARNING: No components provided!')
230
+ return []
231
+
232
+ for component in components:
233
+ unique_components[component['purl']] = {'purl': component['purl']}
234
+ return list(unique_components.values())
235
+
236
+ def _generate_scanoss_file(self, components: list) -> dict:
237
+ """
238
+ Generate a list of PURLs for the scanoss.json file.
239
+
240
+ :param components: List of undeclared components
241
+ :return: scanoss.json Dictionary
242
+ """
243
+ scanoss_settings = {
244
+ 'bom': {
245
+ 'include': self._get_unique_components(components),
246
+ }
247
+ }
248
+
249
+ return scanoss_settings
250
+
251
+ def _generate_sbom_file(self, components: list) -> dict:
252
+ """
253
+ Generate a list of PURLs for the SBOM file.
254
+
255
+ :param components: List of undeclared components
256
+ :return: SBOM Dictionary with components
257
+ """
258
+ sbom = {
259
+ 'components': self._get_unique_components(components),
260
+ }
261
+
262
+ return sbom
263
+
264
+ def _get_components(self):
265
+ """
266
+ Extract and process components from file results only.
267
+
268
+ This method performs the following steps:
269
+ 1. Validates if `self.results` is loaded. Returns `None` if not loaded.
270
+ 2. Extracts file and snippet components into a dictionary.
271
+ 3. Converts the components dictionary into a list of components.
272
+ 4. Processes the licenses for each component by converting them into a list.
273
+
274
+ :return: A list of processed components with their licenses, or `None` if `self.results` is not set.
275
+ """
276
+ if self.results_processor.get_results() is None:
277
+ return None
278
+ components: dict = {}
279
+ # Extract file and snippet components
280
+ components = self.results_processor.get_components_data(components)
281
+ # Convert to list and process licenses
282
+ return self.results_processor.convert_components_to_list(components)
283
+
284
+ def run(self):
285
+ """
286
+ Run the undeclared component inspection process.
287
+
288
+ This method performs the following steps:
289
+ 1. Get all components
290
+ 2. Filter undeclared components
291
+ 3. Format the results
292
+ 4. Save the output to files if required
293
+
294
+ :return: Dictionary containing the inspection results
295
+ """
296
+ self._debug()
297
+ components = self._get_components()
298
+ if components is None:
299
+ return PolicyStatus.ERROR.value, {}
300
+ # Get an undeclared component summary (if any)
301
+ undeclared_components = self._get_undeclared_components(components)
302
+ if undeclared_components is None:
303
+ return PolicyStatus.ERROR.value, {}
304
+ self.print_debug(f'Undeclared components: {undeclared_components}')
305
+ # Format the results and save to files if required
306
+ return self._generate_formatter_report(undeclared_components)
307
+ #
308
+ # End of UndeclaredComponent Class
309
+ #
File without changes
@@ -0,0 +1,170 @@
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
+ import json
25
+ from typing import Any
26
+
27
+ from ...scanossbase import ScanossBase
28
+ from ..policy_check.policy_check import T
29
+ from ..utils.scan_result_processor import ScanResultProcessor
30
+
31
+
32
+ class ComponentSummary(ScanossBase):
33
+
34
+ def __init__( # noqa: PLR0913
35
+ self,
36
+ debug: bool = False,
37
+ trace: bool = False,
38
+ quiet: bool = False,
39
+ filepath: str = None,
40
+ format_type: str = 'json',
41
+ output: str = None,
42
+ ):
43
+ """
44
+ Initialize the ComponentSummary class.
45
+
46
+ :param debug: Enable debug mode
47
+ :param trace: Enable trace mode
48
+ :param quiet: Enable quiet mode
49
+ :param filepath: Path to the file containing component data
50
+ :param format_type: Output format ('json' or 'md')
51
+ """
52
+ super().__init__(debug, trace, quiet)
53
+ self.filepath = filepath
54
+ self.output = output
55
+ self.results_processor = ScanResultProcessor(debug, trace, quiet, filepath)
56
+
57
+
58
+ def _json(self, data: dict[str,Any]) -> dict[str,Any]:
59
+ """
60
+ Format component summary data as JSON.
61
+
62
+ This method returns the component summary data in its original JSON structure
63
+ without any transformation. The data can be directly serialized to JSON format.
64
+
65
+ :param data: Dictionary containing component summary information including:
66
+ - components: List of component-license pairs with status and metadata
67
+ - totalComponents: Total number of unique components
68
+ - undeclaredComponents: Number of components with 'pending' status
69
+ - declaredComponents: Number of components with 'identified' status
70
+ - totalFilesDetected: Total count of files where components were detected
71
+ - totalFilesUndeclared: Count of files with undeclared components
72
+ - totalFilesDeclared: Count of files with declared components
73
+ :return: The same data dictionary, ready for JSON serialization
74
+ """
75
+ return data
76
+
77
+ def _markdown(self, data: list[T]) -> dict[str, Any]:
78
+ """
79
+ Format component summary data as Markdown (not yet implemented).
80
+
81
+ This method is intended to convert component summary data into a human-readable
82
+ Markdown format with tables and formatted sections.
83
+
84
+ :param data: List of component summary items to format
85
+ :return: Dictionary containing formatted Markdown output
86
+ """
87
+ pass
88
+
89
+ def _jira_markdown(self, data: list[T]) -> dict[str, Any]:
90
+ """
91
+ Format component summary data as Jira-flavored Markdown (not yet implemented).
92
+
93
+ This method is intended to convert component summary data into Jira-compatible
94
+ Markdown format, which may include Jira-specific syntax for tables and formatting.
95
+
96
+ :param data: List of component summary items to format
97
+ :return: Dictionary containing Jira-formatted Markdown output
98
+ """
99
+ pass
100
+
101
+ def _get_component_summary_from_components(self, scan_components: list)-> dict:
102
+ """
103
+ Get a component summary from detected components.
104
+
105
+ :param scan_components: List of all components
106
+ :return: Dict with license summary information
107
+ """
108
+ # A component is considered unique by its combination of PURL (Package URL) and license
109
+ component_licenses = self.results_processor.group_components_by_license(scan_components)
110
+ total_components = len(component_licenses)
111
+ # Get undeclared components
112
+ undeclared_components = len([c for c in component_licenses if c['status'] == 'pending'])
113
+
114
+ components: list = []
115
+ total_undeclared_files = 0
116
+ total_files_detected = 0
117
+ for component in scan_components:
118
+ total_files_detected += component['count']
119
+ total_undeclared_files += component['undeclared']
120
+ components.append({
121
+ 'purl': component['purl'],
122
+ 'version': component['version'],
123
+ 'count': component['count'],
124
+ 'undeclared': component['undeclared'],
125
+ 'declared': component['count'] - component['undeclared'],
126
+ })
127
+ ## End for loop components
128
+ return {
129
+ "components": component_licenses,
130
+ 'totalComponents': total_components,
131
+ 'undeclaredComponents': undeclared_components,
132
+ 'declaredComponents': total_components - undeclared_components,
133
+ 'totalFilesDetected': total_files_detected,
134
+ 'totalFilesUndeclared': total_undeclared_files,
135
+ 'totalFilesDeclared': total_files_detected - total_undeclared_files,
136
+ }
137
+
138
+ def _get_components(self):
139
+ """
140
+ Extract and process components from results and their dependencies.
141
+
142
+ This method performs the following steps:
143
+ 1. Validates that `self.results` is loaded. Returns `None` if not.
144
+ 2. Extracts file, snippet, and dependency components into a dictionary.
145
+ 3. Converts components to a list and processes their licenses.
146
+
147
+ :return: A list of processed components with license data, or `None` if `self.results` is not set.
148
+ """
149
+ if self.results_processor.get_results() is None:
150
+ raise ValueError(f'Error: No results found in {self.filepath}')
151
+
152
+ components: dict = {}
153
+ # Extract component and license data from file and dependency results. Both helpers mutate `components`
154
+ self.results_processor.get_components_data(components)
155
+ return self.results_processor.convert_components_to_list(components)
156
+
157
+ def _format(self, component_summary) -> str:
158
+ # TODO: Implement formatter to support dynamic outputs
159
+ json_data = self._json(component_summary)
160
+ return json.dumps(json_data, indent=2)
161
+
162
+ def run(self):
163
+ components = self._get_components()
164
+ component_summary = self._get_component_summary_from_components(components)
165
+ output = self._format(component_summary)
166
+ self.print_to_file_or_stdout(output, self.output)
167
+ return component_summary
168
+ #
169
+ # End of ComponentSummary Class
170
+ #
@@ -0,0 +1,191 @@
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
+ from typing import Any
27
+
28
+ from ...scanossbase import ScanossBase
29
+ from ..policy_check.policy_check import T
30
+ from ..utils.scan_result_processor import ScanResultProcessor
31
+
32
+
33
+ class LicenseSummary(ScanossBase):
34
+ """
35
+ SCANOSS LicenseSummary class
36
+ Inspects results and generates comprehensive license summaries from detected components.
37
+
38
+ This class processes component scan results to extract, validate, and aggregate license
39
+ information, providing detailed summaries including copyleft analysis and license statistics.
40
+ """
41
+
42
+ # Define required license fields as class constants
43
+ REQUIRED_LICENSE_FIELDS = ['spdxid', 'url', 'copyleft', 'source']
44
+
45
+ def __init__( # noqa: PLR0913
46
+ self,
47
+ debug: bool = False,
48
+ trace: bool = False,
49
+ quiet: bool = False,
50
+ filepath: str = None,
51
+ status: str = None,
52
+ output: str = None,
53
+ include: str = None,
54
+ exclude: str = None,
55
+ explicit: str = None,
56
+ ):
57
+ """
58
+ Initialize the LicenseSummary class.
59
+
60
+ :param debug: Enable debug mode
61
+ :param trace: Enable trace mode
62
+ :param quiet: Enable quiet mode
63
+ :param filepath: Path to the file containing component data
64
+ :param output: Path to save detailed output
65
+ :param include: Licenses to include in the analysis
66
+ :param exclude: Licenses to exclude from the analysis
67
+ :param explicit: Explicitly defined licenses
68
+ """
69
+ super().__init__(debug=debug, trace=trace, quiet=quiet)
70
+ self.results_processor = ScanResultProcessor(debug, trace, quiet, filepath, include, exclude, explicit)
71
+ self.filepath = filepath
72
+ self.output = output
73
+ self.status = status
74
+ self.include = include
75
+ self.exclude = exclude
76
+ self.explicit = explicit
77
+
78
+ def _json(self, data: dict[str,Any]) -> dict[str, Any]:
79
+ """
80
+ Format license summary data as JSON.
81
+
82
+ This method is intended to return the license summary data in JSON structure
83
+ for serialization. The data should include license information with copyleft
84
+ analysis and license statistics.
85
+
86
+ :param data: List of license summary items to format
87
+ :return: Dictionary containing license summary information including:
88
+ - licenses: List of detected licenses with SPDX IDs, URLs, and copyleft status
89
+ - detectedLicenses: Total number of unique licenses
90
+ - detectedLicensesWithCopyleft: Count of licenses marked as copyleft
91
+ """
92
+ return data
93
+
94
+ def _markdown(self, data: list[T]) -> dict[str, Any]:
95
+ """
96
+ Format license summary data as Markdown (not yet implemented).
97
+
98
+ This method is intended to convert license summary data into a human-readable
99
+ Markdown format with tables and formatted sections.
100
+
101
+ :param data: List of license summary items to format
102
+ :return: Dictionary containing formatted Markdown output
103
+ """
104
+ pass
105
+
106
+ def _jira_markdown(self, data: list[T]) -> dict[str, Any]:
107
+ """
108
+ Format license summary data as Jira-flavored Markdown (not yet implemented).
109
+
110
+ This method is intended to convert license summary data into Jira-compatible
111
+ Markdown format, which may include Jira-specific syntax for tables and formatting.
112
+
113
+ :param data: List of license summary items to format
114
+ :return: Dictionary containing Jira-formatted Markdown output
115
+ """
116
+ pass
117
+
118
+
119
+ def _get_licenses_summary_from_components(self, components: list)-> dict:
120
+ """
121
+ Get a license summary from detected components.
122
+
123
+ :param components: List of all components
124
+ :return: Dict with license summary information
125
+ """
126
+ # A component is considered unique by its combination of PURL (Package URL) and license
127
+ component_licenses = self.results_processor.group_components_by_license(components)
128
+ license_component_count = {}
129
+ # Count license per component
130
+ for lic in component_licenses:
131
+ if lic['spdxid'] not in license_component_count:
132
+ license_component_count[lic['spdxid']] = 1
133
+ else:
134
+ license_component_count[lic['spdxid']] += 1
135
+ licenses:dict = {}
136
+ for comp_lic in component_licenses:
137
+ spdxid = comp_lic.get("spdxid")
138
+ url = comp_lic.get("url")
139
+ copyleft = comp_lic.get("copyleft")
140
+ if spdxid not in licenses:
141
+ licenses[spdxid] = {
142
+ 'spdxid': spdxid,
143
+ 'url': url,
144
+ 'copyleft': copyleft,
145
+ 'componentCount': license_component_count.get(spdxid, 0), # Append component count to license
146
+ }
147
+ ## End for loop licenses
148
+ ## End for loop components
149
+ detected_licenses = list(licenses.values())
150
+ licenses_with_copyleft = [lic for lic in detected_licenses if lic['copyleft']]
151
+ return {
152
+ 'licenses': detected_licenses,
153
+ 'detectedLicenses': len(detected_licenses), # Count unique licenses. SPDXID is considered unique
154
+ 'detectedLicensesWithCopyleft': len(licenses_with_copyleft),
155
+ }
156
+
157
+
158
+ def _get_components(self):
159
+ """
160
+ Extract and process components from results and their dependencies.
161
+
162
+ This method performs the following steps:
163
+ 1. Validates that `self.results` is loaded. Returns `None` if not.
164
+ 2. Extracts file, snippet, and dependency components into a dictionary.
165
+ 3. Converts components to a list and processes their licenses.
166
+
167
+ :return: A list of processed components with license data, or `None` if `self.results` is not set.
168
+ """
169
+ if self.results_processor.get_results() is None:
170
+ raise ValueError(f'Error: No results found in {self.filepath}')
171
+
172
+ components: dict = {}
173
+ # Extract component and license data from file and dependency results. Both helpers mutate `components`
174
+ self.results_processor.get_components_data(components)
175
+ self.results_processor.get_dependencies_data(components)
176
+ return self.results_processor.convert_components_to_list(components)
177
+
178
+ def _format(self, license_summary) -> str:
179
+ # TODO: Implement formatter to support dynamic outputs
180
+ json_data = self._json(license_summary)
181
+ return json.dumps(json_data, indent=2)
182
+
183
+ def run(self):
184
+ components = self._get_components()
185
+ license_summary = self._get_licenses_summary_from_components(components)
186
+ output = self._format(license_summary)
187
+ self.print_to_file_or_stdout(output, self.output)
188
+ return license_summary
189
+ #
190
+ # End of LicenseSummary Class
191
+ #