Skip to content

Commit 3900859

Browse files
authored
Added handling for empty results files (#145)
* Added handling for empty results files * Fixed Docker and linter issues
1 parent fdc02af commit 3900859

File tree

9 files changed

+68
-19
lines changed

9 files changed

+68
-19
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.31.3] - 2025-08-19
13+
### Fixed
14+
- Added handling for empty results files
15+
1216
## [1.31.2] - 2025-08-12
1317
### Fixed
1418
- 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
638642
[1.31.0]: https://github.com/scanoss/scanoss.py/compare/v1.30.0...v1.31.0
639643
[1.31.1]: https://github.com/scanoss/scanoss.py/compare/v1.31.0...v1.31.1
640644
[1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.1...v1.31.2
645+
[1.31.2]: https://github.com/scanoss/scanoss.py/compare/v1.31.2...v1.31.3

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM --platform=$BUILDPLATFORM python:3.10-slim AS base
1+
FROM --platform=$BUILDPLATFORM python:3.10-slim-bookworm AS base
22

33
LABEL maintainer="SCANOSS <[email protected]>"
44
LABEL org.opencontainers.image.source=https://github.com/scanoss/scanoss.py

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.31.2'
25+
__version__ = '1.31.3'

src/scanoss/csvoutput.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2222
THE SOFTWARE.
2323
"""
24-
24+
import csv
2525
import json
2626
import os.path
2727
import sys
28-
import csv
2928

3029
from .scanossbase import ScanossBase
3130

@@ -44,16 +43,20 @@ def __init__(self, debug: bool = False, output_file: str = None):
4443
self.output_file = output_file
4544
self.debug = debug
4645

47-
def parse(self, data: json):
46+
# TODO Refactor (fails linter)
47+
def parse(self, data: json): #noqa PLR0912, PLR0915
4848
"""
4949
Parse the given input (raw/plain) JSON string and return CSV summary
5050
:param data: json - JSON object
5151
:return: CSV dictionary
5252
"""
53-
if not data:
53+
if data is None:
5454
self.print_stderr('ERROR: No JSON data provided to parse.')
5555
return None
56-
self.print_debug(f'Processing raw results into CSV format...')
56+
if len(data) == 0:
57+
self.print_msg('Warning: Empty scan results provided. Returning empty CSV list.')
58+
return []
59+
self.print_debug('Processing raw results into CSV format...')
5760
csv_dict = []
5861
row_id = 1
5962
for f in data:
@@ -92,7 +95,8 @@ def parse(self, data: json):
9295
detected['licenses'] = ''
9396
else:
9497
detected['licenses'] = ';'.join(dc)
95-
# inventory_id,path,usage,detected_component,detected_license,detected_version,detected_latest,purl
98+
# inventory_id,path,usage,detected_component,detected_license,
99+
# detected_version,detected_latest,purl
96100
csv_dict.append(
97101
{
98102
'inventory_id': row_id,
@@ -183,9 +187,11 @@ def produce_from_json(self, data: json, output_file: str = None) -> bool:
183187
:return: True if successful, False otherwise
184188
"""
185189
csv_data = self.parse(data)
186-
if not csv_data:
190+
if csv_data is None:
187191
self.print_stderr('ERROR: No CSV data returned for the JSON string provided.')
188192
return False
193+
if len(csv_data) == 0:
194+
self.print_msg('Warning: Empty scan results - generating CSV with headers only.')
189195
# Header row/column details
190196
fields = [
191197
'inventory_id',

src/scanoss/cyclonedx.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ def parse(self, data: dict): # noqa: PLR0912, PLR0915
5757
:param data: dict - JSON object
5858
:return: CycloneDX dictionary, and vulnerability dictionary
5959
"""
60-
if not data:
60+
if data is None:
6161
self.print_stderr('ERROR: No JSON data provided to parse.')
6262
return None, None
63+
if len(data) == 0:
64+
self.print_msg('Warning: Empty scan results provided. Returning empty component dictionary.')
65+
return {}, {}
6366
self.print_debug('Processing raw results into CycloneDX format...')
6467
cdx = {}
6568
vdx = {}
@@ -186,9 +189,11 @@ def produce_from_json(self, data: dict, output_file: str = None) -> tuple[bool,
186189
json: The CycloneDX output
187190
"""
188191
cdx, vdx = self.parse(data)
189-
if not cdx:
192+
if cdx is None:
190193
self.print_stderr('ERROR: No CycloneDX data returned for the JSON string provided.')
191-
return False, None
194+
return False, {}
195+
if len(cdx) == 0:
196+
self.print_msg('Warning: Empty scan results - generating minimal CycloneDX SBOM with no components.')
192197
self._spdx.load_license_data() # Load SPDX license name data for later reference
193198
#
194199
# Using CDX version 1.4: https://cyclonedx.org/docs/1.4/json/

src/scanoss/export/dependency_track.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,12 @@ def _encode_sbom(self, sbom_content: dict) -> str:
118118
Base64 encoded string
119119
"""
120120
if not sbom_content:
121-
self.print_stderr('Warning: Empty SBOM content')
121+
self.print_stderr('Warning: Empty SBOM content provided')
122+
return ''
123+
# Check if SBOM has no components (empty scan results)
124+
components = sbom_content.get('components', [])
125+
if len(components) == 0:
126+
self.print_msg('Notice: SBOM contains no components (empty scan results)')
122127
json_str = json.dumps(sbom_content, separators=(',', ':'))
123128
encoded = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
124129
return encoded

src/scanoss/inspection/dependency_track/project_violation.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,34 @@ def is_project_updated(self, dt_project: Dict[str, Any]) -> bool:
230230
if not dt_project:
231231
self.print_stderr('Warning: No project details supplied. Returning False.')
232232
return False
233-
last_import = dt_project.get('lastBomImport', 0)
234-
last_vulnerability_analysis = dt_project.get('lastVulnerabilityAnalysis', 0)
233+
234+
# Safely extract and normalise timestamp values to numeric types
235+
def _safe_timestamp(field, value=None, default=0) -> float:
236+
"""Convert timestamp value to float, handling string/numeric types safely."""
237+
if value is None:
238+
return float(default)
239+
try:
240+
return float(value)
241+
except (ValueError, TypeError):
242+
self.print_stderr(f'Warning: Invalid timestamp for {field}, value: {value}, using default: {default}')
243+
return float(default)
244+
245+
last_import = _safe_timestamp('lastBomImport', dt_project.get('lastBomImport'), 0)
246+
last_vulnerability_analysis = _safe_timestamp('lastVulnerabilityAnalysis',
247+
dt_project.get('lastVulnerabilityAnalysis'), 0
248+
)
235249
metrics = dt_project.get('metrics', {})
236-
last_occurrence = metrics.get('lastOccurrence', 0) if isinstance(metrics, dict) else 0
250+
last_occurrence = _safe_timestamp('lastOccurrence',
251+
metrics.get('lastOccurrence', 0)
252+
if isinstance(metrics, dict) else 0, 0
253+
)
237254
if self.debug:
238255
self.print_msg(f'last_import: {last_import}')
239256
self.print_msg(f'last_vulnerability_analysis: {last_vulnerability_analysis}')
240257
self.print_msg(f'last_occurrence: {last_occurrence}')
241258
self.print_msg(f'last_vulnerability_analysis is updated: {last_vulnerability_analysis >= last_import}')
242259
self.print_msg(f'last_occurrence is updated: {last_occurrence >= last_import}')
260+
# If all timestamps are zero, this indicates no processing has occurred
243261
if last_vulnerability_analysis == 0 or last_occurrence == 0 or last_import == 0:
244262
self.print_stderr(f'Warning: Some project data appears to be unset. Returning False: {dt_project}')
245263
return False
@@ -434,12 +452,16 @@ def run(self) -> int:
434452
return PolicyStatus.ERROR.value
435453
# Get project violations from Dependency Track
436454
dt_project_violations = self.dep_track_service.get_project_violations(self.project_id)
455+
# Handle case where service returns None (API error) vs empty list (no violations)
456+
if dt_project_violations is None:
457+
self.print_stderr('Error: Failed to retrieve project violations from Dependency Track')
458+
return PolicyStatus.ERROR.value
437459
# Sort violations by priority and format output
438460
formatter = self._get_formatter()
439461
if formatter is None:
440462
self.print_stderr('Error: Invalid format specified.')
441463
return PolicyStatus.ERROR.value
442-
# Format and output data
464+
# Format and output data - handle empty results gracefully
443465
data = formatter(self._sort_project_violations(dt_project_violations))
444466
self.print_to_file_or_stdout(data['details'], self.output)
445467
self.print_to_file_or_stderr(data['summary'], self.status)

src/scanoss/services/dependency_track_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def get_project_violations(self,project_id:str):
9797
if not project_id:
9898
self.print_stderr('Error: Missing project id. Cannot search for project violations.')
9999
return None
100+
# Return the result as-is - None indicates API failure, empty list means no violations
100101
return self.get_dep_track_data(f'{self.url}/api/v1/violation/project/{project_id}')
101102

102103
def get_project_by_id(self, project_id:str):

src/scanoss/spdxlite.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ def parse(self, data: json):
7171
:param data: json - JSON object
7272
:return: summary dictionary
7373
"""
74-
if not data:
74+
if data is None:
7575
self.print_stderr('ERROR: No JSON data provided to parse.')
7676
return None
77+
if len(data) == 0:
78+
self.print_debug('Warning: Empty scan results provided. Returning empty summary.')
79+
return {}
7780

7881
self.print_debug('Processing raw results into summary format...')
7982
return self._process_files(data)
@@ -277,9 +280,11 @@ def produce_from_json(self, data: json, output_file: str = None) -> bool:
277280
:return: True if successful, False otherwise
278281
"""
279282
raw_data = self.parse(data)
280-
if not raw_data:
283+
if raw_data is None:
281284
self.print_stderr('ERROR: No SPDX data returned for the JSON string provided.')
282285
return False
286+
if len(raw_data) == 0:
287+
self.print_debug('Warning: Empty scan results - generating minimal SPDX Lite document with no packages.')
283288

284289
self.load_license_data()
285290
spdx_document = self._create_base_document(raw_data)

0 commit comments

Comments
 (0)