diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 090d0cd559453032865300839b487f375e7c15e3..45eda28ee178e0cbbb3d1e93d22df1d67a35eefd 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2005,6 +2005,7 @@ four standard [pagination arguments](#pagination-arguments): | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. | | `owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | +| `policyViolations` {{< icon name="warning-solid" >}} | [`[PolicyViolations!]`](#policyviolations) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy violations.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | `reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. | | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | @@ -32436,6 +32437,7 @@ four standard [pagination arguments](#pagination-arguments): | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. | | `owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | +| `policyViolations` {{< icon name="warning-solid" >}} | [`[PolicyViolations!]`](#policyviolations) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy violations.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | `reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. | | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | @@ -41134,6 +41136,7 @@ four standard [pagination arguments](#pagination-arguments): | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. | | `owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | +| `policyViolations` {{< icon name="warning-solid" >}} | [`[PolicyViolations!]`](#policyviolations) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy violations.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | `reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. | | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | @@ -49873,6 +49876,12 @@ Types of security policy project created status. | `RUNNING` | Represents a running policy violation. | | `WARNING` | Represents a policy violation warning. | +### `PolicyViolations` + +| Value | Description | +| ----- | ----------- | +| `DISMISSED_IN_MR` | Dismissed in Merge request bypass reason. | + ### `PrincipalType` Types of principal that can have secret permissions. diff --git a/ee/app/finders/security/vulnerability_elastic_base_finder.rb b/ee/app/finders/security/vulnerability_elastic_base_finder.rb index 4eee48ed963023ec00d4f2f4a8a9644e4b225fd0..d980a5d9e5505546fbd4e230b36704cc035a3de8 100644 --- a/ee/app/finders/security/vulnerability_elastic_base_finder.rb +++ b/ee/app/finders/security/vulnerability_elastic_base_finder.rb @@ -76,6 +76,7 @@ def initialize_search_params group_by: params[:group_by], identifier_name: params[:identifier_name], reachability: reachability, + policy_violations: policy_violations, sort: sort } end @@ -135,5 +136,12 @@ def es_search_options def root_ancestor_ids [vulnerable.root_ancestor.id] end + + def policy_violations + return unless params[:policy_violations] + + ::Search::Elastic::Preloaders::Vulnerability::PolicyViolations::VIOLATIONS_TYPES + .slice(*params[:policy_violations]).values + end end end diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index 1e2440436c1e99ee8f93eb6ac42e4814261895fb..24c082195f186d3578d8a5af61c8d3d3381c1069 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -105,6 +105,14 @@ class VulnerabilitiesResolver < VulnerabilitiesBaseResolver experiment: { milestone: '18.2' }, description: 'Filter vulnerabilities by reachability.' + argument :policy_violations, [::Types::SecurityOrchestration::PolicyViolationsEnum], + required: false, + experiment: { milestone: '18.4' }, + description: 'Filter by security policy violations.' \ + 'To use this argument, you must have Elasticsearch configured and the ' \ + '`advanced_vulnerability_management` feature flag enabled. ' \ + 'Not supported on Instance Security Dashboard queries.' + def resolve_with_lookahead(**args) return Vulnerability.none unless vulnerable&.feature_available?(:security_dashboard) diff --git a/ee/app/graphql/resolvers/vulnerability_filterable.rb b/ee/app/graphql/resolvers/vulnerability_filterable.rb index de224114398b118f0691dbc5944148be15679ffa..9c4f55ab8fb8e41c8b1e11376425d8ed6f85d971 100644 --- a/ee/app/graphql/resolvers/vulnerability_filterable.rb +++ b/ee/app/graphql/resolvers/vulnerability_filterable.rb @@ -8,7 +8,7 @@ module VulnerabilityFilterable private - ADVANCED_FILTERS = [:owasp_top_10_2021, :identifier_name, :reachability].freeze + ADVANCED_FILTERS = [:owasp_top_10_2021, :identifier_name, :reachability, :policy_violations].freeze def validate_filters(filters) # identifier_name is also supported on postgres @@ -19,6 +19,8 @@ def validate_filters(filters) validate_reachability!(vulnerable) if filters[:reachability].present? + validate_policy_violations!(vulnerable) if filters[:policy_violations].present? + # Identifier validation should only run for # 1. GitLab .com and Dedicated if ES is not available # 2. GitLab Self-managed @@ -71,5 +73,14 @@ def validate_reachability!(vulnerable) 'The \'reachability\' argument is not currently supported on security center dashboard or ' \ 'the required migrations are not completed.' end + + def validate_policy_violations!(vulnerable) + valid_vulnerable = vulnerable.is_a?(Project) || vulnerable.is_a?(Group) + + return if valid_vulnerable + + raise ::Gitlab::Graphql::Errors::ArgumentError, + 'The \'policy_violations\' argument is not currently supported on security center dashboard' + end end end diff --git a/ee/app/graphql/types/security_orchestration/policy_violations_enum.rb b/ee/app/graphql/types/security_orchestration/policy_violations_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..0d8b71b3e20149fd045294a5986f3562aa7d9e7a --- /dev/null +++ b/ee/app/graphql/types/security_orchestration/policy_violations_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module SecurityOrchestration # rubocop:disable Gitlab/BoundedContexts -- Existing module + class PolicyViolationsEnum < BaseEnum + graphql_name 'PolicyViolations' + + value 'DISMISSED_IN_MR', + description: 'Dismissed in Merge request bypass reason.', + value: :dismissed_in_mr + end + end +end diff --git a/ee/app/models/security/policy_dismissal.rb b/ee/app/models/security/policy_dismissal.rb index 546a98a62bb575953f74abc8300b8b7b095f8637..17acd2df49e80d2ea7c4f957190e80e737751b7f 100644 --- a/ee/app/models/security/policy_dismissal.rb +++ b/ee/app/models/security/policy_dismissal.rb @@ -19,6 +19,11 @@ class PolicyDismissal < ApplicationRecord validates :comment, length: { maximum: 255 }, allow_nil: true validate :dismissal_types_are_valid + scope :for_projects, ->(project_ids) { where(project_id: project_ids) } + scope :for_security_findings_uuids, ->(security_findings_uuids) do + where("security_findings_uuids && ARRAY[?]::text[]", security_findings_uuids) + end + private def dismissal_types_are_valid diff --git a/ee/elastic/docs/20251003104903_add_policy_violations_field_to_vulnerability.yml b/ee/elastic/docs/20251003104903_add_policy_violations_field_to_vulnerability.yml new file mode 100644 index 0000000000000000000000000000000000000000..e8f050e70b80a75af7c96e59c61760930d3b0d7e --- /dev/null +++ b/ee/elastic/docs/20251003104903_add_policy_violations_field_to_vulnerability.yml @@ -0,0 +1,10 @@ +--- +name: AddPolicyViolationsFieldToVulnerability +version: '20251003104903' +description: Adds policy_violations field to the Vulnerability index. +group: group::security policies +milestone: '18.5' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/202622 +obsolete: false +marked_obsolete_by_url: +marked_obsolete_in_milestone: diff --git a/ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb b/ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c3f69604aaca31fe1707e550a07f3ac04b85d32 --- /dev/null +++ b/ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddPolicyViolationsFieldToVulnerability < Elastic::Migration + include ::Search::Elastic::MigrationUpdateMappingsHelper + + DOCUMENT_TYPE = Vulnerability + + private + + def new_mappings + { + policy_violations: { + type: 'short' + } + } + end +end diff --git a/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb b/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb index 97b3b8254090db96f164c3992e8f13cbaaf93278..48f9937a3723dc4003c3d715942a70a42738c5c0 100644 --- a/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb +++ b/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb @@ -33,6 +33,7 @@ def preload_and_enhance! def preload_all_data @reachability_data = Reachability.new(records).preload @token_status_data = TokenStatus.new(records).preload + @policy_violations_data = PolicyViolations.new(records).preload end # Create enhanced proxies and assign them to references @@ -52,7 +53,8 @@ def create_enhanced_proxy(record) vulnerability_id = record.vulnerability_id enhancements = { 'reachability' => fetch_reachability_value(vulnerability_id), - 'token_status' => fetch_token_status_value(vulnerability_id) + 'token_status' => fetch_token_status_value(vulnerability_id), + 'policy_violations' => fetch_policy_violation_value(vulnerability_id) }.with_indifferent_access ::Search::Elastic::RecordProxy::Vulnerability.create_with_enhancements(record, enhancements) @@ -65,6 +67,10 @@ def fetch_reachability_value(vulnerability_id) def fetch_token_status_value(vulnerability_id) @token_status_data[vulnerability_id] || ::Security::TokenStatus::UNKNOWN end + + def fetch_policy_violation_value(vulnerability_id) + @policy_violations_data[vulnerability_id] + end end end end diff --git a/ee/lib/search/elastic/preloaders/vulnerability/policy_violations.rb b/ee/lib/search/elastic/preloaders/vulnerability/policy_violations.rb new file mode 100644 index 0000000000000000000000000000000000000000..e81459126671e0af235aef7f40913aa8df800662 --- /dev/null +++ b/ee/lib/search/elastic/preloaders/vulnerability/policy_violations.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Search + module Elastic + module Preloaders + module Vulnerability + class PolicyViolations < Base + VIOLATIONS_TYPES = { + dismissed_in_mr: 0 + }.freeze + + def perform_preload + security_findings_uuids = records.map(&:uuid) + return {} if security_findings_uuids.empty? + + fetch_policy_violations_data(security_findings_uuids) + end + + private + + def fetch_policy_violations_data(security_findings_uuids) + project_ids = records.map(&:project_id) + + dismissed_uuids = ::Security::PolicyDismissal + .for_projects(project_ids) + .for_security_findings_uuids(security_findings_uuids) + .flat_map(&:security_findings_uuids).uniq + + records.each_with_object({}) do |record, result| + result[record.vulnerability_id] = + dismissed_uuids.include?(record.uuid) ? VIOLATIONS_TYPES[:dismissed_in_mr] : nil + end + end + end + end + end + end +end diff --git a/ee/lib/search/elastic/record_proxy/vulnerability.rb b/ee/lib/search/elastic/record_proxy/vulnerability.rb index 3d3d7370dd393822379332af4ad3efe6ac5dccf4..b67860896a9369864d26f3c8b976523f463819be 100644 --- a/ee/lib/search/elastic/record_proxy/vulnerability.rb +++ b/ee/lib/search/elastic/record_proxy/vulnerability.rb @@ -5,7 +5,7 @@ module Elastic module RecordProxy # Vulnerability-specific record proxy that provides optimized access # to elasticsearch indexing data including EPSS scores, CVE values, - # reachability and token status information. + # reachability, token status, and policy_violations information. class Vulnerability < Base # Creates a vulnerability proxy with all optimizations applied def self.create_with_enhancements(record, enhancements) @@ -13,7 +13,8 @@ def self.create_with_enhancements(record, enhancements) proxy.enhance_with_data({ reachability: enhancements[:reachability], - token_status: enhancements[:token_status] + token_status: enhancements[:token_status], + policy_violations: enhancements[:policy_violations] }) proxy diff --git a/ee/lib/search/elastic/references/vulnerability.rb b/ee/lib/search/elastic/references/vulnerability.rb index 6703054034811f556e7cf6ccf1d31baff40d950e..32e30f304ad77d1970ed49e794141764ac229112 100644 --- a/ee/lib/search/elastic/references/vulnerability.rb +++ b/ee/lib/search/elastic/references/vulnerability.rb @@ -7,7 +7,7 @@ class Vulnerability < Reference include Search::Elastic::Concerns::DatabaseReference include ::Gitlab::Utils::StrongMemoize - SCHEMA_VERSION = 25_37 + SCHEMA_VERSION = 25_38 DOC_TYPE = 'vulnerability' INDEX_NAME = 'vulnerabilities' @@ -70,6 +70,7 @@ def serialize self.class.join_delimited([klass, identifier, routing].compact) end + # rubocop: disable Metrics/AbcSize -- TODO # Generate the JSON representation for elasticsearch indexing override :as_indexed_json def as_indexed_json @@ -96,6 +97,10 @@ def as_indexed_json fields["token_status"] = fetch_record_attribute(database_record, :token_status) end + if policy_violations_migration_finished? + fields["policy_violations"] = fetch_record_attribute(database_record, :policy_violations) + end + if resolved_at_dismissed_at_migration_completed? fields["resolved_at"] = database_record.vulnerability.resolved_at fields["dismissed_at"] = database_record.vulnerability.dismissed_at @@ -103,6 +108,7 @@ def as_indexed_json internal_es_fields.merge(fields) end + # rubocop: enable Metrics/AbcSize override :index_name def index_name @@ -134,8 +140,10 @@ def internal_es_fields end def fetch_schema_version - if token_status_migration_finished? + if policy_violations_migration_finished? SCHEMA_VERSION + elsif token_status_migration_finished? + 25_37 elsif resolved_at_dismissed_at_migration_completed? 25_36 elsif reachability_migration_finished? @@ -157,6 +165,10 @@ def token_status_migration_finished? ) end + def policy_violations_migration_finished? + ::Elastic::DataMigrationService.migration_has_finished?(:add_policy_violations_field_to_vulnerability) + end + def resolved_at_dismissed_at_migration_completed? ::Elastic::DataMigrationService.migration_has_finished?( :add_resolved_at_dismissed_at_fields_to_vulnerability) diff --git a/ee/lib/search/elastic/types/vulnerability.rb b/ee/lib/search/elastic/types/vulnerability.rb index 9866d03ffa2590f2ad1f045ddf1f990966599bf5..deb2cd3a23fa48b3c67f5e049760474b2561eee7 100644 --- a/ee/lib/search/elastic/types/vulnerability.rb +++ b/ee/lib/search/elastic/types/vulnerability.rb @@ -73,6 +73,7 @@ def base_mappings reachability: { type: 'short' }, # enum token_status: { type: 'short' }, # enum risk_score: { type: 'float' }, + policy_violations: { type: 'short' }, # enum schema_version: { type: 'short' } } end diff --git a/ee/lib/search/elastic/vulnerability_filters.rb b/ee/lib/search/elastic/vulnerability_filters.rb index 8962bc3f4aa54e71e33593236eabf2a23f111bc0..a94de3a3cbd1b1340cd9da9255c406290ede70f3 100644 --- a/ee/lib/search/elastic/vulnerability_filters.rb +++ b/ee/lib/search/elastic/vulnerability_filters.rb @@ -416,6 +416,22 @@ def by_reachability(query_hash:, options:) end end + def by_policy_violations(query_hash:, options:) + policy_violations = options[:policy_violations] + return query_hash if policy_violations.blank? + + context.name(:filters) do + add_filter(query_hash, :query, :bool, :filter) do + { + terms: { + _name: context.name(:policy_violations), + policy_violations: policy_violations + } + } + end + end + end + private def valid_owasp_values?(owasp_values, regex_constant) diff --git a/ee/lib/search/elastic/vulnerability_query_builder.rb b/ee/lib/search/elastic/vulnerability_query_builder.rb index f1e7cd0f362647efb56b627f345c7e97c264c1e4..dfb527b9c6c544f9cf9f6999e2f3b43344ac2ff6 100644 --- a/ee/lib/search/elastic/vulnerability_query_builder.rb +++ b/ee/lib/search/elastic/vulnerability_query_builder.rb @@ -48,6 +48,11 @@ def build # rubocop:disable Metrics/AbcSize -- need all the filters in one place query_hash: query_hash, options: options) end + if ::Elastic::DataMigrationService.migration_has_finished?(:add_policy_violations_field_to_vulnerability) + query_hash = ::Search::Elastic::VulnerabilityFilters.by_policy_violations( + query_hash: query_hash, options: options) + end + query_hash = ::Search::Elastic::VulnerabilityAggregations.by_severity_counts( query_hash: query_hash, options: options) query_hash = ::Search::Elastic::VulnerabilityAggregations.by_identifiers_search( diff --git a/ee/spec/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability_spec.rb b/ee/spec/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c19966d535ea4b04dab36dacf11847124024fb5c --- /dev/null +++ b/ee/spec/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require File.expand_path('ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb') + +RSpec.describe AddPolicyViolationsFieldToVulnerability, :elastic, feature_category: :security_policy_management do + let(:version) { 20251003104903 } + + include_examples 'migration adds mapping' +end diff --git a/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb b/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb index 42d5627a5a8330585ac18d6af5cae7f4ef14ad52..1ce00f3d8d8ca340d2d6bf43693e67a10db898ad 100644 --- a/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb @@ -290,6 +290,17 @@ end end end + + context 'when filtering vulnerabilities with policy_violations', :elastic do + let(:params) { { policy_violations: ['DISMISSED_IN_MR'] } } + let(:error_msg) { "Feature is not supported for InstanceSecurityDashboard" } + + it 'raises an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, s_(error_msg)) do + resolved + end + end + end end context 'when image is given' do @@ -520,6 +531,52 @@ end end + context 'when filtering vulnerabilities with policy_violations', :elastic do + let(:params) { { policy_violations: ['DISMISSED_IN_MR'] } } + + context 'without elasticsearch' do + before do + allow(::Search::Elastic::VulnerabilityIndexingHelper).to receive(:vulnerability_indexing_allowed?).and_return(false) + end + + it_behaves_like 'raises ES errors' + end + + context 'with advanced_vulnerability_management FF disabled' do + before do + allow(::Search::Elastic::VulnerabilityIndexingHelper).to receive(:vulnerability_indexing_allowed?).and_return(true) + stub_feature_flags(advanced_vulnerability_management: false) + end + + it_behaves_like 'raises ES errors' + end + + context 'with elastic search' do + let_it_be(:dismissed_vulnerability_read) { create(:vulnerability_read, project: project) } + + let_it_be(:non_dismissed_vulnerability_read) { create(:vulnerability_read, project: project) } + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + + create(:policy_dismissal, project: project, security_findings_uuids: [dismissed_vulnerability_read.uuid]) + + Elastic::ProcessBookkeepingService.track!(dismissed_vulnerability_read, non_dismissed_vulnerability_read) + ensure_elasticsearch_index! + + allow(current_user).to receive(:can?).with(:access_advanced_vulnerability_management, vulnerable).and_return(true) + end + + it 'only returns vulnerabilities with matching policy_violations types' do + expect(Gitlab::Search::Client).to receive(:execute_search).and_call_original + + results = resolved.to_a + + expect(results).to match_array([dismissed_vulnerability_read].map(&:vulnerability)) + end + end + end + context 'when identifer_name is given' do let_it_be(:identifier_name) { 'CVE-2024-1234' } diff --git a/ee/spec/lib/search/elastic/preloaders/vulnerability/policy_violations_spec.rb b/ee/spec/lib/search/elastic/preloaders/vulnerability/policy_violations_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..aae7424fd45758e59aedd8eaa1b896b2db7eea04 --- /dev/null +++ b/ee/spec/lib/search/elastic/preloaders/vulnerability/policy_violations_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Search::Elastic::Preloaders::Vulnerability::PolicyViolations, feature_category: :vulnerability_management do + let_it_be(:project) { create(:project) } + let_it_be(:reads_by_policy_violation_type) do + { + dismissed_in_mr: create_read_with_policy_violations(true, project), + no_violations: create_read_with_policy_violations(false, project) + } + end + + def create_read_with_policy_violations(has_violations, project) + vulnerability = create(:vulnerability, :with_finding, project: project) + + create(:policy_dismissal, project: project, security_findings_uuids: [vulnerability.finding.uuid]) if has_violations + + vulnerability.vulnerability_read + end + + describe '#preload' do + subject(:preloader) { described_class.new(records) } + + context 'with vulnerability records having different policy violation types' do + let(:records) { reads_by_policy_violation_type.values } + + it 'returns policy violation status for vulnerabilities' do + result = preloader.preload + + vulnerability_dismissed_in_mr = reads_by_policy_violation_type[:dismissed_in_mr].vulnerability_id + vulnerability_without_dismissal = reads_by_policy_violation_type[:no_violations].vulnerability_id + + expect(result[vulnerability_dismissed_in_mr]).to eq(described_class::VIOLATIONS_TYPES[:dismissed_in_mr]) + expect(result[vulnerability_without_dismissal]).to be_nil + end + end + + context 'with edge cases' do + context 'when records are empty' do + let(:records) { [] } + + it 'returns empty hash' do + expect(preloader.preload).to eq({}) + end + end + + context 'when database query fails' do + let(:records) { [reads_by_policy_violation_type[:dismissed_in_mr]] } + + before do + allow(::Security::PolicyDismissal).to receive(:by_security_findings_uuids).and_raise(StandardError, + 'Database error') + allow(::Gitlab::ErrorTracking).to receive(:track_exception) + end + + it 'handles errors gracefully and tracks exceptions' do + result = preloader.preload + + expect(result).to eq({}) + expect(::Gitlab::ErrorTracking).to have_received(:track_exception) + .with(instance_of(StandardError), class: described_class.name) + end + end + end + end +end diff --git a/ee/spec/lib/search/elastic/types/vulnerability_spec.rb b/ee/spec/lib/search/elastic/types/vulnerability_spec.rb index 8b818c4a430339dae6e4d21582dded4128725709..f4450e705ae77dbfaf830eb218ceeae8e73df236 100644 --- a/ee/spec/lib/search/elastic/types/vulnerability_spec.rb +++ b/ee/spec/lib/search/elastic/types/vulnerability_spec.rb @@ -35,6 +35,7 @@ :reachability, :token_status, :risk_score, + :policy_violations, :schema_version] end