Skip to content

Commit ffcb78b

Browse files
authored
fix(sbom): Do not pollute database with stale SBoM blobs (#889)
SBoMs can be re-generated, however the technical digest used to reference the blobs is not stable. This leads to a situation where regeneration updates the referenced digest, but does not clean up the old blob, effectively polluting the database and letting it grow in size. As a mitigation, introduce an explicit overwrite codepath which tackles deletion. Optionally, skip regeneration in case an SBoM exists for the artefact already (can be detected as OCM artefact identities are used instead of the technical blob referenece). Signed-off-by: Philipp Heil (zkdev) <philipp.heil@sap.com>
1 parent 2cebaea commit ffcb78b

4 files changed

Lines changed: 125 additions & 1 deletion

File tree

charts/bootstrapping/values.documentation.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,11 @@ extensions_cfg:
732732
# - syft (default)
733733
# - bdba
734734
generation_mode: syft
735+
# @param extensions_cfg.sbom_generator.on_exist controls how to behave in case a to-be generated
736+
# SBoM exists already, following options are supported:
737+
# - overwrite (default)
738+
# - skip
739+
on_exist: overwrite
735740
# @param extensions_cfg.sbom_generator.output_format define desired SBoM format, following
736741
# options are supported:
737742
# cyclonedx (default)

src/odg/extensions_cfg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,7 @@ class SBOMGeneratorConfig(BacklogItemMixins):
12031203
processing_mode: bdba.model.ProcessingMode = bdba.model.ProcessingMode.FORCE_UPLOAD
12041204
interval: int = 60 * 60 * 24 # 24h
12051205
generation_mode: odg.model.SbomGenerationMode = odg.model.SbomGenerationMode.SYFT
1206+
on_exist: odg.model.OnExist = odg.model.OnExist.OVERWRITE
12061207

12071208
def is_supported(
12081209
self,

src/odg/model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,3 +1793,8 @@ def artefact_scan_info(
17931793
class SbomGenerationMode(enum.StrEnum):
17941794
SYFT = 'syft'
17951795
BDBA = 'bdba'
1796+
1797+
1798+
class OnExist(enum.StrEnum):
1799+
SKIP = 'skip'
1800+
OVERWRITE = 'overwrite'

src/sbom_generator.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import logging
77
import os
88
import tempfile
9+
import time
910

1011
import ci.log
1112
import cnudie.retrieve
1213
import oci.client
14+
import requests.exceptions
1315

1416
import bdba.client
1517
import bdba.model
@@ -185,6 +187,31 @@ def generate_sbom_with_bdba(
185187
return sbom_result
186188

187189

190+
def find_existing_sbom_metadata(
191+
artefact: odg.model.ComponentArtefactId,
192+
delivery_service_client: odg_client.DeliveryServiceClient,
193+
) -> dict | None:
194+
"""
195+
Query for existing SBOM metadata for the given artefact.
196+
197+
Returns the metadata entry if found, None otherwise.
198+
Failures are logged and treated as "no existing SBOM" (safe fallback).
199+
"""
200+
try:
201+
metadata_entries = delivery_service_client.query_metadata(
202+
artefacts=[artefact],
203+
type=odg.model.Datatype.ARTEFACT_SCAN_INFO,
204+
datasource=odg.model.Datasource.SBOM_GENERATOR,
205+
)
206+
207+
if metadata_entries:
208+
return metadata_entries[0]
209+
return None
210+
except Exception as e:
211+
logger.warning(f'Failed to query existing SBOM metadata for {artefact}: {e}')
212+
return None
213+
214+
188215
def generate_sbom_for_artefact(
189216
artefact: odg.model.ComponentArtefactId,
190217
extension_cfg: odg.extensions_cfg.SBOMGeneratorConfig,
@@ -193,7 +220,7 @@ def generate_sbom_for_artefact(
193220
oci_client: oci.client.Client,
194221
secret_factory: secret_mgmt.SecretFactory,
195222
**kwargs,
196-
) -> SBOM:
223+
) -> SBOM | None:
197224
"""
198225
Generates Software Bill of Materials (SBOM) for a component artefact.
199226
Resolves the component descriptor from OCM repositories,
@@ -226,6 +253,30 @@ def generate_sbom_for_artefact(
226253
)
227254
return
228255

256+
# Check for existing SBOM and handle based on on_exist configuration
257+
existing_metadata = find_existing_sbom_metadata(
258+
artefact=artefact,
259+
delivery_service_client=delivery_service_client,
260+
)
261+
262+
old_digest = None
263+
if existing_metadata:
264+
old_digest = existing_metadata.get('data', {}).get('digest')
265+
266+
if extension_cfg.on_exist is odg.model.OnExist.SKIP:
267+
logger.info(
268+
f'SBOM already exists for {artefact}, skipping generation (digest: {old_digest})',
269+
)
270+
return None
271+
272+
logger.info(
273+
f'SBOM exists for {artefact}, will overwrite and clean up old blob '
274+
f'(old digest: {old_digest})',
275+
)
276+
277+
else:
278+
logger.info(f'No existing SBOM found for {artefact}, generating new one')
279+
229280
logger.info(f'Scanning using mode {extension_cfg.generation_mode}')
230281

231282
match extension_cfg.generation_mode:
@@ -333,6 +384,68 @@ def generate_sbom_for_artefact(
333384
],
334385
)
335386

387+
if digest == old_digest:
388+
# digest remained stable, shortcut blob deletion
389+
return sbom_result
390+
391+
# Clean up old blob if overwriting
392+
if old_digest and extension_cfg.on_exist is odg.model.OnExist.OVERWRITE:
393+
max_retries = 3
394+
retry_delay = 5 # Start with 5 seconds
395+
396+
def handle_retry_logic(
397+
error_msg: str,
398+
current_attempt: int,
399+
current_delay: int,
400+
) -> int:
401+
"""Handle retry logic for blob deletion failures. Returns updated delay."""
402+
if current_attempt < max_retries - 1:
403+
logger.warning(
404+
f'Failed to delete old SBOM blob {old_digest} for {artefact} '
405+
f'(attempt {current_attempt + 1}/{max_retries}): {error_msg}. '
406+
f'Retrying in {current_delay} seconds...',
407+
)
408+
time.sleep(current_delay)
409+
return current_delay * 2 # Exponential backoff
410+
else:
411+
logger.error(
412+
f'Failed to delete old SBOM blob {old_digest} for {artefact} '
413+
f'after {max_retries} attempts: {error_msg}. '
414+
'The blob remains orphaned in storage.',
415+
)
416+
return current_delay
417+
418+
for attempt in range(max_retries):
419+
try:
420+
logger.info(
421+
f'Deleting old SBOM blob: {old_digest} (attempt {attempt + 1}/{max_retries})',
422+
)
423+
delivery_service_client.delete_blob(digest=old_digest)
424+
logger.info(f'Successfully deleted old SBOM blob: {old_digest}')
425+
break # Success, exit retry loop
426+
427+
except requests.exceptions.HTTPError as e:
428+
# Check if it's a 404 - blob already deleted, mission accomplished
429+
if e.response is not None and e.response.status_code == 404:
430+
logger.info(
431+
f'Old SBOM blob {old_digest} not found (404), '
432+
'assuming already deleted - cleanup goal achieved',
433+
)
434+
break # No need to retry, blob is gone
435+
436+
error_msg = f'HTTP {e.response.status_code if e.response else "error"} - {e}'
437+
retry_delay = handle_retry_logic(
438+
error_msg=error_msg,
439+
current_attempt=attempt,
440+
current_delay=retry_delay,
441+
)
442+
except Exception as e:
443+
retry_delay = handle_retry_logic(
444+
error_msg=str(e),
445+
current_attempt=attempt,
446+
current_delay=retry_delay,
447+
)
448+
336449
return sbom_result
337450

338451

0 commit comments

Comments
 (0)