diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index b6ae765d6a9c41793961b25a2d1f3ed88d9b99b7..7f2dfcbdcf082c0e97233f5ff11a781f2302dc1b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -979,6 +979,10 @@
   - 1
 - - vulnerabilities_archival_archive
   - 1
+- - vulnerabilities_archival_export_export
+  - 1
+- - vulnerabilities_archival_export_purge
+  - 1
 - - vulnerabilities_mark_dropped_as_resolved
   - 1
 - - vulnerabilities_namespace_historical_statistics_process_transfer_events
diff --git a/db/migrate/20250304134327_index_vulnerability_archived_records_on_archive_id_and_id.rb b/db/migrate/20250304134327_index_vulnerability_archived_records_on_archive_id_and_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1ea2d6554b97b745d8f91cbcf68d19f0fc567ff6
--- /dev/null
+++ b/db/migrate/20250304134327_index_vulnerability_archived_records_on_archive_id_and_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class IndexVulnerabilityArchivedRecordsOnArchiveIdAndId < Gitlab::Database::Migration[2.2]
+  INDEX_NAME = 'index_vulnerability_archived_records_on_archive_id_and_id'
+
+  disable_ddl_transaction!
+  milestone '17.10'
+
+  def up
+    add_concurrent_index :vulnerability_archived_records, %i[archive_id id], name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :vulnerability_archived_records, INDEX_NAME
+  end
+end
diff --git a/db/migrate/20250304134552_drop_index_from_vulnerability_archived_records_on_archive_id.rb b/db/migrate/20250304134552_drop_index_from_vulnerability_archived_records_on_archive_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6ffc11095459adf1b154b9b43fe8403b92feb1c6
--- /dev/null
+++ b/db/migrate/20250304134552_drop_index_from_vulnerability_archived_records_on_archive_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class DropIndexFromVulnerabilityArchivedRecordsOnArchiveId < Gitlab::Database::Migration[2.2]
+  INDEX_NAME = 'index_vulnerability_archived_records_on_archive_id'
+
+  disable_ddl_transaction!
+  milestone '17.10'
+
+  def up
+    remove_concurrent_index_by_name :vulnerability_archived_records, INDEX_NAME
+  end
+
+  def down
+    add_concurrent_index :vulnerability_archived_records, :archive_id, name: INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20250304134327 b/db/schema_migrations/20250304134327
new file mode 100644
index 0000000000000000000000000000000000000000..3e042c682706654e30ab9320f6068a814768ae4c
--- /dev/null
+++ b/db/schema_migrations/20250304134327
@@ -0,0 +1 @@
+22c4e9ded5b0474d1854de01a1b363394df170e63fc05b0a1b8d968aaa891505
\ No newline at end of file
diff --git a/db/schema_migrations/20250304134552 b/db/schema_migrations/20250304134552
new file mode 100644
index 0000000000000000000000000000000000000000..923bcd16534ad14a723765b1d5236d053d1b15fa
--- /dev/null
+++ b/db/schema_migrations/20250304134552
@@ -0,0 +1 @@
+6d2337f6c66588dfe3d7e3f6fa2b0cf73160a2a0256b28cefb1f58f54c88adfb
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 54d7f6789e87723b2ed512daef59b6b946fe6ba8..b7b1f4d5852eb5e2f806268adaffed4a298ce67d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -36196,7 +36196,7 @@ CREATE INDEX index_vulnerability_archive_exports_on_project_id ON ONLY vulnerabi
 
 CREATE INDEX index_vulnerability_archive_exports_on_status ON ONLY vulnerability_archive_exports USING btree (status);
 
-CREATE INDEX index_vulnerability_archived_records_on_archive_id ON vulnerability_archived_records USING btree (archive_id);
+CREATE INDEX index_vulnerability_archived_records_on_archive_id_and_id ON vulnerability_archived_records USING btree (archive_id, id);
 
 CREATE INDEX index_vulnerability_archived_records_on_project_id ON vulnerability_archived_records USING btree (project_id);
 
diff --git a/ee/app/models/vulnerabilities/archive_export.rb b/ee/app/models/vulnerabilities/archive_export.rb
index 748fc56e59f2e6cfec96c7ad56fa269ddb7ef993..144d6693402d6d958454a71848097976008c2f6d 100644
--- a/ee/app/models/vulnerabilities/archive_export.rb
+++ b/ee/app/models/vulnerabilities/archive_export.rb
@@ -52,6 +52,10 @@ class ArchiveExport < Gitlab::Database::SecApplicationRecord
         transition running: :created
       end
 
+      event :purge do
+        transition any => :purged
+      end
+
       before_transition created: :running do |export|
         export.started_at = Time.current
       end
@@ -85,6 +89,14 @@ def first_record_in(partition_number)
       end
     end
 
+    def completed?
+      finished? || failed?
+    end
+
+    def archives
+      project.vulnerability_archives.where(date: date_range)
+    end
+
     def uploads_sharding_key
       { project_id: project_id }
     end
diff --git a/ee/app/models/vulnerabilities/archived_record.rb b/ee/app/models/vulnerabilities/archived_record.rb
index d277bfbe08ab2146e30a5de578eed6b7d3dd9878..6039e3a26c09f2f787c4fcaa414fde2b9b15d8ed 100644
--- a/ee/app/models/vulnerabilities/archived_record.rb
+++ b/ee/app/models/vulnerabilities/archived_record.rb
@@ -3,6 +3,7 @@
 module Vulnerabilities
   class ArchivedRecord < Gitlab::Database::SecApplicationRecord
     include BulkInsertSafe
+    include EachBatch
 
     self.table_name = 'vulnerability_archived_records'
 
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index b3f91977d6f1d8853e9c0b6d0accba33bd57d84f..ac2db92ce1cf1710e624a16ab2fab43b2f2db437 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -564,6 +564,7 @@ module ProjectPolicy
       rule { can?(:read_security_resource) }.policy do
         enable :read_project_security_dashboard
         enable :create_vulnerability_export
+        enable :create_vulnerability_archive_export
         enable :admin_vulnerability_issue_link
         enable :admin_vulnerability_merge_request_link
         enable :admin_vulnerability_external_issue_link
@@ -926,6 +927,7 @@ module ProjectPolicy
         enable :read_vulnerability
         enable :read_security_resource
         enable :create_vulnerability_export
