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