From 3bff93c3005371bbc3b1064f46d348dc080b712c Mon Sep 17 00:00:00 2001 From: mc_rocha Date: Fri, 22 Aug 2025 14:06:23 -0400 Subject: [PATCH 1/5] Add security_policy_dismissals table This MR adds a new database table to stores the relation between security policies and dismissed findings. Changelog: added --- ..._foreign_key_to_security_policy_dismissals.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb diff --git a/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb b/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb new file mode 100644 index 00000000000000..f30a84249d6c23 --- /dev/null +++ b/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddProjectForeignKeyToSecurityPolicyDismissals < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.4' + + def up + add_concurrent_foreign_key :security_policy_dismissals, :projects, + column: :project_id, + on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :security_policy_dismissals, column: :project_id + end +end -- GitLab From ce05d141355152c41955e945d5221196507e6dd6 Mon Sep 17 00:00:00 2001 From: mc_rocha Date: Mon, 25 Aug 2025 10:07:30 -0400 Subject: [PATCH 2/5] Filter vulnerabilities dismissed by bypass reason --- ...dd_dismissal_reason_to_policy_dismissal.rb | 13 +++++++ ...reign_key_to_security_policy_dismissals.rb | 16 --------- db/schema_migrations/20250828195016 | 1 + doc/api/graphql/reference/_index.md | 10 ++++++ .../security/vulnerability_reads_finder.rb | 10 ++++++ .../resolvers/vulnerabilities_resolver.rb | 4 +++ .../security_policy_bypass_reason_enum.rb | 17 ++++++++++ .../factories/security/policy_dismissal.rb | 8 +++++ .../models/security/policy_dismissal_spec.rb | 34 +++++++++++++++++++ 9 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb delete mode 100644 db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb create mode 100644 db/schema_migrations/20250828195016 create mode 100644 ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb diff --git a/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb b/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb new file mode 100644 index 00000000000000..a4ab0666eeb9eb --- /dev/null +++ b/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddDismissalReasonToPolicyDismissal < Gitlab::Database::Migration[2.3] + milestone '18.4' + + def up + add_column :security_policy_dismissals, :dismissal_reason, :smallint, default: 0, null: false, if_not_exists: true + end + + def down + remove_column :security_policy_dismissals, :dismissal_reason, if_exists: true + end +end diff --git a/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb b/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb deleted file mode 100644 index f30a84249d6c23..00000000000000 --- a/db/post_migrate/20250822200745_add_project_foreign_key_to_security_policy_dismissals.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class AddProjectForeignKeyToSecurityPolicyDismissals < Gitlab::Database::Migration[2.3] - disable_ddl_transaction! - milestone '18.4' - - def up - add_concurrent_foreign_key :security_policy_dismissals, :projects, - column: :project_id, - on_delete: :cascade - end - - def down - remove_foreign_key_if_exists :security_policy_dismissals, column: :project_id - end -end diff --git a/db/schema_migrations/20250828195016 b/db/schema_migrations/20250828195016 new file mode 100644 index 00000000000000..7f11e520eea4d1 --- /dev/null +++ b/db/schema_migrations/20250828195016 @@ -0,0 +1 @@ +b0e2bd239302dbcf6159d10b0c873ca0e0be698a35d286ea7ed6a311e2396dbc \ No newline at end of file diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 090d0cd5594530..14ee2d518aed1d 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2010,6 +2010,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | +| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -32441,6 +32442,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | +| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -41139,6 +41141,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | +| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -50231,6 +50234,13 @@ Template type for predefined security categories. | `BUSINESS_UNIT` | Business unit category. | | `EXPOSURE` | Exposure category. | +### `SecurityPolicyBypassReason` + +| Value | Description | +| ----- | ----------- | +| `DISMISSED_IN_MR` | Dismissed in Merge request bypass reason. | +| `RISK_ACCEPTED_IN_MR` | Risk accepted in Merge request bypass reason. | + ### `SecurityPolicyRelationType` | Value | Description | diff --git a/ee/app/finders/security/vulnerability_reads_finder.rb b/ee/app/finders/security/vulnerability_reads_finder.rb index 6c8b3fc5566383..1f64294cbae0d4 100644 --- a/ee/app/finders/security/vulnerability_reads_finder.rb +++ b/ee/app/finders/security/vulnerability_reads_finder.rb @@ -62,6 +62,7 @@ def execute filter_by_has_remediations filter_by_owasp_top_10 filter_by_identifier_name + filter_by_security_policy_dismissal_reason sort end @@ -205,6 +206,15 @@ def filter_by_has_remediations @vulnerability_reads = vulnerability_reads.with_remediations(params[:has_remediations]) end + def filter_by_security_policy_dismissal_reason + return unless params[:security_policy_bypass_reason].present? + + uuids = ::Security::PolicyDismissal.by_dismissal_reason(params[:security_policy_bypass_reason]) + .pluck(:security_findings_uuids) # rubocop:disable CodeReuse/ActiveRecord, Database/AvoidUsingPluckWithoutLimit -- avoids cross-join + + @vulnerability_reads = vulnerability_reads.by_uuid(uuids) + end + def sort if vulnerable_is_a_group? @vulnerability_reads.order_by_params_and_traversal_ids(params[:sort]) diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index 1e2440436c1e99..25f5b3e2be34cb 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -105,6 +105,10 @@ class VulnerabilitiesResolver < VulnerabilitiesBaseResolver experiment: { milestone: '18.2' }, description: 'Filter vulnerabilities by reachability.' + argument :security_policy_bypass_reason, [::Types::SecurityOrchestration::SecurityPolicyBypassReasonEnum], + required: false, + description: "Filter by security policy bypass reason." + def resolve_with_lookahead(**args) return Vulnerability.none unless vulnerable&.feature_available?(:security_dashboard) diff --git a/ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb b/ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb new file mode 100644 index 00000000000000..2d57663b2fad89 --- /dev/null +++ b/ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module SecurityOrchestration # rubocop:disable Gitlab/BoundedContexts -- Existing module + class SecurityPolicyBypassReasonEnum < BaseEnum + graphql_name 'SecurityPolicyBypassReason' + + value 'DISMISSED_IN_MR', + description: 'Dismissed in Merge request bypass reason.', + value: :dismissed_in_mr + + value 'RISK_ACCEPTED_IN_MR', + description: 'Risk accepted in Merge request bypass reason.', + value: :risk_accepted_in_mr + end + end +end diff --git a/ee/spec/factories/security/policy_dismissal.rb b/ee/spec/factories/security/policy_dismissal.rb index 9e5cd02039490c..aa51c39617e384 100644 --- a/ee/spec/factories/security/policy_dismissal.rb +++ b/ee/spec/factories/security/policy_dismissal.rb @@ -9,4 +9,12 @@ dismissal_types { Security::PolicyDismissal::DISMISSAL_TYPES.values.sample(2) } security_findings_uuids { [SecureRandom.uuid] } end + + trait :dismissed_in_mr do + dismissal_reason { :dismissed_in_mr } + end + + trait :risk_accepted_in_mr do + dismissal_reason { :risk_accepted_in_mr } + end end diff --git a/ee/spec/models/security/policy_dismissal_spec.rb b/ee/spec/models/security/policy_dismissal_spec.rb index deefb0282b4c70..29e5e1e8f8930e 100644 --- a/ee/spec/models/security/policy_dismissal_spec.rb +++ b/ee/spec/models/security/policy_dismissal_spec.rb @@ -10,6 +10,14 @@ it { is_expected.to belong_to(:user).optional } end + describe 'enums' do + let(:dismissal_reasons) do + { dismissed_in_mr: 0, risk_accepted_in_mr: 1, other: 2 } + end + + it { is_expected.to define_enum_for(:dismissal_reason).with_values(dismissal_reasons) } + end + describe 'validations' do subject(:policy_dismissal) { create(:policy_dismissal) } @@ -37,4 +45,30 @@ end end end + + describe 'scopes' do + describe '.by_dismissal_reason' do + subject(:policy_dismissals_by_dismissal_reasons) { described_class.by_dismissal_reason(dismissal_reasons) } + + let_it_be(:policy_dismissal_dismissed_in_mr) { create(:policy_dismissal, :dismissed_in_mr) } + let_it_be(:policy_dismissal_risk_accepted_in_mr) { create(:policy_dismissal, :risk_accepted_in_mr) } + + context 'when filtering by one dismissal reason' do + let(:dismissal_reasons) { [:dismissed_in_mr] } + + it 'returns the policy dismissal with matching dismissal_reason' do + expect(policy_dismissals_by_dismissal_reasons).to match_array([policy_dismissal_dismissed_in_mr]) + end + end + + context 'when filtering by multiple dismissal reasons' do + let(:dismissal_reasons) { [:dismissed_in_mr, :risk_accepted_in_mr] } + + it 'returns the policy dismissal with matching dismissal reasons' do + expect(policy_dismissals_by_dismissal_reasons).to match_array([policy_dismissal_dismissed_in_mr, + policy_dismissal_risk_accepted_in_mr]) + end + end + end + end end -- GitLab From 72726357e59261a4aad4baa1ad4411020ec3b250 Mon Sep 17 00:00:00 2001 From: mc_rocha Date: Mon, 8 Sep 2025 06:40:41 -0400 Subject: [PATCH 3/5] Add policy bypass reason es filter for vulnerabilities --- doc/api/graphql/reference/_index.md | 6 +++--- .../vulnerability_elastic_base_finder.rb | 1 + .../resolvers/vulnerabilities_resolver.rb | 1 + .../search/elastic/vulnerability_filters.rb | 19 +++++++++++++++++++ .../elastic/vulnerability_query_builder.rb | 3 +++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 14ee2d518aed1d..07965f3847e5d4 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2010,7 +2010,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -32442,7 +32442,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -41141,7 +41141,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | diff --git a/ee/app/finders/security/vulnerability_elastic_base_finder.rb b/ee/app/finders/security/vulnerability_elastic_base_finder.rb index 4eee48ed963023..c37181456c07e4 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, + security_policy_bypass_reason: params[:security_policy_bypass_reason], sort: sort } end diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index 25f5b3e2be34cb..63c88e8551a639 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -107,6 +107,7 @@ class VulnerabilitiesResolver < VulnerabilitiesBaseResolver argument :security_policy_bypass_reason, [::Types::SecurityOrchestration::SecurityPolicyBypassReasonEnum], required: false, + experiment: { milestone: '18.4' }, description: "Filter by security policy bypass reason." def resolve_with_lookahead(**args) diff --git a/ee/lib/search/elastic/vulnerability_filters.rb b/ee/lib/search/elastic/vulnerability_filters.rb index 8962bc3f4aa54e..8ed796cb20615b 100644 --- a/ee/lib/search/elastic/vulnerability_filters.rb +++ b/ee/lib/search/elastic/vulnerability_filters.rb @@ -416,6 +416,25 @@ def by_reachability(query_hash:, options:) end end + def by_security_policy_bypass_reasons(query_hash:, options:) + security_policy_bypass_reasons = options[:security_policy_bypass_reason] + return query_hash if security_policy_bypass_reasons.blank? + + uuids = ::Security::PolicyDismissal.by_dismissal_reason(params[:security_policy_bypass_reason]) + .pluck(:security_findings_uuids) # rubocop:disable CodeReuse/ActiveRecord, -- avoids cross-join + + context.name(:filters) do + add_filter(query_hash, :query, :bool, :filter) do + { + terms: { + _name: context.name(:uuids), + uuids: uuids + } + } + 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 f1e7cd0f362647..596a5f3e96abb3 100644 --- a/ee/lib/search/elastic/vulnerability_query_builder.rb +++ b/ee/lib/search/elastic/vulnerability_query_builder.rb @@ -48,6 +48,9 @@ def build # rubocop:disable Metrics/AbcSize -- need all the filters in one place query_hash: query_hash, options: options) end + query_hash = ::Search::Elastic::VulnerabilityFilters.by_security_policy_bypass_reasons( + query_hash: query_hash, options: options) + query_hash = ::Search::Elastic::VulnerabilityAggregations.by_severity_counts( query_hash: query_hash, options: options) query_hash = ::Search::Elastic::VulnerabilityAggregations.by_identifiers_search( -- GitLab From 3ac7b50fd90979b29cc7b7394cb7dc7445cd8f75 Mon Sep 17 00:00:00 2001 From: mc_rocha Date: Tue, 16 Sep 2025 16:40:59 -0400 Subject: [PATCH 4/5] Add security_policy_bypass_reason to vulnerabilities in es --- doc/api/graphql/reference/_index.md | 6 +++--- .../resolvers/vulnerabilities_resolver.rb | 5 ++++- .../resolvers/vulnerability_filterable.rb | 13 +++++++++++- ..._policy_bypass_reason_to_vulnerability.yml | 10 +++++++++ ..._add_rechability_field_to_vulnerability.rb | 2 +- ...y_policy_bypass_reason_to_vulnerability.rb | 21 +++++++++++++++++++ ee/lib/search/elastic/types/vulnerability.rb | 1 + .../search/elastic/vulnerability_filters.rb | 11 ++++------ ...icy_bypass_reason_to_vulnerability_spec.rb | 10 +++++++++ 9 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml create mode 100644 ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb create mode 100644 ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 07965f3847e5d4..4aacdee5c10923 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2010,7 +2010,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -32442,7 +32442,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -41141,7 +41141,7 @@ four standard [pagination arguments](#pagination-arguments): | `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason. | +| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index 63c88e8551a639..51324293a65ce0 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -108,7 +108,10 @@ class VulnerabilitiesResolver < VulnerabilitiesBaseResolver argument :security_policy_bypass_reason, [::Types::SecurityOrchestration::SecurityPolicyBypassReasonEnum], required: false, experiment: { milestone: '18.4' }, - description: "Filter by security policy bypass reason." + description: 'Filter by security policy bypass reason.' \ + '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 de224114398b11..c2dd2981933cca 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, :security_policy_bypass_reason].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_security_policy_bypass_reason!(vulnerable) if filters[:security_policy_bypass_reason].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_security_policy_bypass_reason!(vulnerable) + valid_vulnerable = vulnerable.is_a?(Project) || vulnerable.is_a?(Group) + + return if valid_vulnerable + + raise ::Gitlab::Graphql::Errors::ArgumentError, + 'The \'security_policy_bypass_reason\' argument is not currently supported on security center dashboard' + end end end diff --git a/ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml b/ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml new file mode 100644 index 00000000000000..30bbc78baf45a4 --- /dev/null +++ b/ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml @@ -0,0 +1,10 @@ +--- +name: AddSecurityPolicyBypassReasonToVulnerability +version: '20250916144308' +description: Adds security_policy_bypass_reason field to the Vulnerability index. +group: group::group::security policies +milestone: '18.4' +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/20250606204649_add_rechability_field_to_vulnerability.rb b/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb index b68d016374665b..3a1f70f494a2a7 100644 --- a/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb +++ b/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb @@ -13,7 +13,7 @@ def index_name def new_mappings { - reachability: { + security_policy_bypass_reason: { type: 'short' } } diff --git a/ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb b/ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb new file mode 100644 index 00000000000000..44d624544cff2b --- /dev/null +++ b/ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddSecurityPolicyBypassReasonToVulnerability < Elastic::Migration + include ::Search::Elastic::MigrationUpdateMappingsHelper + + DOCUMENT_TYPE = Vulnerability + + private + + def index_name + ::Search::Elastic::Types::Vulnerability.index_name + end + + def new_mappings + { + your_field_name: { + type: 'short' + } + } + end +end diff --git a/ee/lib/search/elastic/types/vulnerability.rb b/ee/lib/search/elastic/types/vulnerability.rb index 9866d03ffa2590..b1b08b77ae92c1 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' }, + security_policy_bypass_reason: { 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 8ed796cb20615b..831efecd123a9c 100644 --- a/ee/lib/search/elastic/vulnerability_filters.rb +++ b/ee/lib/search/elastic/vulnerability_filters.rb @@ -417,18 +417,15 @@ def by_reachability(query_hash:, options:) end def by_security_policy_bypass_reasons(query_hash:, options:) - security_policy_bypass_reasons = options[:security_policy_bypass_reason] - return query_hash if security_policy_bypass_reasons.blank? - - uuids = ::Security::PolicyDismissal.by_dismissal_reason(params[:security_policy_bypass_reason]) - .pluck(:security_findings_uuids) # rubocop:disable CodeReuse/ActiveRecord, -- avoids cross-join + security_policy_bypass_reason = options[:security_policy_bypass_reason] + return query_hash if security_policy_bypass_reason.blank? context.name(:filters) do add_filter(query_hash, :query, :bool, :filter) do { terms: { - _name: context.name(:uuids), - uuids: uuids + _name: context.name(:security_policy_bypass_reasons), + security_policy_bypass_reason: security_policy_bypass_reason } } end diff --git a/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb b/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb new file mode 100644 index 00000000000000..4c7f14af6f525b --- /dev/null +++ b/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require File.expand_path('ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb') + +RSpec.describe AddSecurityPolicyBypassReasonToVulnerability, feature_category: :security_policy_management do + let(:version) { 20250916144308 } + + include_examples 'migration adds mapping' +end -- GitLab From 2cc353f211bf0f7ba87798432c541d788845a25e Mon Sep 17 00:00:00 2001 From: mc_rocha Date: Fri, 26 Sep 2025 09:27:03 -0400 Subject: [PATCH 5/5] Ingest policy_violations for vulnerabilities in ES --- ...dd_dismissal_reason_to_policy_dismissal.rb | 13 ---- db/schema_migrations/20250828195016 | 1 - doc/api/graphql/reference/_index.md | 19 +++--- .../vulnerability_elastic_base_finder.rb | 9 ++- .../security/vulnerability_reads_finder.rb | 10 --- .../resolvers/vulnerabilities_resolver.rb | 4 +- .../resolvers/vulnerability_filterable.rb | 8 +-- ...ason_enum.rb => policy_violations_enum.rb} | 8 +-- ee/app/models/security/policy_dismissal.rb | 5 ++ ..._policy_bypass_reason_to_vulnerability.yml | 10 --- ...licy_violations_field_to_vulnerability.yml | 10 +++ ..._add_rechability_field_to_vulnerability.rb | 2 +- ...licy_violations_field_to_vulnerability.rb} | 8 +-- .../vulnerability/enhanced_proxy.rb | 8 ++- .../vulnerability/policy_violations.rb | 38 +++++++++++ .../elastic/record_proxy/vulnerability.rb | 5 +- .../elastic/references/vulnerability.rb | 16 ++++- ee/lib/search/elastic/types/vulnerability.rb | 2 +- .../search/elastic/vulnerability_filters.rb | 10 +-- .../elastic/vulnerability_query_builder.rb | 6 +- ...icy_bypass_reason_to_vulnerability_spec.rb | 10 --- ..._violations_field_to_vulnerability_spec.rb | 10 +++ .../factories/security/policy_dismissal.rb | 8 --- .../vulnerabilities_resolver_spec.rb | 57 ++++++++++++++++ .../vulnerability/policy_violations_spec.rb | 67 +++++++++++++++++++ .../elastic/types/vulnerability_spec.rb | 1 + .../models/security/policy_dismissal_spec.rb | 34 ---------- 27 files changed, 250 insertions(+), 129 deletions(-) delete mode 100644 db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb delete mode 100644 db/schema_migrations/20250828195016 rename ee/app/graphql/types/security_orchestration/{security_policy_bypass_reason_enum.rb => policy_violations_enum.rb} (53%) delete mode 100644 ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml create mode 100644 ee/elastic/docs/20251003104903_add_policy_violations_field_to_vulnerability.yml rename ee/elastic/migrate/{20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb => 20251003104903_add_policy_violations_field_to_vulnerability.rb} (53%) create mode 100644 ee/lib/search/elastic/preloaders/vulnerability/policy_violations.rb delete mode 100644 ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb create mode 100644 ee/spec/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability_spec.rb create mode 100644 ee/spec/lib/search/elastic/preloaders/vulnerability/policy_violations_spec.rb diff --git a/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb b/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb deleted file mode 100644 index a4ab0666eeb9eb..00000000000000 --- a/db/migrate/20250828195016_add_dismissal_reason_to_policy_dismissal.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class AddDismissalReasonToPolicyDismissal < Gitlab::Database::Migration[2.3] - milestone '18.4' - - def up - add_column :security_policy_dismissals, :dismissal_reason, :smallint, default: 0, null: false, if_not_exists: true - end - - def down - remove_column :security_policy_dismissals, :dismissal_reason, if_exists: true - end -end diff --git a/db/schema_migrations/20250828195016 b/db/schema_migrations/20250828195016 deleted file mode 100644 index 7f11e520eea4d1..00000000000000 --- a/db/schema_migrations/20250828195016 +++ /dev/null @@ -1 +0,0 @@ -b0e2bd239302dbcf6159d10b0c873ca0e0be698a35d286ea7ed6a311e2396dbc \ No newline at end of file diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4aacdee5c10923..45eda28ee178e0 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2005,12 +2005,12 @@ 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. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -32437,12 +32437,12 @@ 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. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -41136,12 +41136,12 @@ 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. | | `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. | -| `securityPolicyBypassReason` {{< icon name="warning-solid" >}} | [`[SecurityPolicyBypassReason!]`](#securitypolicybypassreason) | **Introduced** in GitLab 18.4. **Status**: Experiment. Filter by security policy bypass reason.To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. | | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | @@ -49876,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. @@ -50234,13 +50240,6 @@ Template type for predefined security categories. | `BUSINESS_UNIT` | Business unit category. | | `EXPOSURE` | Exposure category. | -### `SecurityPolicyBypassReason` - -| Value | Description | -| ----- | ----------- | -| `DISMISSED_IN_MR` | Dismissed in Merge request bypass reason. | -| `RISK_ACCEPTED_IN_MR` | Risk accepted in Merge request bypass reason. | - ### `SecurityPolicyRelationType` | Value | Description | diff --git a/ee/app/finders/security/vulnerability_elastic_base_finder.rb b/ee/app/finders/security/vulnerability_elastic_base_finder.rb index c37181456c07e4..d980a5d9e55055 100644 --- a/ee/app/finders/security/vulnerability_elastic_base_finder.rb +++ b/ee/app/finders/security/vulnerability_elastic_base_finder.rb @@ -76,7 +76,7 @@ def initialize_search_params group_by: params[:group_by], identifier_name: params[:identifier_name], reachability: reachability, - security_policy_bypass_reason: params[:security_policy_bypass_reason], + policy_violations: policy_violations, sort: sort } end @@ -136,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/finders/security/vulnerability_reads_finder.rb b/ee/app/finders/security/vulnerability_reads_finder.rb index 1f64294cbae0d4..6c8b3fc5566383 100644 --- a/ee/app/finders/security/vulnerability_reads_finder.rb +++ b/ee/app/finders/security/vulnerability_reads_finder.rb @@ -62,7 +62,6 @@ def execute filter_by_has_remediations filter_by_owasp_top_10 filter_by_identifier_name - filter_by_security_policy_dismissal_reason sort end @@ -206,15 +205,6 @@ def filter_by_has_remediations @vulnerability_reads = vulnerability_reads.with_remediations(params[:has_remediations]) end - def filter_by_security_policy_dismissal_reason - return unless params[:security_policy_bypass_reason].present? - - uuids = ::Security::PolicyDismissal.by_dismissal_reason(params[:security_policy_bypass_reason]) - .pluck(:security_findings_uuids) # rubocop:disable CodeReuse/ActiveRecord, Database/AvoidUsingPluckWithoutLimit -- avoids cross-join - - @vulnerability_reads = vulnerability_reads.by_uuid(uuids) - end - def sort if vulnerable_is_a_group? @vulnerability_reads.order_by_params_and_traversal_ids(params[:sort]) diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index 51324293a65ce0..24c082195f186d 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -105,10 +105,10 @@ class VulnerabilitiesResolver < VulnerabilitiesBaseResolver experiment: { milestone: '18.2' }, description: 'Filter vulnerabilities by reachability.' - argument :security_policy_bypass_reason, [::Types::SecurityOrchestration::SecurityPolicyBypassReasonEnum], + argument :policy_violations, [::Types::SecurityOrchestration::PolicyViolationsEnum], required: false, experiment: { milestone: '18.4' }, - description: 'Filter by security policy bypass reason.' \ + 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.' diff --git a/ee/app/graphql/resolvers/vulnerability_filterable.rb b/ee/app/graphql/resolvers/vulnerability_filterable.rb index c2dd2981933cca..9c4f55ab8fb8e4 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, :security_policy_bypass_reason].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,7 +19,7 @@ def validate_filters(filters) validate_reachability!(vulnerable) if filters[:reachability].present? - validate_security_policy_bypass_reason!(vulnerable) if filters[:security_policy_bypass_reason].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 @@ -74,13 +74,13 @@ def validate_reachability!(vulnerable) 'the required migrations are not completed.' end - def validate_security_policy_bypass_reason!(vulnerable) + 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 \'security_policy_bypass_reason\' argument is not currently supported on security center dashboard' + 'The \'policy_violations\' argument is not currently supported on security center dashboard' end end end diff --git a/ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb b/ee/app/graphql/types/security_orchestration/policy_violations_enum.rb similarity index 53% rename from ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb rename to ee/app/graphql/types/security_orchestration/policy_violations_enum.rb index 2d57663b2fad89..0d8b71b3e20149 100644 --- a/ee/app/graphql/types/security_orchestration/security_policy_bypass_reason_enum.rb +++ b/ee/app/graphql/types/security_orchestration/policy_violations_enum.rb @@ -2,16 +2,12 @@ module Types module SecurityOrchestration # rubocop:disable Gitlab/BoundedContexts -- Existing module - class SecurityPolicyBypassReasonEnum < BaseEnum - graphql_name 'SecurityPolicyBypassReason' + class PolicyViolationsEnum < BaseEnum + graphql_name 'PolicyViolations' value 'DISMISSED_IN_MR', description: 'Dismissed in Merge request bypass reason.', value: :dismissed_in_mr - - value 'RISK_ACCEPTED_IN_MR', - description: 'Risk accepted in Merge request bypass reason.', - value: :risk_accepted_in_mr end end end diff --git a/ee/app/models/security/policy_dismissal.rb b/ee/app/models/security/policy_dismissal.rb index 546a98a62bb575..17acd2df49e80d 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/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml b/ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml deleted file mode 100644 index 30bbc78baf45a4..00000000000000 --- a/ee/elastic/docs/20250916144308_add_security_policy_bypass_reason_to_vulnerability.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: AddSecurityPolicyBypassReasonToVulnerability -version: '20250916144308' -description: Adds security_policy_bypass_reason field to the Vulnerability index. -group: group::group::security policies -milestone: '18.4' -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/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 00000000000000..e8f050e70b80a7 --- /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/20250606204649_add_rechability_field_to_vulnerability.rb b/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb index 3a1f70f494a2a7..b68d016374665b 100644 --- a/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb +++ b/ee/elastic/migrate/20250606204649_add_rechability_field_to_vulnerability.rb @@ -13,7 +13,7 @@ def index_name def new_mappings { - security_policy_bypass_reason: { + reachability: { type: 'short' } } diff --git a/ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb b/ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb similarity index 53% rename from ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb rename to ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb index 44d624544cff2b..1c3f69604aaca3 100644 --- a/ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb +++ b/ee/elastic/migrate/20251003104903_add_policy_violations_field_to_vulnerability.rb @@ -1,19 +1,15 @@ # frozen_string_literal: true -class AddSecurityPolicyBypassReasonToVulnerability < Elastic::Migration +class AddPolicyViolationsFieldToVulnerability < Elastic::Migration include ::Search::Elastic::MigrationUpdateMappingsHelper DOCUMENT_TYPE = Vulnerability private - def index_name - ::Search::Elastic::Types::Vulnerability.index_name - end - def new_mappings { - your_field_name: { + policy_violations: { type: 'short' } } diff --git a/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb b/ee/lib/search/elastic/preloaders/vulnerability/enhanced_proxy.rb index 97b3b8254090db..48f9937a3723dc 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 00000000000000..e81459126671e0 --- /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 3d3d7370dd3938..b67860896a9369 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 6703054034811f..32e30f304ad77d 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 b1b08b77ae92c1..deb2cd3a23fa48 100644 --- a/ee/lib/search/elastic/types/vulnerability.rb +++ b/ee/lib/search/elastic/types/vulnerability.rb @@ -73,7 +73,7 @@ def base_mappings reachability: { type: 'short' }, # enum token_status: { type: 'short' }, # enum risk_score: { type: 'float' }, - security_policy_bypass_reason: { type: 'short' }, # enum + 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 831efecd123a9c..a94de3a3cbd1b1 100644 --- a/ee/lib/search/elastic/vulnerability_filters.rb +++ b/ee/lib/search/elastic/vulnerability_filters.rb @@ -416,16 +416,16 @@ def by_reachability(query_hash:, options:) end end - def by_security_policy_bypass_reasons(query_hash:, options:) - security_policy_bypass_reason = options[:security_policy_bypass_reason] - return query_hash if security_policy_bypass_reason.blank? + 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(:security_policy_bypass_reasons), - security_policy_bypass_reason: security_policy_bypass_reason + _name: context.name(:policy_violations), + policy_violations: policy_violations } } end diff --git a/ee/lib/search/elastic/vulnerability_query_builder.rb b/ee/lib/search/elastic/vulnerability_query_builder.rb index 596a5f3e96abb3..dfb527b9c6c544 100644 --- a/ee/lib/search/elastic/vulnerability_query_builder.rb +++ b/ee/lib/search/elastic/vulnerability_query_builder.rb @@ -48,8 +48,10 @@ def build # rubocop:disable Metrics/AbcSize -- need all the filters in one place query_hash: query_hash, options: options) end - query_hash = ::Search::Elastic::VulnerabilityFilters.by_security_policy_bypass_reasons( - query_hash: query_hash, options: options) + 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) diff --git a/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb b/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb deleted file mode 100644 index 4c7f14af6f525b..00000000000000 --- a/ee/spec/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require File.expand_path('ee/elastic/migrate/20250916144308_add_security_policy_bypass_reason_to_vulnerability.rb') - -RSpec.describe AddSecurityPolicyBypassReasonToVulnerability, feature_category: :security_policy_management do - let(:version) { 20250916144308 } - - include_examples 'migration adds mapping' -end 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 00000000000000..c19966d535ea4b --- /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/factories/security/policy_dismissal.rb b/ee/spec/factories/security/policy_dismissal.rb index aa51c39617e384..9e5cd02039490c 100644 --- a/ee/spec/factories/security/policy_dismissal.rb +++ b/ee/spec/factories/security/policy_dismissal.rb @@ -9,12 +9,4 @@ dismissal_types { Security::PolicyDismissal::DISMISSAL_TYPES.values.sample(2) } security_findings_uuids { [SecureRandom.uuid] } end - - trait :dismissed_in_mr do - dismissal_reason { :dismissed_in_mr } - end - - trait :risk_accepted_in_mr do - dismissal_reason { :risk_accepted_in_mr } - end end diff --git a/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb b/ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb index 42d5627a5a8330..1ce00f3d8d8ca3 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 00000000000000..aae7424fd45758 --- /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 8b818c4a430339..f4450e705ae77d 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 diff --git a/ee/spec/models/security/policy_dismissal_spec.rb b/ee/spec/models/security/policy_dismissal_spec.rb index 29e5e1e8f8930e..deefb0282b4c70 100644 --- a/ee/spec/models/security/policy_dismissal_spec.rb +++ b/ee/spec/models/security/policy_dismissal_spec.rb @@ -10,14 +10,6 @@ it { is_expected.to belong_to(:user).optional } end - describe 'enums' do - let(:dismissal_reasons) do - { dismissed_in_mr: 0, risk_accepted_in_mr: 1, other: 2 } - end - - it { is_expected.to define_enum_for(:dismissal_reason).with_values(dismissal_reasons) } - end - describe 'validations' do subject(:policy_dismissal) { create(:policy_dismissal) } @@ -45,30 +37,4 @@ end end end - - describe 'scopes' do - describe '.by_dismissal_reason' do - subject(:policy_dismissals_by_dismissal_reasons) { described_class.by_dismissal_reason(dismissal_reasons) } - - let_it_be(:policy_dismissal_dismissed_in_mr) { create(:policy_dismissal, :dismissed_in_mr) } - let_it_be(:policy_dismissal_risk_accepted_in_mr) { create(:policy_dismissal, :risk_accepted_in_mr) } - - context 'when filtering by one dismissal reason' do - let(:dismissal_reasons) { [:dismissed_in_mr] } - - it 'returns the policy dismissal with matching dismissal_reason' do - expect(policy_dismissals_by_dismissal_reasons).to match_array([policy_dismissal_dismissed_in_mr]) - end - end - - context 'when filtering by multiple dismissal reasons' do - let(:dismissal_reasons) { [:dismissed_in_mr, :risk_accepted_in_mr] } - - it 'returns the policy dismissal with matching dismissal reasons' do - expect(policy_dismissals_by_dismissal_reasons).to match_array([policy_dismissal_dismissed_in_mr, - policy_dismissal_risk_accepted_in_mr]) - end - end - end - end end -- GitLab