+        enable :create_vulnerability_archive_export
       end
 
       rule { custom_role_enables_admin_merge_request }.policy do
diff --git a/ee/app/policies/vulnerabilities/archive_export_policy.rb b/ee/app/policies/vulnerabilities/archive_export_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e80fbf1a9f93bd93d9888e7f3008b6b4e8020409
--- /dev/null
+++ b/ee/app/policies/vulnerabilities/archive_export_policy.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  class ArchiveExportPolicy < BasePolicy
+    delegate { @subject.project }
+
+    condition(:is_author) { @user && @subject.author == @user }
+    condition(:exportable) { can?(:create_vulnerability_archive_export, @subject.project) }
+
+    rule { exportable & is_author }.policy do
+      enable :read_vulnerability_archive_export
+    end
+  end
+end
diff --git a/ee/app/services/vulnerabilities/archival/archived_record_builder_service.rb b/ee/app/services/vulnerabilities/archival/archived_record_builder_service.rb
index 7784165a611237c01e5e7c2f932ea3b85ea6bf65..fafdb0b1016e64f52767d42c26c2fe8fc380d12a 100644
--- a/ee/app/services/vulnerabilities/archival/archived_record_builder_service.rb
+++ b/ee/app/services/vulnerabilities/archival/archived_record_builder_service.rb
@@ -41,6 +41,7 @@ def archive_data
           description: finding.description,
           cve_value: vulnerability.cve_value,
           cwe_value: vulnerability.cwe_value,
+          other_identifiers: vulnerability.other_identifier_values,
           created_at: vulnerability.created_at.to_s,
           location: vulnerability.location,
           resolved_on_default_branch: vulnerability.resolved_on_default_branch,
