diff --git a/ee/elastic/docs/20251023152050_add_detected_at_field_to_vulnerability.yml b/ee/elastic/docs/20251023152050_add_detected_at_field_to_vulnerability.yml new file mode 100644 index 0000000000000000000000000000000000000000..03515d536200f149f7bb3c1b9d33f44222e7387f --- /dev/null +++ b/ee/elastic/docs/20251023152050_add_detected_at_field_to_vulnerability.yml @@ -0,0 +1,10 @@ +--- +name: AddDetectedAtFieldToVulnerability +version: '20251023152050' +description: Adding the detected_at field to Vulnerability +group: group::security insights +milestone: '18.6' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209927 +obsolete: false +marked_obsolete_by_url: +marked_obsolete_in_milestone: diff --git a/ee/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability.rb b/ee/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability.rb new file mode 100644 index 0000000000000000000000000000000000000000..86b1d2e1df193614ba16114f9b51b4d9e5e91271 --- /dev/null +++ b/ee/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddDetectedAtFieldToVulnerability < Elastic::Migration + include ::Search::Elastic::MigrationUpdateMappingsHelper + + DOCUMENT_TYPE = Vulnerability + + private + + def new_mappings + { + detected_at: { + type: 'date' + } + } + end +end diff --git a/ee/lib/search/elastic/references/vulnerability.rb b/ee/lib/search/elastic/references/vulnerability.rb index 3d426a210f0135384e78f9aa0729ed237634dfcf..a7c22a4e82a87e6d4179981fbd890c3a2bdf48ac 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_43 + SCHEMA_VERSION = 25_44 DOC_TYPE = 'vulnerability' INDEX_NAME = 'vulnerabilities' @@ -83,6 +83,8 @@ def as_indexed_json fields.merge!(build_other_fields) + fields["detected_at"] = database_record.vulnerability.detected_at if detected_at_migration_completed? + fields["risk_score"] = fetch_record_attribute(database_record, :risk_score) if risk_score_migration_completed? fields["reachability"] = fetch_record_attribute(database_record, :reachability) @@ -143,8 +145,10 @@ def internal_es_fields end def fetch_schema_version - if risk_score_migration_completed? + if detected_at_migration_completed? SCHEMA_VERSION + elsif risk_score_migration_completed? + 25_43 elsif policy_violations_migration_finished? 25_42 elsif token_status_migration_finished? @@ -175,6 +179,10 @@ def risk_score_migration_completed? ::Elastic::DataMigrationService.migration_has_finished?(:add_risk_score_field_to_vulnerability) end + def detected_at_migration_completed? + ::Elastic::DataMigrationService.migration_has_finished?(:add_detected_at_field_to_vulnerability) + end + # # Private class methods for implementation details # diff --git a/ee/lib/search/elastic/types/vulnerability.rb b/ee/lib/search/elastic/types/vulnerability.rb index deb2cd3a23fa48b3c67f5e049760474b2561eee7..c833ddd8c939005dd71332f94296b486947331be 100644 --- a/ee/lib/search/elastic/types/vulnerability.rb +++ b/ee/lib/search/elastic/types/vulnerability.rb @@ -30,6 +30,7 @@ def base_mappings vulnerability_id: { type: 'long' }, created_at: { type: 'date' }, updated_at: { type: 'date' }, + detected_at: { type: 'date' }, project_id: { type: 'long' }, scanner_id: { type: 'long' }, scanner_external_id: { type: 'keyword' }, diff --git a/ee/spec/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability_spec.rb b/ee/spec/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..82836080e7b33f04950e64ef17bec5b2c1bb0be6 --- /dev/null +++ b/ee/spec/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require File.expand_path('ee/elastic/migrate/20251023152050_add_detected_at_field_to_vulnerability.rb') + +RSpec.describe AddDetectedAtFieldToVulnerability, :elastic, feature_category: :vulnerability_management do + let(:version) { 20251023152050 } + + include_examples 'migration adds mapping' +end diff --git a/ee/spec/lib/search/elastic/references/vulnerability_spec.rb b/ee/spec/lib/search/elastic/references/vulnerability_spec.rb index a3102a04d1ba5ff585d161dfc39080644ba1b0ff..9aa1108f1f54f78ecf27441abc36f5b724620b10 100644 --- a/ee/spec/lib/search/elastic/references/vulnerability_spec.rb +++ b/ee/spec/lib/search/elastic/references/vulnerability_spec.rb @@ -52,6 +52,7 @@ vulnerability_id: object.vulnerability_id, created_at: be_within(0.1.seconds).of(object.vulnerability.created_at), updated_at: be_within(0.1.seconds).of(object.vulnerability.updated_at), + detected_at: be_within(0.1.seconds).of(object.vulnerability.detected_at), resolved_at: nil, dismissed_at: nil, project_id: object.project_id, @@ -302,6 +303,46 @@ end end + context 'with detected_at mappings' do + before do + set_elasticsearch_migration_to(:add_detected_at_field_to_vulnerability) + end + + context 'when migrations are completed' do + context 'with detected_at' do + let(:detected_at) { 2.days.ago } + let(:detected_vulnerability) do + create(:vulnerability, :with_read, project: project, detected_at: detected_at) + end + + let(:detected_vulnerability_read) { detected_vulnerability.vulnerability_read } + let(:object) { detected_vulnerability_read } + + it 'sets detected_at field on the indexed json' do + expect(indexed_json[:detected_at]).to be_within(0.1.seconds).of(detected_at) + end + + it 'returns schema version' do + expect(indexed_json[:schema_version]).to eq(25_44) + end + end + + context 'when the detected_at migration has not completed' do + before do + set_elasticsearch_migration_to(:add_detected_at_field_to_vulnerability, including: false) + end + + it 'returns schema version with risk_score only' do + expect(indexed_json[:schema_version]).to eq(25_43) + end + + it 'does not assign detected_at on the indexed json', :aggregate_failures do + expect(indexed_json[:detected_at]).to be_nil + end + end + end + end + context 'when all migrations have completed' do it 'returns the current schema version' do expect(indexed_json[:schema_version]).to eq(described_class::SCHEMA_VERSION)