diff --git a/ee/app/finders/sbom/possibly_affected_occurrences_finder.rb b/ee/app/finders/sbom/possibly_affected_occurrences_finder.rb index ed84f649c978bf085b5f85a85bfe30299427c76c..37d41e6c1bd723e852bd841818b6a1a9bb1008c1 100644 --- a/ee/app/finders/sbom/possibly_affected_occurrences_finder.rb +++ b/ee/app/finders/sbom/possibly_affected_occurrences_finder.rb @@ -21,6 +21,7 @@ def execute_in_batches(of: BATCH_SIZE) search_scope.each_batch(of: of) do |batch| yield batch .with_component_source_version_and_project + .with_project_setting .with_pipeline_project_and_namespace .filter_by_non_nil_component_version end diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb index 28a9d0a47ad4b0dc1a54041072d7cd26cab5bc07..596f8e9ff26d321e6f2b67f01891ddf190356da4 100644 --- a/ee/app/models/sbom/occurrence.rb +++ b/ee/app/models/sbom/occurrence.rb @@ -167,6 +167,7 @@ class Occurrence < ApplicationRecord includes(:component, :source, :component_version, :project) .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/473758') end + scope :with_project_setting, -> { preload(project: :project_setting) } scope :filter_by_non_nil_component_version, -> { where.not(component_version: nil) } scope :order_by_severity, ->(direction) do diff --git a/ee/app/services/security/ingestion/ingest_cvs_slice_service.rb b/ee/app/services/security/ingestion/ingest_cvs_slice_service.rb index a96f4074189db58f0f5202f9bb4fb82dedbe62ae..4b62e22bce04fd3196b8548a65bf807f1839a93c 100644 --- a/ee/app/services/security/ingestion/ingest_cvs_slice_service.rb +++ b/ee/app/services/security/ingestion/ingest_cvs_slice_service.rb @@ -16,6 +16,7 @@ class IngestCvsSliceService < IngestSliceBaseService IngestFindingEvidence IngestVulnerabilityFlags IngestVulnerabilityReads + MarkCvsProjectsAsVulnerable IngestVulnerabilityStatistics HooksExecution ].freeze diff --git a/ee/app/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable.rb b/ee/app/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable.rb new file mode 100644 index 0000000000000000000000000000000000000000..1efd72511b3437250c4b1033d0cd192848be804b --- /dev/null +++ b/ee/app/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Security + module Ingestion + module Tasks + class MarkCvsProjectsAsVulnerable < AbstractTask + def execute + new_vulnerable_projects.each(&:mark_as_vulnerable!) + end + + private + + def new_vulnerable_projects + unique_projects.select { |project| !project.project_setting&.has_vulnerabilities? } + end + + def unique_projects + finding_maps.map(&:project).uniq + end + end + end + end +end diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb index 682c79101f5d754e088d532f2d5f0e9cb37cbae5..fe668e99713bb817d9106db7f094bbd6bef5476e 100644 --- a/ee/spec/models/sbom/occurrence_spec.rb +++ b/ee/spec/models/sbom/occurrence_spec.rb @@ -151,6 +151,21 @@ end end + describe '.with_project_setting' do + let!(:occurrence) { create(:sbom_occurrence) } + + it 'pre-loads relations to avoid executing additional queries' do + record = described_class.with_project_setting.first + + queries = ActiveRecord::QueryRecorder.new do + record.project + record.project.project_setting + end + + expect(queries.count).to be_zero + end + end + describe '.with_pipeline_project_and_namespace' do before do create(:sbom_occurrence, component: create(:sbom_component)) diff --git a/ee/spec/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable_spec.rb b/ee/spec/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9d11b5df2c0c0fbffd6682033cbb335e85ef9acb --- /dev/null +++ b/ee/spec/services/security/ingestion/tasks/mark_cvs_projects_as_vulnerable_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::Ingestion::Tasks::MarkCvsProjectsAsVulnerable, feature_category: :software_composition_analysis do + describe '#execute' do + let(:project_setting_1) { create(:project_setting, has_vulnerabilities: false) } + let(:project_setting_2) { create(:project_setting, has_vulnerabilities: true) } + + let(:project_1) { project_setting_1.project } + let(:project_2) { project_setting_2.project } + let(:project_3) { create(:project) } + + let(:pipeline_1) { create(:ci_pipeline, project: project_1) } + let(:pipeline_2) { create(:ci_pipeline, project: project_2) } + let(:pipeline_3) { create(:ci_pipeline, project: project_3) } + + let(:finding_map_1) { create(:vs_finding_map, pipeline: pipeline_1) } + let(:finding_map_2) { create(:vs_finding_map, pipeline: pipeline_2) } + let(:finding_map_3) { create(:vs_finding_map, pipeline: pipeline_3) } + + let(:task) { described_class.new(nil, [finding_map_1, finding_map_2, finding_map_3]) } + + subject(:execute) { task.execute } + + it 'marks projects as has_vulnerabilities' do + expect { execute }.to change { project_1.reload.project_setting.has_vulnerabilities? }.to(true) + .and change { project_3.reload.project_setting.has_vulnerabilities? }.to(true) + .and not_change { project_2.reload.project_setting.has_vulnerabilities? }.from(true) + end + end +end