diff --git a/ee/app/services/vulnerabilities/archival/export/create_service.rb b/ee/app/services/vulnerabilities/archival/export/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d916c66115d84282fc5a595a0aafc712b03aec62
--- /dev/null
+++ b/ee/app/services/vulnerabilities/archival/export/create_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      class CreateService
+        include Gitlab::Allowable
+
+        def initialize(project, author, start_date, end_date, format:)
+          @project = project
+          @author = author
+          @start_date = start_date
+          @end_date = end_date
+          @format = format
+        end
+
+        def execute
+          authorize_author!
+
+          schedule_export if archive_export.persisted?
+
+          archive_export
+        end
+
+        private
+
+        attr_reader :project, :author, :start_date, :end_date, :format
+
+        def authorize_author!
+          return if can?(author, :create_vulnerability_archive_export, project)
+
+          raise Gitlab::Access::AccessDeniedError
+        end
+
+        def archive_export
+          @archive_export ||= Vulnerabilities::ArchiveExport.create(
+            project: project,
+            author: author,
+            date_range: date_range,
+            format: format)
+        end
+
+        def schedule_export
+          Vulnerabilities::Archival::Export::ExportWorker.perform_async(archive_export.id)
+        end
+
+        def date_range
+          start_date..end_date
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/vulnerabilities/archival/export/export_service.rb b/ee/app/services/vulnerabilities/archival/export/export_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6a2981d7f32fc12be5bffb6f9a852c69def321cd
--- /dev/null
+++ b/ee/app/services/vulnerabilities/archival/export/export_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      class ExportService
+        EXPORTERS = {
+          'csv' => Vulnerabilities::Archival::Export::Exporters::CsvService
+        }.freeze
+
+        def self.export(archive_export)
+          new(archive_export).export
+        end
+
+        def initialize(archive_export)
+          @archive_export = archive_export
+        end
+
+        def export
+          archive_export.start!
+
+          generate_export_file
+
+          archive_export.store_file_now!
+          archive_export.finish!
+
+          schedule_purging_export
+        rescue StandardError
+          archive_export.reset_state!
+
+          raise
+        end
+
+        private
+
+        attr_reader :archive_export
+
+        delegate :project, :format, :date_range, to: :archive_export, private: true
+
+        def generate_export_file
+          exporter.generate { |f| archive_export.file = f }
+          archive_export.file.filename = filename
+        end
+
+        def exporter
+          @exporter ||= EXPORTERS[archive_export.format].new(iterator)
+        end
+
+        def iterator
+          Iterator.new(archive_export.archives)
+        end
+
+        def schedule_purging_export
+          Vulnerabilities::Archival::Export::PurgeWorker.perform_in(24.hours, archive_export.id)
+        end
+
+        def filename
+          [
+            project.full_path.parameterize,
+            '_vulnerabilities_archive_',
+            date_range,
+            Time.current.utc.strftime('_%FT%H%M'),
+            '.',
+            format
+          ].join
+        end
+
+        class Iterator
+          include Enumerable
+
+          def initialize(archives)
+            @archives = archives
+          end
+
+          def each(&block)
+            return to_enum(:each) unless block
+
+            archives.each { |archive| yield_archived_records_of(archive, &block) }
+          end
+
+          def yield_archived_records_of(archive)
+            archive.archived_records.each_batch do |batch|
+              batch.each { |archived_record| yield archived_record.data }
+            end
+          end
+
+          private
+
+          attr_reader :archives
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/vulnerabilities/archival/export/exporters/csv_service.rb b/ee/app/services/vulnerabilities/archival/export/exporters/csv_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f5aa1d8662c48e63f5e59b7f85197d52191303d
--- /dev/null
+++ b/ee/app/services/vulnerabilities/archival/export/exporters/csv_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      module Exporters
+        class CsvService
+          CSV_DELIMITER = '; '
+
+          def initialize(iterator)
+            @iterator = iterator
+          end
+
+          def generate(&block)
+            csv_builder.render(&block)
+          end
+
+          private
+
+          attr_reader :iterator
+
+          def csv_builder
+            @csv_builder ||= CsvBuilder.new(iterator, mapping, replace_newlines: true)
+          end
+
+          def mapping
+            {
+              'Tool' => 'report_type',
+              'Scanner Name' => 'scanner',
+              'Status' => 'state',
+              'Vulnerability' => 'title',
+              'Details' => 'description',
+              'Severity' => 'severity',
+              'CVE' => 'cve_value',
+              'CWE' => 'cwe_value',
+              'Other Identifiers' => method(:identifier_formatter),
+              'Detected At' => 'created_at',
+              'Location' => 'location',
+              'Activity' => 'resolved_on_default_branch',
+              'Comments' => 'notes_summary',
+              'Full Path' => 'full_path',
+              'CVSS Vectors' => method(:cvss_formatter),
+              'Dismissal Reason' => method(:dismissal_formatter)
+            }
+          end
+
+          def identifier_formatter(data)
+            data['other_identifiers'].to_csv(col_sep: CSV_DELIMITER, row_sep: '')
+          end
+
+          def dismissal_formatter(data)
+            data['dismissal_reason']&.humanize
+          end
+
+          def cvss_formatter(data)
+            data['cvss'].map { |cvss| "#{cvss['vendor']}=#{cvss['vector']}" }
+                        .to_csv(col_sep: CSV_DELIMITER, row_sep: '')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/vulnerabilities/archival/export/purge_service.rb b/ee/app/services/vulnerabilities/archival/export/purge_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6c95f3407a1fad4eed3c44494ed4edf5a37ea71b
--- /dev/null
+++ b/ee/app/services/vulnerabilities/archival/export/purge_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      class PurgeService
+        def self.purge(archive_export)
+          new(archive_export).purge
+        end
+
+        def initialize(archive_export)
+          @archive_export = archive_export
+        end
+
+        def purge
+          archive_export.remove_file!
+          archive_export.purge!
+        end
+
+        private
+
+        attr_reader :archive_export
+      end
+    end
+  end
+end
diff --git a/ee/app/validators/json_schemas/archived_record_data.json b/ee/app/validators/json_schemas/archived_record_data.json
index df5c6615d80efde3fb1cabfa5c4e9e79be1fa17a..f3788fa0c21d5c216b48e4930b1bf9691efcf38d 100644
--- a/ee/app/validators/json_schemas/archived_record_data.json
+++ b/ee/app/validators/json_schemas/archived_record_data.json
@@ -18,7 +18,7 @@
       "type": "string"
     },
     "description": {
-      "type": "string"
+      "type": ["null", "string"]
     },
     "severity": {
       "type": "string"
@@ -29,6 +29,12 @@
     "cwe_value": {
       "type": ["null", "string"]
     },
+    "other_identifiers": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
     "created_at": {
       "type": "date-time"
     },
diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml
index de3c78ccfb05bdf15d093196db20ecb8e069579f..36010b8dafc11ff3b17a16f4e750d39140ee6155 100644
--- a/ee/app/workers/all_queues.yml
+++ b/ee/app/workers/all_queues.yml
@@ -3483,6 +3483,26 @@
   :idempotent: true
   :tags: []
   :queue_namespace:
+- :name: vulnerabilities_archival_export_export
+  :worker_name: Vulnerabilities::Archival::Export::ExportWorker
+  :feature_category: :vulnerability_management
+  :has_external_dependencies: false
+  :urgency: :low
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
+  :queue_namespace:
+- :name: vulnerabilities_archival_export_purge
+  :worker_name: Vulnerabilities::Archival::Export::PurgeWorker
+  :feature_category: :vulnerability_management
+  :has_external_dependencies: false
+  :urgency: :low
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
+  :queue_namespace:
 - :name: vulnerabilities_mark_dropped_as_resolved
   :worker_name: Vulnerabilities::MarkDroppedAsResolvedWorker
   :feature_category: :static_application_security_testing
diff --git a/ee/app/workers/vulnerabilities/archival/export/export_worker.rb b/ee/app/workers/vulnerabilities/archival/export/export_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eefe7663b79851d7e576cec668b8e1b5f0812c1b
--- /dev/null
+++ b/ee/app/workers/vulnerabilities/archival/export/export_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      class ExportWorker
+        include ApplicationWorker
+
+        data_consistency :sticky
+        idempotent!
+        deduplicate :until_executing, including_scheduled: true
+
+        feature_category :vulnerability_management
+
+        sidekiq_retries_exhausted do |job|
+          archive_export_id = job['args'].first
+
+          Vulnerabilities::Archival::Export::PurgeWorker.perform_in(24.hours, archive_export_id)
+        end
+
+        def perform(archive_export_id)
+          archive_export = Vulnerabilities::ArchiveExport.find_by_id(archive_export_id)
+
+          return unless archive_export
+
+          Vulnerabilities::Archival::Export::ExportService.export(archive_export)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/workers/vulnerabilities/archival/export/purge_worker.rb b/ee/app/workers/vulnerabilities/archival/export/purge_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09c4b609b2c5e12e6b66b42fa0d7e634af81d452
--- /dev/null
+++ b/ee/app/workers/vulnerabilities/archival/export/purge_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  module Archival
+    module Export
+      class PurgeWorker
+        include ApplicationWorker
+
+        data_consistency :sticky
+        idempotent!
+        deduplicate :until_executing, including_scheduled: true
+
+        feature_category :vulnerability_management
+
+        def perform(archive_export_id)
+          archive_export = Vulnerabilities::ArchiveExport.find_by_id(archive_export_id)
+
+          return unless archive_export
+
+          Vulnerabilities::Archival::Export::PurgeService.purge(archive_export)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/api/entities/vulnerability_archive_export.rb b/ee/lib/api/entities/vulnerability_archive_export.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec1d4fd921731b40e248b7c0f6d8d10883dd2f20
--- /dev/null
+++ b/ee/lib/api/entities/vulnerability_archive_export.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+  module Entities
+    class VulnerabilityArchiveExport < Grape::Entity
+      include ::API::Helpers::RelatedResourcesHelpers
+
+      expose :id
+      expose :created_at
+      expose :project_id
+      expose :format
+      expose :status
+      expose :started_at
+      expose :finished_at
+
+      expose :_links do
+        expose :self do |export|
+          expose_url api_v4_security_vulnerability_archive_exports_path(id: export.id)
+        end
+
+        expose :download do |export|
+          expose_url api_v4_security_vulnerability_archive_exports_download_path(id: export.id)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/api/vulnerability_archive_exports.rb b/ee/lib/api/vulnerability_archive_exports.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fbedfcf1328c72fbd13969cf506ff6b962912af9
--- /dev/null
+++ b/ee/lib/api/vulnerability_archive_exports.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module API
+  class VulnerabilityArchiveExports < ::API::Base
+    feature_category :vulnerability_management
+    urgency :low
+
+    helpers do
+      def vulnerability_archive_export
+        @vulnerability_archive_export ||= ::Vulnerabilities::ArchiveExport.find(params[:id])
+      end
+    end
+
+    before do
+      authenticate!
+    end
+
+    namespace :security do
+      resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+        params do
+          requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
+          requires :start_date, type: Date
+          requires :end_date, type: Date
+          optional :export_format,
+            type: String, desc: 'The format of export to be generated',
+            default: ::Vulnerabilities::ArchiveExport.formats.each_key.first,
+            values: ::Vulnerabilities::ArchiveExport.formats.keys
+        end
+        desc 'Generate an export file for the archived vulnerabilities within the given date range' do
+          success ::API::Entities::VulnerabilityArchiveExport
+        end
+
+        post ':id/vulnerability_archive_exports' do
+          authorize! :create_vulnerability_archive_export, user_project
+
+          service_object = ::Vulnerabilities::Archival::Export::CreateService.new(
+            user_project,
+            current_user,
+            declared_params[:start_date],
+            declared_params[:end_date],
+            format: declared_params[:export_format])
+
+          new_vulnerability_archive_export = service_object.execute
+
+          if new_vulnerability_archive_export.persisted?
+            status :created
+
+            present new_vulnerability_archive_export, with: ::API::Entities::VulnerabilityArchiveExport
+          else
+            render_validation_error!(new_vulnerability_archive_export)
+          end
+        end
+      end
+
+      desc 'Get the vulnerability archive export entity' do
+        success ::API::Entities::VulnerabilityArchiveExport
+      end
+      get 'vulnerability_archive_exports/:id' do
+        authorize! :read_vulnerability_archive_export, vulnerability_archive_export
+
+        unless vulnerability_archive_export.completed?
+          ::Gitlab::PollingInterval.set_api_header(self, interval: 5_000)
+          status :accepted
+        end
+
+        present vulnerability_archive_export, with: ::API::Entities::VulnerabilityArchiveExport
+      end
+
+      desc 'Download a single vulnerability export'
+      get 'vulnerability_archive_exports/:id/download' do
+        authorize! :read_vulnerability_archive_export, vulnerability_archive_export
+
+        if vulnerability_archive_export.finished?
+          present_carrierwave_file!(vulnerability_archive_export.file, content_disposition: :attachment)
+        else
+          not_found!('Archive Export')
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb
index 922f9903251c108c498ec1a59ac36683f960699a..1b0e6643fc11e3e24db6b590436234f2d4c9a309 100644
--- a/ee/lib/ee/api/api.rb
+++ b/ee/lib/ee/api/api.rb
@@ -54,6 +54,7 @@ module API
         mount ::API::Vulnerabilities
         mount ::API::VulnerabilityFindings
         mount ::API::VulnerabilityIssueLinks
+        mount ::API::VulnerabilityArchiveExports
         mount ::API::VulnerabilityExports
         mount ::API::MergeRequestApprovalRules
         mount ::API::ProjectAliases
diff --git a/ee/spec/factories/vulnerabilities/archive_exports.rb b/ee/spec/factories/vulnerabilities/archive_exports.rb
index 2139057aba580d0ae3332df52454db0575957946..23770508a9d9c01888c6437c1235c884c5cd423e 100644
--- a/ee/spec/factories/vulnerabilities/archive_exports.rb
+++ b/ee/spec/factories/vulnerabilities/archive_exports.rb
@@ -16,5 +16,11 @@
       status { :running }
       started_at { 1.minute.ago }
     end
+
+    trait :finished do
+      with_csv_file
+
+      status { :finished }
+    end
   end
 end
diff --git a/ee/spec/factories/vulnerabilities/archived_records.rb b/ee/spec/factories/vulnerabilities/archived_records.rb
index 6429bff7fa41d6b36712d35a66cf085fd49186aa..7e75c1e19dc31f729a026a64816a95ee9cf81991 100644
--- a/ee/spec/factories/vulnerabilities/archived_records.rb
+++ b/ee/spec/factories/vulnerabilities/archived_records.rb
@@ -15,6 +15,7 @@
         description: 'Test Description',
         cve_value: 'CVE-2018-1234',
         cwe_value: 'CWE-123',
+        other_identifiers: ['OWASP-A01:2021'],
         created_at: '2025-01-29 19:02:08 UTC',
         location: {
           class: 'com.gitlab.security_products.tests.App',
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/vulnerability_archive_export.json b/ee/spec/fixtures/api/schemas/public_api/v4/vulnerability_archive_export.json
new file mode 100644
index 0000000000000000000000000000000000000000..b0206cc86f804d13561a2291348280d7f0f9ba01
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/vulnerability_archive_export.json
@@ -0,0 +1,53 @@
+{
+  "type" : "object",
+  "required": [
+    "id",
+    "created_at",
+    "project_id",
+    "format",
+    "status",
+    "started_at",
+    "finished_at",
+    "_links"
+  ],
+  "properties" : {
+    "id": {
+      "type": "integer"
+    },
+    "created_at": {
+      "type": "string",
+      "format": "date-time"
+    },
+    "project_id": {
+      "type": "integer"
+    },
+    "format": {
+      "type": "string",
+      "enum": ["csv"]
+    },
+    "status": {
+      "type": "string",
+      "enum": ["created", "running", "finished", "failed", "purged"]
+    },
+    "started_at": {
+      "type": ["string", "null"]
+    },
+    "finished_at": {
+      "type": ["string", "null"]
+    },
+    "_links": {
+      "type": "object",
+      "required": ["self", "download"],
+      "properties": {
+        "self": {
+          "type": "string"
+        },
+        "download": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additional_properties" : false
+}
diff --git a/ee/spec/models/vulnerabilities/archive_export_spec.rb b/ee/spec/models/vulnerabilities/archive_export_spec.rb
index 69f3c14693e1cd86a62de8b858d1c843a1c8ec1a..a9f88c050f66fa07d93939f5c3558aac64b12bbb 100644
--- a/ee/spec/models/vulnerabilities/archive_export_spec.rb
+++ b/ee/spec/models/vulnerabilities/archive_export_spec.rb
@@ -73,6 +73,16 @@
         expect { reset_state }.to change { export.reload.started_at }.to(nil)
       end
     end
+
+    describe '#purge' do
+      let(:export) { create(:vulnerability_archive_export, :running) }
+
+      subject(:purge) { export.purge! }
+
+      it 'sets the status of the record as purged' do
+        expect { purge }.to change { export.reload.status }.to('purged')
+      end
+    end
   end
 
   describe 'partition helpers' do
@@ -125,4 +135,38 @@
       end
     end
   end
+
+  describe '#completed?' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:status, :completed?) do
+      :created | false
+      :running | false
+      :finished | true
+      :failed | true
+      :purged | false
+    end
+
+    with_them do
+      let(:archive_export) { build(:vulnerability_archive_export, status: status) }
+
+      subject { archive_export.completed? }
+
+      it { is_expected.to eq(completed?) }
+    end
+  end
+
+  describe '#archives', :freeze_time do
+    let_it_be(:project) { create(:project) }
+    let_it_be(:project_archive_1) { create(:vulnerability_archive, project: project, date: 3.months.ago) }
+    let_it_be(:project_archive_2) { create(:vulnerability_archive, project: project, date: 1.month.ago) }
+    let_it_be(:another_project_archive) { create(:vulnerability_archive, date: 1.month.ago) }
+
+    let_it_be(:date_range) { project_archive_2.date..Time.zone.today }
+    let_it_be(:archive_export) { create(:vulnerability_archive_export, project: project, date_range: date_range) }
+
+    subject { archive_export.archives }
+
+    it { is_expected.to contain_exactly(project_archive_2) }
+  end
 end
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index 80a45e95bef77b9f134949dcb45903d24da957de..6ca952799d05428a1eba48f07cb724eb8f756f6d 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -806,15 +806,15 @@
     end
 
     describe 'vulnerability permissions' do
-      describe 'dismiss_vulnerability' do
-        context 'with developer' do
-          let(:current_user) { developer }
+      context 'with developer' do
+        let(:current_user) { developer }
 
-          include_context 'when security dashboard feature is not available'
-          it { is_expected.to be_disallowed(:admin_vulnerability) }
-          it { is_expected.to be_disallowed(:read_vulnerability) }
-          it { is_expected.to be_disallowed(:create_vulnerability_export) }
-        end
+        include_context 'when security dashboard feature is not available'
+
+        it { is_expected.to be_disallowed(:admin_vulnerability) }
+        it { is_expected.to be_disallowed(:read_vulnerability) }
+        it { is_expected.to be_disallowed(:create_vulnerability_export) }
+        it { is_expected.to be_disallowed(:create_vulnerability_archive_export) }
       end
     end
 
@@ -2912,6 +2912,7 @@ def create_member_role(member, abilities = member_role_abilities)
           [
             :access_security_and_compliance,
             :create_vulnerability_export,
+            :create_vulnerability_archive_export,
             :read_security_resource,
             :read_vulnerability,
             :read_vulnerability_feedback,
diff --git a/ee/spec/policies/vulnerabilities/archive_export_policy_spec.rb b/ee/spec/policies/vulnerabilities/archive_export_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..035da4d0a811be5bfca0b3b2f2b45df6f03e7772
--- /dev/null
+++ b/ee/spec/policies/vulnerabilities/archive_export_policy_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::ArchiveExportPolicy, feature_category: :vulnerability_management do
+  let_it_be_with_refind(:project) { create(:project) }
+  let_it_be(:user) { create(:user) }
+
+  let(:archive_export) { create(:vulnerability_archive_export, project: project, author: author) }
+
+  subject { described_class.new(user, archive_export) }
+
+  context 'when security dashboard is licensed' do
+    before do
+      stub_licensed_features(security_dashboard: true)
+    end
+
+    context 'when the user is the author of the archive export' do
+      let(:author) { user }
+
+      context 'when user has access to vulnerabilities from the project' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { is_expected.to be_allowed(:read_vulnerability_archive_export) }
+      end
+
+      context 'when user has no access to vulnerabilities from the project' do
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+    end
+
+    context 'when the user is not the author of the archive export' do
+      let(:author) { create(:user) }
+
+      context 'when user has access to vulnerabilities from the project' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+
+      context 'when user has no access to vulnerabilities from the project' do
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+    end
+  end
+
+  context 'when security dashboard is not licensed' do
+    before do
+      stub_licensed_features(security_dashboard: false)
+    end
+
+    context 'when the user is the author of the archive export' do
+      let(:author) { user }
+
+      context 'when user has access to vulnerabilities from the project' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+
+      context 'when user has no access to vulnerabilities from the project' do
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+    end
+
+    context 'when the user is not the author of the archive export' do
+      let(:author) { create(:user) }
+
+      context 'when user has access to vulnerabilities from the project' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+
+      context 'when user has no access to vulnerabilities from the project' do
+        it { is_expected.to be_disallowed(:read_vulnerability_archive_export) }
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/vulnerability_archive_exports_spec.rb b/ee/spec/requests/api/vulnerability_archive_exports_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba4bdf7c617c2d9c4f4e3b9eb7653841773a0818
--- /dev/null
+++ b/ee/spec/requests/api/vulnerability_archive_exports_spec.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::VulnerabilityArchiveExports, feature_category: :vulnerability_management do
+  include AccessMatchersForRequest
+
+  before do
+    stub_licensed_features(security_dashboard: true)
+  end
+
+  let_it_be_with_refind(:project) { create(:project) }
+  let_it_be(:user) { create(:user) }
+
+  describe 'POST /security/projects/:id/vulnerability_archive_exports' do
+    let(:request_path) { "/security/projects/#{project.id}/vulnerability_archive_exports" }
+    let(:export_format) { :csv }
+    let(:request_params) do
+      {
+        export_format: export_format,
+        start_date: '01/01/2025',
+        end_date: '01/01/2025'
+      }
+    end
+
+    subject(:create_archive_export) { post api(request_path, user), params: request_params }
+
+    context 'when the request does not fulfill the requirements' do
+      let(:export_format) { 'exif' }
+
+      it 'responds with bad_request', :aggregate_failures do
+        create_archive_export
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response).to eq('error' => 'export_format does not have a valid value')
+      end
+    end
+
+    context 'when the request fulfills the requirements' do
+      let(:mock_service_object) do
+        instance_double(::Vulnerabilities::Archival::Export::CreateService, execute: archive_export)
+      end
+
+      before_all do
+        project.add_developer(user)
+      end
+
+      before do
+        allow(::Vulnerabilities::Archival::Export::CreateService).to receive(:new).and_return(mock_service_object)
+      end
+
+      context 'when the export creation succeeds' do
+        let(:archive_export) { create(:vulnerability_archive_export) }
+
+        it 'returns information about new archive export', :aggregate_failures do
+          create_archive_export
+
+          expect(response).to have_gitlab_http_status(:created)
+          expect(response).to match_response_schema('public_api/v4/vulnerability_archive_export', dir: 'ee')
+        end
+      end
+
+      context 'when the export creation fails' do
+        let(:errors) { instance_double(ActiveModel::Errors, any?: true, messages: ['foo']) }
+        let(:archive_export) { instance_double(Vulnerabilities::ArchiveExport, persisted?: false, errors: errors) }
+
+        it 'returns the error message', :aggregate_failures do
+          create_archive_export
+
+          expect(response).to have_gitlab_http_status(:bad_request)
+          expect(json_response).to eq('message' => ['foo'])
+        end
+      end
+    end
+
+    describe 'permissions', :enable_admin_mode do
+      it { expect { create_archive_export }.to be_allowed_for(:admin) }
+      it { expect { create_archive_export }.to be_allowed_for(:owner).of(project) }
+      it { expect { create_archive_export }.to be_allowed_for(:maintainer).of(project) }
+      it { expect { create_archive_export }.to be_allowed_for(:developer).of(project) }
+      it { expect { create_archive_export }.to be_allowed_for(:auditor) }
+
+      it { expect { create_archive_export }.to be_denied_for(:reporter).of(project) }
+      it { expect { create_archive_export }.to be_denied_for(:guest).of(project) }
+      it { expect { create_archive_export }.to be_denied_for(:anonymous) }
+    end
+
+    it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features' do
+      before_all do
+        project.add_developer(user)
+      end
+    end
+  end
+
+  describe 'GET /security/vulnerability_archive_exports/:id' do
+    let_it_be(:archive_export) { create(:vulnerability_archive_export, :finished, project: project, author: user) }
+
+    let(:request_path) { "/security/vulnerability_archive_exports/#{archive_export.id}" }
+
+    subject(:get_archive_export) { get api(request_path, user) }
+
+    context 'with an authorized user with proper permissions' do
+      before_all do
+        project.add_developer(user)
+      end
+
+      context 'when export is finished' do
+        it 'returns information about archive export', :aggregate_failures do
+          get_archive_export
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(response).to match_response_schema('public_api/v4/vulnerability_archive_export', dir: 'ee')
+          expect(json_response['id']).to eq archive_export.id
+        end
+
+        it 'does not return Poll-Interval header' do
+          get_archive_export
+
+          expect(response.headers['Poll-Interval']).to be_blank
+        end
+      end
+
+      context 'when export is running' do
+        before do
+          archive_export.update_column(:status, 'running')
+        end
+
+        it 'returns information about archive export', :aggregate_failures do
+          get_archive_export
+
+          expect(response).to have_gitlab_http_status(:accepted)
+          expect(response).to match_response_schema('public_api/v4/vulnerability_archive_export', dir: 'ee')
+          expect(json_response['id']).to eq archive_export.id
+        end
+
+        it 'returns Poll-Interval header with value set to 5 seconds' do
+          get_archive_export
+
+          expect(response.headers['Poll-Interval']).to eq '5000'
+        end
+      end
+    end
+
+    describe 'permissions', :enable_admin_mode do
+      context 'for export author' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { expect { get_archive_export }.to be_allowed_for(user) }
+      end
+
+      context 'for other users' do
+        it { expect { get_archive_export }.to be_denied_for(:admin) }
+        it { expect { get_archive_export }.to be_denied_for(:owner).of(project) }
+        it { expect { get_archive_export }.to be_denied_for(:maintainer).of(project) }
+        it { expect { get_archive_export }.to be_denied_for(:developer).of(project) }
+        it { expect { get_archive_export }.to be_denied_for(:auditor) }
+        it { expect { get_archive_export }.to be_denied_for(:reporter).of(project) }
+        it { expect { get_archive_export }.to be_denied_for(:guest).of(project) }
+        it { expect { get_archive_export }.to be_denied_for(:anonymous) }
+      end
+    end
+
+    it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features' do
+      before_all do
+        project.add_developer(user)
+      end
+    end
+  end
+
+  describe 'GET /security/vulnerability_archive_exports/:id/download' do
+    let!(:archive_export) { create(:vulnerability_archive_export, :finished, project: project, author: user) }
+    let(:request_path) { "/security/vulnerability_archive_exports/#{archive_export.id}/download" }
+
+    subject(:download_export) { get api(request_path, user) }
+
+    context 'with an authorized user with proper permissions' do
+      before_all do
+        project.add_developer(user)
+      end
+
+      context 'when export is running' do
+        before do
+          archive_export.update_column(:status, 'running')
+        end
+
+        it 'renders 404', :aggregate_failures do
+          download_export
+
+          expect(response).to have_gitlab_http_status(:not_found)
+          expect(json_response).to eq('message' => '404 Archive Export Not Found')
+        end
+      end
+
+      context 'when export is finished' do
+        it 'renders 200 with CSV file', :aggregate_failures do
+          download_export
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(response.body).to include 'Scanner Type,Scanner Name'
+        end
+      end
+    end
+
+    describe 'permissions', :enable_admin_mode do
+      context 'for export author' do
+        before_all do
+          project.add_developer(user)
+        end
+
+        it { expect { download_export }.to be_allowed_for(user) }
+      end
+
+      context 'for other users' do
+        it { expect { download_export }.to be_denied_for(:admin) }
+        it { expect { download_export }.to be_denied_for(:owner).of(project) }
+        it { expect { download_export }.to be_denied_for(:maintainer).of(project) }
+        it { expect { download_export }.to be_denied_for(:developer).of(project) }
+        it { expect { download_export }.to be_denied_for(:auditor) }
+        it { expect { download_export }.to be_denied_for(:reporter).of(project) }
+        it { expect { download_export }.to be_denied_for(:guest).of(project) }
+        it { expect { download_export }.to be_denied_for(:anonymous) }
+      end
+    end
+
+    it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features' do
+      before_all do
+        project.add_developer(user)
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/vulnerabilities/archival/archived_record_builder_service_spec.rb b/ee/spec/services/vulnerabilities/archival/archived_record_builder_service_spec.rb
index ac6edd67a281fcbf0f1ffb26773e93ab73afeb0a..a834ba97ff4503985d929ceb0c7000c63eeb12ac 100644
--- a/ee/spec/services/vulnerabilities/archival/archived_record_builder_service_spec.rb
+++ b/ee/spec/services/vulnerabilities/archival/archived_record_builder_service_spec.rb
@@ -51,7 +51,12 @@
           project: project,
           external_type: 'CWE',
           external_id: 'CWE-123',
-          name: 'CWE-123')
+          name: 'CWE-123'),
+        build(:vulnerabilities_identifier,
+          project: project,
+          external_type: 'OWASP',
+          external_id: 'A01:2021',
+          name: 'OWASP-A01:2021')
       ]
 
       vulnerability.vulnerability_read.dismissal_reason = :false_positive
@@ -71,6 +76,7 @@
           description: 'Test Description',
           cve_value: 'CVE-2018-1234',
           cwe_value: 'CWE-123',
+          other_identifiers: ['OWASP-A01:2021'],
           created_at: vulnerability.created_at.to_s,
           location: {
             class: 'com.gitlab.security_products.tests.App',
diff --git a/ee/spec/services/vulnerabilities/archival/export/create_service_spec.rb b/ee/spec/services/vulnerabilities/archival/export/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1c75c4a3ca0875e1dcacc1935f8eb745ee529128
--- /dev/null
+++ b/ee/spec/services/vulnerabilities/archival/export/create_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::CreateService, feature_category: :vulnerability_management do
+  describe '#execute' do
+    let_it_be_with_refind(:project) { create(:project) }
+    let_it_be_with_refind(:author) { create(:user) }
+
+    let(:start_date) { 5.days.ago.to_date }
+    let(:end_date) { 2.days.ago.to_date }
+    let(:service_object) { described_class.new(project, author, start_date, end_date, format: :csv) }
+
+    subject(:create_export) { service_object.execute }
+
+    before do
+      stub_licensed_features(security_dashboard: true)
+    end
+
+    context 'when the user does not have permission to create export' do
+      it 'raises AccessDenied error' do
+        expect { create_export }.to raise_error(Gitlab::Access::AccessDeniedError)
+      end
+    end
+
+    context 'when the user has permission to create export' do
+      before_all do
+        project.add_developer(author)
+      end
+
+      before do
+        allow(Vulnerabilities::Archival::Export::ExportWorker).to receive(:perform_async)
+      end
+
+      it 'creates a new export record in the database' do
+        expect { create_export }.to change { Vulnerabilities::ArchiveExport.count }.by(1)
+      end
+
+      it 'creates the export record with correct attributes and returns it' do
+        expect(create_export).to have_attributes(
+          project_id: project.id,
+          author_id: author.id,
+          date_range: (start_date..end_date),
+          format: 'csv'
+        )
+      end
+
+      it 'schedules the export generation' do
+        create_export
+
+        expect(Vulnerabilities::Archival::Export::ExportWorker).to have_received(:perform_async)
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/vulnerabilities/archival/export/export_service_spec.rb b/ee/spec/services/vulnerabilities/archival/export/export_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f86f02e71ff2ae5b2373340383b5d1c1ef123903
--- /dev/null
+++ b/ee/spec/services/vulnerabilities/archival/export/export_service_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::ExportService, feature_category: :vulnerability_management do
+  describe '.export' do
+    let(:mock_archive_export) { instance_double(Vulnerabilities::ArchiveExport) }
+    let(:mock_service_object) { instance_spy(described_class) }
+
+    subject(:export) { described_class.export(mock_archive_export) }
+
+    before do
+      allow(described_class).to receive(:new).with(mock_archive_export).and_return(mock_service_object)
+    end
+
+    it 'allocates a service object and delegates the call to it' do
+      export
+
+      expect(mock_service_object).to have_received(:export)
+    end
+  end
+
+  describe '#export' do
+    let(:archive_export) { create(:vulnerability_archive_export) }
+    let(:archive_1) { create(:vulnerability_archive) }
+    let(:archive_2) { create(:vulnerability_archive) }
+    let(:service_object) { described_class.new(archive_export) }
+
+    subject(:export) { service_object.export }
+
+    around do |example|
+      travel_to('23/04/2025') { example.run }
+    end
+
+    before do
+      allow(archive_export).to receive(:archives).and_return([archive_1, archive_2])
+      allow(archive_export.project).to receive(:full_path).and_return('full_path')
+      allow(Vulnerabilities::Archival::Export::PurgeWorker).to receive(:perform_in)
+
+      create(:vulnerability_archived_record, archive: archive_1)
+      create(:vulnerability_archived_record, archive: archive_2)
+    end
+
+    it 'creates the export file' do
+      expect { export }.to change { archive_export.reload.status }.to('finished')
+                       .and change { archive_export.file.file }.from(nil)
+    end
+
+    it 'sets the correct filename for the export' do
+      expected_file_name = 'full_path_vulnerabilities_archive_2025-04-18..2025-04-23_2025-04-23T0000.csv'
+
+      expect { export }.to change { archive_export.file.filename }.from(nil).to(expected_file_name)
+    end
+
+    it 'creates the export for all archived records' do
+      export
+
+      csv = CSV.parse(archive_export.file.read, headers: true)
+
+      expect(csv.length).to be(2)
+    end
+
+    it 'schedules the deletion of the export' do
+      export
+
+      expect(Vulnerabilities::Archival::Export::PurgeWorker)
+        .to have_received(:perform_in).with(24.hours, archive_export.id)
+    end
+
+    context 'when an error happens' do
+      before do
+        allow(Vulnerabilities::Archival::Export::Exporters::CsvService).to receive(:new).and_raise('Foo')
+      end
+
+      it 'does not change the status of the export and propagates the error' do
+        expect { export }.to raise_error('Foo')
+                         .and not_change { archive_export.reload.status }.from('created')
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/vulnerabilities/archival/export/exporters/csv_service_spec.rb b/ee/spec/services/vulnerabilities/archival/export/exporters/csv_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..20e0f8c6fa1339900b7e8cab4d1f52cb18c9089e
--- /dev/null
+++ b/ee/spec/services/vulnerabilities/archival/export/exporters/csv_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::Exporters::CsvService, feature_category: :vulnerability_management do
+  describe '#generate' do
+    let_it_be(:archived_record) { create(:vulnerability_archived_record) }
+
+    let(:export_csv_service) { described_class.new([archived_record.data]) }
+
+    context 'when block is not given' do
+      it 'renders csv to string' do
+        expect(export_csv_service.generate).to be_a(String)
+      end
+    end
+
+    context 'when block is given' do
+      it 'returns handle to Tempfile' do
+        expect(export_csv_service.generate { |file| file }).to be_a(Tempfile)
+      end
+    end
+
+    describe 'CSV content' do
+      let(:csv) { CSV.parse(export_csv_service.generate, headers: true) }
+
+      describe 'headers' do
+        let(:expected_headers) do
+          ['Tool', 'Scanner Name', 'Status', 'Vulnerability', 'Details', 'Severity', 'CVE', 'CWE', 'Other Identifiers',
+            'Detected At', 'Location', 'Activity', 'Comments', 'Full Path', 'CVSS Vectors', 'Dismissal Reason']
+        end
+
+        it 'contains the expected headers' do
+          expect(csv.headers).to eq(expected_headers)
+        end
+      end
+
+      describe 'rows' do
+        it 'serializes correct number of rows' do
+          expect(csv.length).to be(1)
+        end
+
+        it 'serializes the correct content' do
+          expect(csv[0].to_h).to match(
+            {
+              'Tool' => 'sast',
+              'Scanner Name' => 'Find Security Bugs',
+              'Status' => 'dismissed',
+              'Vulnerability' => 'Test Title',
+              'Details' => 'Test Description',
+              'Severity' => 'high',
+              'CVE' => 'CVE-2018-1234',
+              'CWE' => 'CWE-123',
+              'Other Identifiers' => 'OWASP-A01:2021',
+              'Detected At' => '2025-01-29 19:02:08 UTC',
+              'Location' => '{"class"=>"com.gitlab.security_products.tests.App", "end_line"=>29, ' \
+                '"file"=>"maven/src/main/java/com/gitlab/security_products/tests/App.java", ' \
+                '"method"=>"insecureCypher", "start_line"=>29}',
+              'Activity' => 'false',
+              'Comments' => 'Test notes summary',
+              'Full Path' => 'Test full path',
+              'CVSS Vectors' => 'GitLab=CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N',
+              'Dismissal Reason' => 'False positive'
+            }
+          )
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/vulnerabilities/archival/export/purge_service_spec.rb b/ee/spec/services/vulnerabilities/archival/export/purge_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0b3e9d1c67ebb8d5d3615986c87f2b7dc16e9a31
--- /dev/null
+++ b/ee/spec/services/vulnerabilities/archival/export/purge_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::PurgeService, feature_category: :vulnerability_management do
+  describe '.purge' do
+    let(:archive_export) { create(:vulnerability_archive_export, :with_csv_file) }
+
+    subject(:purge) { described_class.purge(archive_export) }
+
+    it 'changes the status of the record' do
+      expect { purge }.to change { archive_export.reload.status }.to('purged')
+    end
+
+    it 'deletes the file associated with the record' do
+      expect { purge }.to change { archive_export.reload.file.file }.to(nil)
+    end
+  end
+end
diff --git a/ee/spec/workers/vulnerabilities/archival/export/export_worker_spec.rb b/ee/spec/workers/vulnerabilities/archival/export/export_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e379d09356a0002ae6a4770c6ba84a68ead08c6
--- /dev/null
+++ b/ee/spec/workers/vulnerabilities/archival/export/export_worker_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::ExportWorker, feature_category: :vulnerability_management do
+  describe '.sidekiq_retries_exhausted' do
+    let(:archive_export_id) { 1 }
+    let(:job) { { 'args' => [1] } }
+
+    subject(:retries_exhausted) { described_class.sidekiq_retries_exhausted_block.call(job) }
+
+    before do
+      allow(Vulnerabilities::Archival::Export::PurgeWorker).to receive(:perform_in)
+    end
+
+    it 'schedules the purge job' do
+      retries_exhausted
+
+      expect(Vulnerabilities::Archival::Export::PurgeWorker)
+        .to have_received(:perform_in).with(24.hours, archive_export_id)
+    end
+  end
+
+  describe '#perform' do
+    let(:worker) { described_class.new }
+
+    subject(:perform_worker) { worker.perform(archive_export_id) }
+
+    before do
+      allow(Vulnerabilities::Archival::Export::ExportService).to receive(:export)
+    end
+
+    context 'when there is no record for the given ID' do
+      let(:archive_export_id) { non_existing_record_id }
+
+      it 'does not run the service layer logic' do
+        perform_worker
+
+        expect(Vulnerabilities::Archival::Export::ExportService).not_to have_received(:export)
+      end
+    end
+
+    context 'when there is a record for the given ID' do
+      let(:archive_export) { create(:vulnerability_archive_export) }
+      let(:archive_export_id) { archive_export.id }
+
+      it 'runs the service layer logic' do
+        perform_worker
+
+        expect(Vulnerabilities::Archival::Export::ExportService).to have_received(:export)
+      end
+    end
+  end
+end
diff --git a/ee/spec/workers/vulnerabilities/archival/export/purge_worker_spec.rb b/ee/spec/workers/vulnerabilities/archival/export/purge_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b59aee5ac4c46289694faba5c4caff7a97cd6a1
--- /dev/null
+++ b/ee/spec/workers/vulnerabilities/archival/export/purge_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Vulnerabilities::Archival::Export::PurgeWorker, feature_category: :vulnerability_management do
+  describe '#perform' do
+    let(:worker) { described_class.new }
+
+    subject(:perform_worker) { worker.perform(archive_export_id) }
+
+    before do
+      allow(Vulnerabilities::Archival::Export::PurgeService).to receive(:purge)
+    end
+
+    context 'when there is no record for the given ID' do
+      let(:archive_export_id) { non_existing_record_id }
+
+      it 'does not run the service layer logic' do
+        perform_worker
+
+        expect(Vulnerabilities::Archival::Export::PurgeService).not_to have_received(:purge)
+      end
+    end
+
+    context 'when there is a record for the given ID' do
+      let(:archive_export) { create(:vulnerability_archive_export) }
+      let(:archive_export_id) { archive_export.id }
+
+      it 'runs the service layer logic' do
+        perform_worker
+
+        expect(Vulnerabilities::Archival::Export::PurgeService).to have_received(:purge)
+      end
+    end
+  end
+end