diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1d65c..456a5b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... +## [1.25.2] - 2025-06-18 +### Fixed +- Fixed errors when no versions are declared in scanner results for `inspect` subcommand +### Changed +- Prioritized licenses by source priority in `inspect copyleft` subcommand + ## [1.25.1] - 2025-06-12 ### Fixed - Removed dependency components from the undeclared component list in the `inspect` subcommand. @@ -534,4 +540,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.23.0]: https://github.com/scanoss/scanoss.py/compare/v1.22.0...v1.23.0 [1.24.0]: https://github.com/scanoss/scanoss.py/compare/v1.23.0...v1.24.0 [1.25.0]: https://github.com/scanoss/scanoss.py/compare/v1.24.0...v1.25.0 -[1.25.1]: https://github.com/scanoss/scanoss.py/compare/v1.25.0...v1.25.1 \ No newline at end of file +[1.25.1]: https://github.com/scanoss/scanoss.py/compare/v1.25.0...v1.25.1 +[1.25.2]: https://github.com/scanoss/scanoss.py/compare/v1.25.1...v1.25.2 \ No newline at end of file diff --git a/src/scanoss/__init__.py b/src/scanoss/__init__.py index 4f23f22..dc954aa 100644 --- a/src/scanoss/__init__.py +++ b/src/scanoss/__init__.py @@ -22,4 +22,4 @@ THE SOFTWARE. """ -__version__ = '1.25.1' +__version__ = '1.25.2' diff --git a/src/scanoss/inspection/copyleft.py b/src/scanoss/inspection/copyleft.py index 81aee21..edc1703 100644 --- a/src/scanoss/inspection/copyleft.py +++ b/src/scanoss/inspection/copyleft.py @@ -177,11 +177,7 @@ def _get_components(self): # Extract component and license data from file and dependency results. Both helpers mutate `components` self._get_components_data(self.results, components) self._get_dependencies_data(self.results, components) - # Convert to list and process licenses - results_list = list(components.values()) - for component in results_list: - component['licenses'] = list(component['licenses'].values()) - return results_list + return self._convert_components_to_list(components) def run(self): """ diff --git a/src/scanoss/inspection/policy_check.py b/src/scanoss/inspection/policy_check.py index c1e2f4c..6c5d057 100644 --- a/src/scanoss/inspection/policy_check.py +++ b/src/scanoss/inspection/policy_check.py @@ -212,6 +212,10 @@ def _append_component( else: purl = new_component['purl'] + if not purl: + self.print_debug(f'WARNING: _append_component: No purl found for new component: {new_component}') + return components + component_key = f'{purl}@{new_component["version"]}' components[component_key] = { 'purl': purl, @@ -222,14 +226,21 @@ def _append_component( if not new_component.get('licenses'): self.print_debug(f'WARNING: Results missing licenses. Skipping: {new_component}') return components + + + licenses_order_by_source_priority = self._get_licenses_order_by_source_priority(new_component['licenses']) # Process licenses for this component - for license_item in new_component['licenses']: + for license_item in licenses_order_by_source_priority: if license_item.get('name'): spdxid = license_item['name'] + source = license_item.get('source') + if not source: + source = 'unknown' components[component_key]['licenses'][spdxid] = { 'spdxid': spdxid, 'copyleft': self.license_util.is_copyleft(spdxid), 'url': self.license_util.get_spdx_url(spdxid), + 'source': source, } return components @@ -261,10 +272,12 @@ def _get_components_data(self, results: Dict[str, Any], components: Dict[str, An if len(c.get('purl')) <= 0: self.print_debug(f'WARNING: Result missing purls. Skipping: {c}') continue - if not c.get('version'): - self.print_msg(f'WARNING: Result missing version. Skipping: {c}') - continue - component_key = f'{c["purl"][0]}@{c["version"]}' + version = c.get('version') + if not version: + self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}') + version = 'unknown' + c['version'] = version #If no version exists. Set 'unknown' version to current component + component_key = f'{c["purl"][0]}@{version}' if component_key not in components: components = self._append_component(components, c, component_id, status) # End component loop @@ -296,10 +309,12 @@ def _get_dependencies_data(self, results: Dict[str, Any], components: Dict[str, if not dependency.get('purl'): self.print_debug(f'WARNING: Dependency result missing purl. Skipping: {dependency}') continue - if not dependency.get('version'): - self.print_msg(f'WARNING: Dependency result missing version. Skipping: {dependency}') - continue - component_key = f'{dependency["purl"]}@{dependency["version"]}' + version = c.get('version') + if not version: + self.print_debug(f'WARNING: Result missing version. Setting it to unknown: {c}') + version = 'unknown' + c['version'] = version # If no version exists. Set 'unknown' version to current component + component_key = f'{dependency["purl"]}@{version}' if component_key not in components: components = self._append_component(components, dependency, component_id, status) # End dependency loop @@ -411,6 +426,61 @@ def _load_input_file(self): self.print_stderr(f'ERROR: Problem parsing input JSON: {e}') return None + def _convert_components_to_list(self, components: dict): + if components is None: + self.print_debug(f'WARNING: Components is empty {self.results}') + return None + results_list = list(components.values()) + for component in results_list: + licenses = component.get('licenses') + if licenses is not None: + component['licenses'] = list(licenses.values()) + else: + self.print_debug(f'WARNING: Licenses missing for: {component}') + component['licenses'] = [] + return results_list + + def _get_licenses_order_by_source_priority(self,licenses_data): + """ + Select licenses based on source priority: + 1. component_declared (highest priority) + 2. license_file + 3. file_header + 4. scancode (lowest priority) + + If any high-priority source is found, return only licenses from that source. + If none found, return all licenses. + + Returns: list with ordered licenses by source. + """ + # Define priority order (highest to lowest) + priority_sources = ['component_declared', 'license_file', 'file_header', 'scancode'] + + # Group licenses by source + licenses_by_source = {} + for license_item in licenses_data: + + source = license_item.get('source', 'unknown') + if source not in licenses_by_source: + licenses_by_source[source] = {} + + license_name = license_item.get('name') + if license_name: + # Use license name as key, store full license object as value + # If duplicate license names exist in same source, the last one wins + licenses_by_source[source][license_name] = license_item + + # Find the highest priority source that has licenses + for priority_source in priority_sources: + if priority_source in licenses_by_source: + self.print_trace(f'Choosing {priority_source} as source') + return list(licenses_by_source[priority_source].values()) + + # If no priority sources found, combine all licenses into a single list + self.print_debug("No priority sources found, returning all licenses as list") + return licenses_data + + # # End of PolicyCheck Class # diff --git a/src/scanoss/inspection/undeclared_component.py b/src/scanoss/inspection/undeclared_component.py index 865f276..f0c174f 100644 --- a/src/scanoss/inspection/undeclared_component.py +++ b/src/scanoss/inspection/undeclared_component.py @@ -254,10 +254,7 @@ def _get_components(self): # Extract file and snippet components components = self._get_components_data(self.results, components) # Convert to list and process licenses - results_list = list(components.values()) - for component in results_list: - component['licenses'] = list(component['licenses'].values()) - return results_list + return self._convert_components_to_list(components) def run(self): """ diff --git a/tests/test_policy_inspect.py b/tests/test_policy_inspect.py index 50792a4..771a7aa 100644 --- a/tests/test_policy_inspect.py +++ b/tests/test_policy_inspect.py @@ -114,7 +114,7 @@ def test_copyleft_policy_explicit(self): copyleft = Copyleft(filepath=input_file_name, format_type='json', explicit='MIT') status, results = copyleft.run() details = json.loads(results['details']) - self.assertEqual(len(details['components']), 3) + self.assertEqual(len(details['components']), 2) self.assertEqual(status, 0) """ @@ -144,11 +144,10 @@ def test_copyleft_policy_markdown(self): expected_detail_output = ( '### Copyleft licenses \n | Component | Version | License | URL | Copyleft |\n' ' | - | :-: | - | - | :-: |\n' - '| pkg:github/scanoss/engine | 4.0.4 | MIT | https://spdx.org/licenses/MIT.html | YES | \n' ' | pkg:npm/%40electron/rebuild | 3.7.0 | MIT | https://spdx.org/licenses/MIT.html | YES |\n' '| pkg:npm/%40emotion/react | 11.13.3 | MIT | https://spdx.org/licenses/MIT.html | YES | \n' ) - expected_summary_output = '3 component(s) with copyleft licenses were found.\n' + expected_summary_output = '2 component(s) with copyleft licenses were found.\n' self.assertEqual( re.sub(r'\s|\\(?!`)|\\(?=`)', '', results['details']), re.sub(r'\s|\\(?!`)|\\(?=`)', '', expected_detail_output), @@ -214,9 +213,9 @@ def test_undeclared_policy_markdown(self): expected_details_output = """ ### Undeclared components | Component | Version | License | | - | - | - | - | pkg:github/scanoss/scanner.c | 1.3.3 | BSD-2-Clause - GPL-2.0-only | + | pkg:github/scanoss/scanner.c | 1.3.3 | GPL-2.0-only | | pkg:github/scanoss/scanner.c | 1.1.4 | GPL-2.0-only | - | pkg:github/scanoss/wfp | 6afc1f6 | Zlib - GPL-2.0-only | """ + | pkg:github/scanoss/wfp | 6afc1f6 | GPL-2.0-only | """ expected_summary_output = """3 undeclared component(s) were found. Add the following snippet into your `sbom.json` file @@ -257,9 +256,9 @@ def test_undeclared_policy_markdown_scanoss_summary(self): expected_details_output = """ ### Undeclared components | Component | Version | License | | - | - | - | - | pkg:github/scanoss/scanner.c | 1.3.3 | BSD-2-Clause - GPL-2.0-only | + | pkg:github/scanoss/scanner.c | 1.3.3 | GPL-2.0-only | | pkg:github/scanoss/scanner.c | 1.1.4 | GPL-2.0-only | - | pkg:github/scanoss/wfp | 6afc1f6 | Zlib - GPL-2.0-only | """ + | pkg:github/scanoss/wfp | 6afc1f6 | GPL-2.0-only | """ expected_summary_output = """3 undeclared component(s) were found. Add the following snippet into your `scanoss.json` file @@ -332,9 +331,9 @@ def test_undeclared_policy_jira_markdown_output(self): details = results['details'] summary = results['summary'] expected_details_output = """|*Component*|*Version*|*License*| -|pkg:github/scanoss/scanner.c|1.3.3|BSD-2-Clause - GPL-2.0-only| +|pkg:github/scanoss/scanner.c|1.3.3|GPL-2.0-only| |pkg:github/scanoss/scanner.c|1.1.4|GPL-2.0-only| -|pkg:github/scanoss/wfp|6afc1f6|Zlib - GPL-2.0-only| +|pkg:github/scanoss/wfp|6afc1f6|GPL-2.0-only| """ expected_summary_output = """3 undeclared component(s) were found. Add the following snippet into your `scanoss.json` file