[go: up one dir, main page]

Add BBM to delete orphaned dependency scanning Security::Scan records

What does this MR do and why?

Add batched background migration to delete orphaned Dependency Scanning Security::Scan records.

When the Dependency Scanning using SBOM beta feature processes SBOM reports, it creates Security::Scan records in a created state. However, the cleanup logic only runs when the security report has data. Since SBOM reports may have blank security data (when they're incompatible with the DS analyzer), these records are skipped during cleanup and remain in the created state indefinitely.

This MR adds a batched background migration to remove these orphaned records that have been in created state for more than 7 days.

Note: This complements !212842 (merged), which fixes the code to prevent new orphaned records from being created. This migration cleans up the existing backlog.

References

DS using SBOM feature creates dangling Security... (#580826)

Remove dangling scans even if there are no scan... (!212842 - merged)

Query Plan

EXPLAIN SELECT * FROM security_scans
WHERE scan_type = 2 
  AND status = 0 
  AND created_at < NOW() - INTERVAL '7 days'
LIMIT 10000;

image

How to set up and validate locally

Click to expand test setup and verification steps

Prerequisites

Ensure you have a running GDK environment with access to the Rails console.

1. Create test data

Run the following script in the Rails console to create test records:

# Get the current user and organization
current_user = User.first
organization = Organizations::Organization.first

# Create a namespace with owner and organization
namespace = Namespace.create!(
  name: 'test-migration',
  path: 'test-migration',
  owner: current_user,
  organization: organization
)

# Create a project
project = Project.create!(
  name: 'test-migration-project',
  path: 'test-migration-project',
  namespace: namespace,
  creator: current_user,
  organization: organization,
  visibility_level: Gitlab::VisibilityLevel::PRIVATE
)

# Create a pipeline
pipeline = Ci::Pipeline.create!(
  project: project,
  status: 'success',
  source: 'push',
  sha: 'abc123def456',
  ref: 'main'
)

# Create a stage for the pipeline
stage = Ci::Stage.create!(
  pipeline: pipeline,
  project: project,
  name: 'test',
  position: 1,
  status: 'success'
)

# Helper to create builds
def create_build(project, pipeline, stage, index)
  Ci::Build.create!(
    project: project,
    pipeline: pipeline,
    stage_id: stage.id,
    name: "test-job-#{index}",
    status: 'success',
    ref: 'main',
    tag: false,
    scheduling_type: :stage
  )
end

# Create test scans
test_cases = [
  { count: 1000, prefix: 'orphaned', scan_type: :dependency_scanning, status: :created, stale: true, description: 'orphaned dependency scanning scans (stale - SHOULD be deleted)' },
  { count: 200, prefix: 'recent', scan_type: :dependency_scanning, status: :created, stale: false, description: 'recent created scans (should NOT be deleted)' },
  { count: 300, prefix: 'processed', scan_type: :dependency_scanning, status: :succeeded, stale: true, description: 'processed scans (should NOT be deleted)' },
  { count: 150, prefix: 'sast', scan_type: :sast, status: :created, stale: true, description: 'SAST scans in created state (should NOT be deleted)' }
]

test_cases.each do |tc|
  puts "Creating #{tc[:count]} #{tc[:description]}..."
  tc[:count].times do |i|
    build = create_build(project, pipeline, stage, "#{tc[:prefix]}-#{i}")
    days_ago = tc[:stale] ? (10 + rand(30)).days.ago : (1 + rand(6)).days.ago

    Security::Scan.create!(
      project: project,
      pipeline: pipeline,
      build: build,
      scan_type: tc[:scan_type],
      status: tc[:status],
      created_at: days_ago,
      updated_at: days_ago
    )
    puts "  Created #{i + 1}/#{tc[:count]}" if (i + 1) % 100 == 0
  end
end

2. Verify test data before migration

puts "=== Test Data Summary ==="
puts "Total Security::Scan records: #{Security::Scan.count}"
puts "Orphaned scans (SHOULD be deleted): #{Security::Scan.where(scan_type: :dependency_scanning, status: :created).where('created_at < ?', 7.days.ago).count}"
puts "Recent created scans (should NOT be deleted): #{Security::Scan.where(scan_type: :dependency_scanning, status: :created).where('created_at >= ?', 7.days.ago).count}"
puts "Processed scans (should NOT be deleted): #{Security::Scan.where(scan_type: :dependency_scanning, status: :succeeded).count}"
puts "SAST scans (should NOT be deleted): #{Security::Scan.where(scan_type: :sast).count}"

Expected output (approx):

Category Count
Total records 1650
Orphaned scans (to delete) 1000
Recent created scans 200
Processed scans 300
SAST scans 150

3. Run the migration

migration = Gitlab::BackgroundMigration::DeleteOrphanedDependencyScans.new(
  start_id: Security::Scan.minimum(:id),
  end_id: Security::Scan.maximum(:id),
  batch_table: :security_scans,
  batch_column: :id,
  sub_batch_size: 100,
  pause_ms: 0,
  connection: ApplicationRecord.connection
)
migration.perform

4. Verify results after migration

Run the same verification script from step 2.

Expected output (approx):

Category Count
Total records 650
Orphaned scans 0
Recent created scans 200
Processed scans 300
SAST scans 150

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Albina Yusupova

Merge request reports

Loading