From f1ebbed05a130e3c271c2058c3a5793a67e4f069 Mon Sep 17 00:00:00 2001 From: Alex Egan Date: Tue, 19 Aug 2025 12:03:05 +0100 Subject: [PATCH 1/4] Added handling for empty results files --- CHANGELOG.md | 5 ++++ src/scanoss/csvoutput.py | 9 +++++-- src/scanoss/cyclonedx.py | 11 +++++--- src/scanoss/export/dependency_track.py | 7 +++++- .../dependency_track/project_violation.py | 25 ++++++++++++++++--- .../services/dependency_track_service.py | 1 + src/scanoss/spdxlite.py | 9 +++++-- 7 files changed, 55 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c6af6ab..a422aa7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... +## [1.31.3] - 2025-08-19 +### Fixed +- Added handling for empty results files + ## [1.31.2] - 2025-08-12 ### Fixed - Removed an unnecessary print statement from the policy checker @@ -638,3 +642,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.31.0]: https://github.com/scanoss/scanoss.py/compare/v1.30.0...v1.31.0 [1.31.1]: https://github.com/scanoss/scanoss.py/compare/v1.31.0...v1.31.1 [1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.1...v1.31.2 +[1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.2...v1.31.3 diff --git a/src/scanoss/csvoutput.py b/src/scanoss/csvoutput.py index 5b36284c..b4560874 100644 --- a/src/scanoss/csvoutput.py +++ b/src/scanoss/csvoutput.py @@ -50,9 +50,12 @@ def parse(self, data: json): :param data: json - JSON object :return: CSV dictionary """ - if not data: + if data is None: self.print_stderr('ERROR: No JSON data provided to parse.') return None + if len(data) == 0: + self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.') + return [] self.print_debug(f'Processing raw results into CSV format...') csv_dict = [] row_id = 1 @@ -183,9 +186,11 @@ def produce_from_json(self, data: json, output_file: str = None) -> bool: :return: True if successful, False otherwise """ csv_data = self.parse(data) - if not csv_data: + if csv_data is None: self.print_stderr('ERROR: No CSV data returned for the JSON string provided.') return False + if len(csv_data) == 0: + self.print_msg('Warning: Empty scan results - generating CSV with headers only.') # Header row/column details fields = [ 'inventory_id', diff --git a/src/scanoss/cyclonedx.py b/src/scanoss/cyclonedx.py index d6ad389a..f3685a97 100644 --- a/src/scanoss/cyclonedx.py +++ b/src/scanoss/cyclonedx.py @@ -57,9 +57,12 @@ def parse(self, data: dict): # noqa: PLR0912, PLR0915 :param data: dict - JSON object :return: CycloneDX dictionary, and vulnerability dictionary """ - if not data: + if data is None: self.print_stderr('ERROR: No JSON data provided to parse.') return None, None + if len(data) == 0: + self.print_msg('Warning: Empty scan results provided. Returning empty component dictionary.') + return {}, {} self.print_debug('Processing raw results into CycloneDX format...') cdx = {} vdx = {} @@ -186,9 +189,11 @@ def produce_from_json(self, data: dict, output_file: str = None) -> tuple[bool, json: The CycloneDX output """ cdx, vdx = self.parse(data) - if not cdx: + if cdx is None: self.print_stderr('ERROR: No CycloneDX data returned for the JSON string provided.') - return False, None + return False, {} + if len(cdx) == 0: + self.print_msg('Warning: Empty scan results - generating minimal CycloneDX SBOM with no components.') self._spdx.load_license_data() # Load SPDX license name data for later reference # # Using CDX version 1.4: https://cyclonedx.org/docs/1.4/json/ diff --git a/src/scanoss/export/dependency_track.py b/src/scanoss/export/dependency_track.py index 38dccd4d..f3969f40 100644 --- a/src/scanoss/export/dependency_track.py +++ b/src/scanoss/export/dependency_track.py @@ -118,7 +118,12 @@ def _encode_sbom(self, sbom_content: dict) -> str: Base64 encoded string """ if not sbom_content: - self.print_stderr('Warning: Empty SBOM content') + self.print_stderr('Warning: Empty SBOM content provided') + return '' + # Check if SBOM has no components (empty scan results) + components = sbom_content.get('components', []) + if len(components) == 0: + self.print_msg('Notice: SBOM contains no components (empty scan results)') json_str = json.dumps(sbom_content, separators=(',', ':')) encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8') return encoded diff --git a/src/scanoss/inspection/dependency_track/project_violation.py b/src/scanoss/inspection/dependency_track/project_violation.py index 0216ab87..08cdf174 100644 --- a/src/scanoss/inspection/dependency_track/project_violation.py +++ b/src/scanoss/inspection/dependency_track/project_violation.py @@ -230,16 +230,29 @@ def is_project_updated(self, dt_project: Dict[str, Any]) -> bool: if not dt_project: self.print_stderr('Warning: No project details supplied. Returning False.') return False - last_import = dt_project.get('lastBomImport', 0) - last_vulnerability_analysis = dt_project.get('lastVulnerabilityAnalysis', 0) + + # Safely extract and normalise timestamp values to numeric types + def _safe_timestamp(field, value=None, default=0) -> float: + """Convert timestamp value to float, handling string/numeric types safely.""" + if value is None: + return float(default) + try: + return float(value) + except (ValueError, TypeError): + self.print_stderr(f'Warning: Invalid timestamp for {field}, value: {value}, using default: {default}') + return float(default) + + last_import = _safe_timestamp('lastBomImport', dt_project.get('lastBomImport'), 0) + last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis', dt_project.get('lastVulnerabilityAnalysis'), 0) metrics = dt_project.get('metrics', {}) - last_occurrence = metrics.get('lastOccurrence', 0) if isinstance(metrics, dict) else 0 + last_occurrence = _safe_timestamp('lastOccurrence', metrics.get('lastOccurrence', 0) if isinstance(metrics, dict) else 0, 0) if self.debug: self.print_msg(f'last_import: {last_import}') self.print_msg(f'last_vulnerability_analysis: {last_vulnerability_analysis}') self.print_msg(f'last_occurrence: {last_occurrence}') self.print_msg(f'last_vulnerability_analysis is updated: {last_vulnerability_analysis >= last_import}') self.print_msg(f'last_occurrence is updated: {last_occurrence >= last_import}') + # If all timestamps are zero, this indicates no processing has occurred if last_vulnerability_analysis == 0 or last_occurrence == 0 or last_import == 0: self.print_stderr(f'Warning: Some project data appears to be unset. Returning False: {dt_project}') return False @@ -434,12 +447,16 @@ def run(self) -> int: return PolicyStatus.ERROR.value # Get project violations from Dependency Track dt_project_violations = self.dep_track_service.get_project_violations(self.project_id) + # Handle case where service returns None (API error) vs empty list (no violations) + if dt_project_violations is None: + self.print_stderr('Error: Failed to retrieve project violations from Dependency Track') + return PolicyStatus.ERROR.value # Sort violations by priority and format output formatter = self._get_formatter() if formatter is None: self.print_stderr('Error: Invalid format specified.') return PolicyStatus.ERROR.value - # Format and output data + # Format and output data - handle empty results gracefully data = formatter(self._sort_project_violations(dt_project_violations)) self.print_to_file_or_stdout(data['details'], self.output) self.print_to_file_or_stderr(data['summary'], self.status) diff --git a/src/scanoss/services/dependency_track_service.py b/src/scanoss/services/dependency_track_service.py index e32c2452..7a367a0f 100644 --- a/src/scanoss/services/dependency_track_service.py +++ b/src/scanoss/services/dependency_track_service.py @@ -97,6 +97,7 @@ def get_project_violations(self,project_id:str): if not project_id: self.print_stderr('Error: Missing project id. Cannot search for project violations.') return None + # Return the result as-is - None indicates API failure, empty list means no violations return self.get_dep_track_data(f'{self.url}/api/v1/violation/project/{project_id}') def get_project_by_id(self, project_id:str): diff --git a/src/scanoss/spdxlite.py b/src/scanoss/spdxlite.py index fe864eba..7313b271 100644 --- a/src/scanoss/spdxlite.py +++ b/src/scanoss/spdxlite.py @@ -71,9 +71,12 @@ def parse(self, data: json): :param data: json - JSON object :return: summary dictionary """ - if not data: + if data is None: self.print_stderr('ERROR: No JSON data provided to parse.') return None + if len(data) == 0: + self.print_debug('Warning: Empty scan results provided. Returning empty summary.') + return {} self.print_debug('Processing raw results into summary format...') return self._process_files(data) @@ -277,9 +280,11 @@ def produce_from_json(self, data: json, output_file: str = None) -> bool: :return: True if successful, False otherwise """ raw_data = self.parse(data) - if not raw_data: + if raw_data is None: self.print_stderr('ERROR: No SPDX data returned for the JSON string provided.') return False + if len(raw_data) == 0: + self.print_debug('Warning: Empty scan results - generating minimal SPDX Lite document with no packages.') self.load_license_data() spdx_document = self._create_base_document(raw_data) From f38f5af2238d2f7d18f9b34dcb0e15def528b043 Mon Sep 17 00:00:00 2001 From: Alex Egan Date: Tue, 19 Aug 2025 14:25:24 +0100 Subject: [PATCH 2/4] Updated __init__.py --- src/scanoss/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanoss/__init__.py b/src/scanoss/__init__.py index a16668c3..40a8be75 100644 --- a/src/scanoss/__init__.py +++ b/src/scanoss/__init__.py @@ -22,4 +22,4 @@ THE SOFTWARE. """ -__version__ = '1.31.2' +__version__ = '1.31.3' From f3b52820db2d84330ad44ec523330c034a8583f4 Mon Sep 17 00:00:00 2001 From: Alex Egan Date: Tue, 19 Aug 2025 17:12:15 +0100 Subject: [PATCH 3/4] Fixed linting issues --- .../inspection/dependency_track/project_violation.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/scanoss/inspection/dependency_track/project_violation.py b/src/scanoss/inspection/dependency_track/project_violation.py index 08cdf174..a5876682 100644 --- a/src/scanoss/inspection/dependency_track/project_violation.py +++ b/src/scanoss/inspection/dependency_track/project_violation.py @@ -243,9 +243,14 @@ def _safe_timestamp(field, value=None, default=0) -> float: return float(default) last_import = _safe_timestamp('lastBomImport', dt_project.get('lastBomImport'), 0) - last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis', dt_project.get('lastVulnerabilityAnalysis'), 0) + last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis', + dt_project.get('lastVulnerabilityAnalysis'), 0 + ) metrics = dt_project.get('metrics', {}) - last_occurrence = _safe_timestamp('lastOccurrence', metrics.get('lastOccurrence', 0) if isinstance(metrics, dict) else 0, 0) + last_occurrence = _safe_timestamp('lastOccurrence', + metrics.get('lastOccurrence', 0) + if isinstance(metrics, dict) else 0, 0 + ) if self.debug: self.print_msg(f'last_import: {last_import}') self.print_msg(f'last_vulnerability_analysis: {last_vulnerability_analysis}') From 25b8835ab682005b48ee4da1dbd6da94a2b3a250 Mon Sep 17 00:00:00 2001 From: Alex Egan Date: Tue, 19 Aug 2025 18:01:47 +0100 Subject: [PATCH 4/4] Fixed Docker and linter issues --- Dockerfile | 2 +- src/scanoss/csvoutput.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 14d18c2c..f1fb4648 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM python:3.10-slim AS base +FROM --platform=$BUILDPLATFORM python:3.10-slim-bookworm AS base LABEL maintainer="SCANOSS " LABEL org.opencontainers.image.source=https://github.com/scanoss/scanoss.py diff --git a/src/scanoss/csvoutput.py b/src/scanoss/csvoutput.py index b4560874..5a7b1974 100644 --- a/src/scanoss/csvoutput.py +++ b/src/scanoss/csvoutput.py @@ -21,11 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ - +import csv import json import os.path import sys -import csv from .scanossbase import ScanossBase @@ -44,7 +43,8 @@ def __init__(self, debug: bool = False, output_file: str = None): self.output_file = output_file self.debug = debug - def parse(self, data: json): + # TODO Refactor (fails linter) + def parse(self, data: json): #noqa PLR0912, PLR0915 """ Parse the given input (raw/plain) JSON string and return CSV summary :param data: json - JSON object @@ -56,7 +56,7 @@ def parse(self, data: json): if len(data) == 0: self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.') return [] - self.print_debug(f'Processing raw results into CSV format...') + self.print_debug('Processing raw results into CSV format...') csv_dict = [] row_id = 1 for f in data: @@ -95,7 +95,8 @@ def parse(self, data: json): detected['licenses'] = '' else: detected['licenses'] = ';'.join(dc) - # inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl + # inventory_id,path,usage,detected_component,detected_license, + # detected_version,detected_latest,purl csv_dict.append( { 'inventory_id': row_id,