Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
9 changes: 7 additions & 2 deletions src/scanoss/csvoutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
11 changes: 8 additions & 3 deletions src/scanoss/cyclonedx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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/
Expand Down
7 changes: 6 additions & 1 deletion src/scanoss/export/dependency_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Comment on lines 120 to +126
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Don’t upload an empty BOM to Dependency Track

When sbom_content is falsy, _encode_sbom returns an empty string. The caller (upload_sbom_contents) will still build a payload and attempt the upload, which is likely to be rejected by the API.

Add a guard in upload_sbom_contents to skip the upload if encoding produced an empty string.

@@
-            self.print_debug('Encoding SBOM to base64')
-            payload = _build_payload(self._encode_sbom(sbom_content), project_id, project_name, project_version)
+            self.print_debug('Encoding SBOM to base64')
+            encoded_bom = self._encode_sbom(sbom_content)
+            if not encoded_bom:
+                self.print_msg('Empty SBOM content detected. Skipping upload to Dependency Track.')
+                return False
+            payload = _build_payload(encoded_bom, project_id, project_name, project_version)

🏁 Script executed:

#!/bin/bash
# Locate the upload_sbom_contents method and inspect its implementation
rg -nP "def upload_sbom_contents" -C10 src/scanoss/export/dependency_track.py

# Confirm where and how _encode_sbom is invoked within that method
rg -nP "_encode_sbom" -C10 src/scanoss/export/dependency_track.py

Length of output: 3311


Add guard to skip upload when SBOM encoding yields an empty string

To prevent attempting to send an empty BOM payload (which the API will reject), update upload_sbom_contents in src/scanoss/export/dependency_track.py as follows:

• File: src/scanoss/export/dependency_track.py
• Method: upload_sbom_contents (around line 186)

@@ -186,7 +186,12 @@
             self.print_debug('Encoding SBOM to base64')
-            payload = _build_payload(self._encode_sbom(sbom_content), project_id, project_name, project_version)
+            # Skip sending if SBOM content is empty or invalid
+            encoded_bom = self._encode_sbom(sbom_content)
+            if not encoded_bom:
+                self.print_msg('Empty SBOM content detected. Skipping upload to Dependency Track.')
+                return False
+            payload = _build_payload(encoded_bom, project_id, project_name, project_version)
 
             url = f'{self.url}/api/v1/bom'
             headers = {'Content-Type': 'application/json', 'X-Api-Key': self.apikey}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)')
self.print_debug('Encoding SBOM to base64')
# Skip sending if SBOM content is empty or invalid
encoded_bom = self._encode_sbom(sbom_content)
if not encoded_bom:
self.print_msg('Empty SBOM content detected. Skipping upload to Dependency Track.')
return False
payload = _build_payload(encoded_bom, project_id, project_name, project_version)
url = f'{self.url}/api/v1/bom'
headers = {'Content-Type': 'application/json', 'X-Api-Key': self.apikey}
🤖 Prompt for AI Agents
In src/scanoss/export/dependency_track.py around lines 120-126 (and adjust in
upload_sbom_contents near line ~186), add a guard that prevents attempting to
upload when SBOM encoding results in an empty string: after encoding/serializing
the SBOM (the variable that becomes the payload), check if the encoded_payload
is falsy or empty and if so log/print a warning/notice and return early without
calling the upload API; ensure the function returns an empty string or
appropriate sentinel consistent with existing returns and does not proceed to
send the empty BOM to the API.

json_str = json.dumps(sbom_content, separators=(',', ':'))
encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
return encoded
Expand Down
25 changes: 21 additions & 4 deletions src/scanoss/inspection/dependency_track/project_violation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/scanoss/services/dependency_track_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 7 additions & 2 deletions src/scanoss/spdxlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading