From 9d60df828fd3a32eea47b20a2139a82b87b52482 Mon Sep 17 00:00:00 2001 From: Duo Developer Date: Mon, 15 Dec 2025 12:06:59 +0000 Subject: [PATCH] Filter Vulnerabities::Read by security_project_tracked_context_id Changelog: added EE: true --- .../security/vulnerability_reads_finder.rb | 10 ++ ee/app/models/vulnerabilities/read.rb | 1 + .../vulnerability_reads_finder_spec.rb | 121 ++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/ee/app/finders/security/vulnerability_reads_finder.rb b/ee/app/finders/security/vulnerability_reads_finder.rb index 6c8b3fc5566383..897443c1fdbe4e 100644 --- a/ee/app/finders/security/vulnerability_reads_finder.rb +++ b/ee/app/finders/security/vulnerability_reads_finder.rb @@ -22,6 +22,7 @@ # has_issues: only return vulnerabilities that have issues linked # has_merge_request: only return vulnerabilities that have MR(s) linked # cluster_agent_id: only return vulnerabilities with these cluster_agent_ids +# security_project_tracked_context_id: only return vulnerabilities with these security_project_tracked_context_ids # has_remediations: only return vulnerabilities that have remediations # before_severity: only return vulnerabilities with lower severity than given value if sorting is ascending, # returns vulnerabilities with higher severity otherwise. Works only for groups. @@ -48,6 +49,7 @@ def execute use_unnested_filters filter_archived_projects filter_by_projects + filter_by_security_project_tracked_context_id filter_by_image filter_by_report_types filter_by_severities @@ -205,6 +207,14 @@ def filter_by_has_remediations @vulnerability_reads = vulnerability_reads.with_remediations(params[:has_remediations]) end + def filter_by_security_project_tracked_context_id + return unless params[:security_project_tracked_context_id].present? + + @vulnerability_reads = vulnerability_reads.by_security_project_tracked_context_id( + params[:security_project_tracked_context_id] + ) + end + def sort if vulnerable_is_a_group? @vulnerability_reads.order_by_params_and_traversal_ids(params[:sort]) diff --git a/ee/app/models/vulnerabilities/read.rb b/ee/app/models/vulnerabilities/read.rb index 58e1fa61fe536e..3a1b78b7f447df 100644 --- a/ee/app/models/vulnerabilities/read.rb +++ b/ee/app/models/vulnerabilities/read.rb @@ -100,6 +100,7 @@ class << self scope :by_projects, ->(values) { where(project_id: values) } scope :by_scanner, ->(scanner) { where(scanner: scanner) } scope :by_scanner_ids, ->(scanner_ids) { where(scanner_id: scanner_ids) } + scope :by_security_project_tracked_context_id, ->(context_ids) { where(security_project_tracked_context_id: context_ids) } scope :grouped_by_severity, -> { reorder(severity: :desc).group(:severity) } scope :with_report_types, ->(report_types) { where(report_type: report_types) } scope :with_severities, ->(severities) { where(severity: severities) } diff --git a/ee/spec/finders/security/vulnerability_reads_finder_spec.rb b/ee/spec/finders/security/vulnerability_reads_finder_spec.rb index 8d182827d1f983..200ffa29356ac0 100644 --- a/ee/spec/finders/security/vulnerability_reads_finder_spec.rb +++ b/ee/spec/finders/security/vulnerability_reads_finder_spec.rb @@ -773,6 +773,127 @@ end end + context 'when filtered by security_project_tracked_context_id' do + let_it_be(:tracked_context_1) { create(:security_project_tracked_context, project: project) } + let_it_be(:tracked_context_2) { create(:security_project_tracked_context, project: project) } + let_it_be(:tracked_context_3) { create(:security_project_tracked_context, project: project) } + + let_it_be(:vuln_read_with_context_1) do + create(:vulnerability_read, project: project, tracked_context: tracked_context_1) + end + + let_it_be(:vuln_read_with_context_2) do + create(:vulnerability_read, project: project, tracked_context: tracked_context_2) + end + + let_it_be(:vuln_read_without_context) do + create(:vulnerability_read, project: project, tracked_context: nil) + end + + context 'when filtered by a single tracked context ID' do + let(:filters) { { security_project_tracked_context_id: [tracked_context_1.id] } } + + it 'only returns vulnerabilities with the specified tracked context' do + is_expected.to contain_exactly(vuln_read_with_context_1) + end + end + + context 'when filtered by multiple tracked context IDs' do + let(:filters) { { security_project_tracked_context_id: [tracked_context_1.id, tracked_context_2.id] } } + + it 'only returns vulnerabilities with any of the specified tracked contexts' do + is_expected.to contain_exactly(vuln_read_with_context_1, vuln_read_with_context_2) + end + end + + context 'when security_project_tracked_context_id is nil' do + let(:filters) { { security_project_tracked_context_id: nil } } + + it 'returns all vulnerabilities without filtering' do + is_expected.to contain_exactly(low_severity_vuln_read, + medium_severity_vuln_read, + high_severity_vuln_read, + vulnerability_dismissed_without_reason_read, + dismissed_vulnerability_read, + vuln_read_with_context_1, + vuln_read_with_context_2, + vuln_read_without_context + ) + end + end + + context 'when security_project_tracked_context_id is blank' do + let(:filters) { { security_project_tracked_context_id: [] } } + + it 'returns all vulnerabilities without filtering' do + is_expected.to contain_exactly(low_severity_vuln_read, + medium_severity_vuln_read, + high_severity_vuln_read, + vulnerability_dismissed_without_reason_read, + dismissed_vulnerability_read, + vuln_read_with_context_1, + vuln_read_with_context_2, + vuln_read_without_context + ) + end + end + + context 'when combined with other filters' do + let_it_be(:vuln_read_high_with_context_1) do + create(:vulnerability_read, project: project, tracked_context: tracked_context_1, severity: :high) + end + + let(:filters) do + { security_project_tracked_context_id: [tracked_context_1.id], severity: %w[high] } + end + + it 'returns vulnerabilities matching all filters' do + is_expected.to contain_exactly(vuln_read_high_with_context_1) + end + end + + context 'when vulnerable is a Group' do + let(:vulnerable) { group } + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:another_project) { create(:project, namespace: group) } + + let_it_be(:group_tracked_context_1) { create(:security_project_tracked_context, project: project) } + let_it_be(:group_tracked_context_2) { create(:security_project_tracked_context, project: another_project) } + + let_it_be(:group_vuln_read_with_context_1) do + create(:vulnerability_read, project: project, tracked_context: group_tracked_context_1) + end + + let_it_be(:group_vuln_read_with_context_2) do + create(:vulnerability_read, project: another_project, tracked_context: group_tracked_context_2) + end + + let_it_be(:group_vuln_read_without_context) do + create(:vulnerability_read, project: project, tracked_context: nil) + end + + context 'when filtered by a single tracked context ID' do + let(:filters) { { security_project_tracked_context_id: [group_tracked_context_1.id] } } + + it 'only returns vulnerabilities with the specified tracked context' do + is_expected.to contain_exactly(group_vuln_read_with_context_1) + end + end + + context 'when filtered by multiple tracked context IDs' do + let(:filters) do + { security_project_tracked_context_id: [group_tracked_context_1.id, group_tracked_context_2.id] } + end + + it 'only returns vulnerabilities with any of the specified tracked contexts' do + is_expected.to contain_exactly(group_vuln_read_with_context_1, group_vuln_read_with_context_2) + end + end + end + end + describe 'use of unnested filters' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project) } -- GitLab