diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml
index 9b1f404a78598e933215d82fcc80f2ac77830f08..0e227a6fcbeeeef1ba826bc0952c9d1ce3569329 100644
--- a/.rubocop_todo/gitlab/bounded_contexts.yml
+++ b/.rubocop_todo/gitlab/bounded_contexts.yml
@@ -3081,9 +3081,6 @@ Gitlab/BoundedContexts:
     - 'ee/app/services/dashboard/projects/create_service.rb'
     - 'ee/app/services/dashboard/projects/list_service.rb'
     - 'ee/app/services/dependencies/create_export_service.rb'
-    - 'ee/app/services/dependencies/export_serializers/group_dependencies_service.rb'
-    - 'ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb'
-    - 'ee/app/services/dependencies/export_serializers/project_dependencies_service.rb'
     - 'ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb'
     - 'ee/app/services/dependencies/export_service.rb'
     - 'ee/app/services/deployments/approval_service.rb'
diff --git a/ee/app/models/dependencies/dependency_list_export.rb b/ee/app/models/dependencies/dependency_list_export.rb
index 980d2aacc66cb0c2d8def644c2bc44ab41759d50..049eeb52b6079c01ebef22eaf9f009e9e6a77190 100644
--- a/ee/app/models/dependencies/dependency_list_export.rb
+++ b/ee/app/models/dependencies/dependency_list_export.rb
@@ -27,7 +27,9 @@ class DependencyListExport < Gitlab::Database::SecApplicationRecord
 
     enum export_type: {
       dependency_list: 0,
-      sbom: 1
+      sbom: 1,
+      json_array: 2,
+      csv: 3
     }
 
     scope :expired, -> { where(expires_at: ..Time.zone.now) }
diff --git a/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb b/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb
deleted file mode 100644
index fa0ff7540f0de5d0aac3dc62a65bae5b72fc2d9a..0000000000000000000000000000000000000000
--- a/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module Dependencies
-  module ExportSerializers
-    class OrganizationDependenciesService
-      def initialize(export)
-        @export = export
-      end
-
-      def filename
-        "#{export.organization.to_param}_dependencies_#{Time.current.utc.strftime('%FT%H%M')}.csv"
-      end
-
-      def each
-        yield header
-
-        iterator.each_batch do |batch|
-          build_list_for(batch).each do |occurrence|
-            yield to_csv([
-              occurrence.component_name,
-              occurrence.version,
-              occurrence.package_manager,
-              occurrence.location[:blob_path]
-            ])
-          end
-        end
-      end
-
-      private
-
-      attr_reader :export
-
-      def header
-        to_csv(%w[Name Version Packager Location])
-      end
-
-      def iterator
-        Gitlab::Pagination::Keyset::Iterator.new(scope: ::Sbom::Occurrence.order_by_id)
-      end
-
-      def build_list_for(batch)
-        batch
-          .with_source
-          .with_version
-          .with_project_namespace
-      end
-
-      def to_csv(row)
-        CSV.generate_line(row, force_quotes: true)
-      end
-    end
-  end
-end
diff --git a/ee/app/services/dependencies/export_serializers/project_dependencies_service.rb b/ee/app/services/dependencies/export_serializers/project_dependencies_service.rb
deleted file mode 100644
index a3bc0ce60e93a78a2393a40c665339b85c50b1b9..0000000000000000000000000000000000000000
--- a/ee/app/services/dependencies/export_serializers/project_dependencies_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-module Dependencies
-  module ExportSerializers
-    class ProjectDependenciesService
-      def self.execute(dependency_list_export)
-        new(dependency_list_export).execute
-      end
-
-      def initialize(dependency_list_export)
-        @dependency_list_export = dependency_list_export
-      end
-
-      def execute
-        DependencyListEntity.represent(dependencies_list, serializer_parameters)
-      end
-
-      private
-
-      attr_reader :dependency_list_export
-
-      delegate :project, :author, to: :dependency_list_export, private: true
-
-      def dependencies_list
-        ::Sbom::DependenciesFinder.new(project, params: default_filters).execute
-          .with_component
-          .with_version
-          .with_vulnerabilities
-      end
-
-      def default_filters
-        { source_types: default_source_type_filters }
-      end
-
-      def default_source_type_filters
-        ::Sbom::Source::DEFAULT_SOURCES.keys + [nil]
-      end
-
-      def pipeline
-        @pipeline ||= project.latest_ingested_sbom_pipeline
-      end
-
-      def job_artifacts
-        ::Ci::JobArtifact.of_report_type(:dependency_list)
-      end
-
-      def serializer_parameters
-        {
-          request: EntityRequest.new({ project: project, user: author }),
-          pipeline: pipeline,
-          project: project,
-          include_vulnerabilities: true
-        }
-      end
-    end
-  end
-end
diff --git a/ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb b/ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb
index 5d58bb7a7691db3f6bbe533ea91f7200a81c19c2..03e10bd011889c9c841aa0d818d84f33db638581 100644
--- a/ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb
+++ b/ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb
@@ -4,32 +4,30 @@ module Dependencies
   module ExportSerializers
     module Sbom
       class PipelineService
-        SchemaValidationError = Class.new(StandardError)
+        include ::Sbom::Exporters::WriteBlob
 
-        # Creates and execute the service.
-        #
-        # @param dependency_list_export [Dependencies::DependencyListExport]
-        # @return [Sbom::SbomEntity] with sbom information of a pipeline.
-        def self.execute(dependency_list_export)
-          new(dependency_list_export).execute
-        end
+        SchemaValidationError = Class.new(StandardError)
 
-        def initialize(dependency_list_export)
+        def initialize(dependency_list_export, _sbom_occurrences)
           @dependency_list_export = dependency_list_export
 
           @pipeline = dependency_list_export.pipeline
           @project = pipeline.project
         end
 
-        def execute
+        def generate(&block)
+          write_json_blob(sbom_data, &block)
+        end
+
+        private
+
+        def sbom_data
           entity = serializer_service.execute
           return entity if serializer_service.valid?
 
           raise SchemaValidationError, "Invalid CycloneDX report: #{serializer_service.errors.join(', ')}"
         end
 
-        private
-
         def serializer_service
           @service ||= ::Sbom::ExportSerializers::JsonService.new(merged_report)
         end
diff --git a/ee/app/services/dependencies/export_service.rb b/ee/app/services/dependencies/export_service.rb
index 085ea616b362cfc2bbd66b66f84b315a010c0fa6..7574cde9784d454abce20812fe56667158d3aa73 100644
--- a/ee/app/services/dependencies/export_service.rb
+++ b/ee/app/services/dependencies/export_service.rb
@@ -2,16 +2,12 @@
 
 module Dependencies
   class ExportService
-    SERIALIZER_SERVICES = {
-      dependency_list: {
-        Organizations::Organization => ExportSerializers::OrganizationDependenciesService,
-        Project => ExportSerializers::ProjectDependenciesService,
-        Group => ExportSerializers::GroupDependenciesService
-      },
-      sbom: {
-        Ci::Pipeline => ExportSerializers::Sbom::PipelineService
-      }
-    }.with_indifferent_access.freeze
+    EXPORTERS = {
+      dependency_list: ::Sbom::Exporters::DependencyListService,
+      sbom: ::Dependencies::ExportSerializers::Sbom::PipelineService,
+      json_array: ::Sbom::Exporters::JsonArrayService,
+      csv: ::Sbom::Exporters::CsvService
+    }.freeze
 
     def self.execute(dependency_list_export)
       new(dependency_list_export).execute
@@ -36,80 +32,64 @@ def execute
 
     def create_export
       dependency_list_export.start!
-
-      if exportable.is_a?(Organizations::Organization)
-        Tempfile.open('dependencies') do |file|
-          serializer = serializer_service.new(dependency_list_export)
-          serializer.each { |item| file << item }
-
-          dependency_list_export.file = file
-          dependency_list_export.file.filename = serializer.filename
-        end
-      else
-        create_export_file
-      end
-
+      write_export_file
       dependency_list_export.finish!
       dependency_list_export.send_completion_email!
-    rescue StandardError, Dependencies::ExportSerializers::Sbom::PipelineService::SchemaValidationError
+    rescue StandardError
       dependency_list_export.reset_state!
 
       raise
     end
 
-    def create_export_file
-      Tempfile.open('json') do |file|
-        file.write(file_content)
-
-        dependency_list_export.file = file
-        dependency_list_export.file.filename = filename
-      end
+    def write_export_file
+      exporter.generate { |file| dependency_list_export.file = file }
+      dependency_list_export.file.filename = filename
     end
 
-    def file_content
-      ::Gitlab::Json.dump(exported_object)
+    def exporter
+      EXPORTERS[dependency_list_export.export_type.to_sym].new(dependency_list_export, sbom_occurrences)
     end
 
-    def exported_object
-      serializer_service.execute(dependency_list_export)
+    def sbom_occurrences
+      case exportable
+      when ::Project
+        ::Sbom::DependenciesFinder.new(exportable, params: default_filters).execute
+      when ::Group
+        exportable.sbom_occurrences.order_by_id
+      when ::Organizations::Organization
+        ::Sbom::Occurrence.order_by_id
+      end
     end
 
-    def serializer_service
-      SERIALIZER_SERVICES.fetch(dependency_list_export.export_type).fetch(exportable.class)
+    def default_filters
+      { source_types: default_source_type_filters }
     end
 
-    def filename
-      if dependency_list_export.export_type == 'sbom'
-        sbom_filename
-      else
-        dependency_list_filename
-      end
+    def default_source_type_filters
+      ::Sbom::Source::DEFAULT_SOURCES.keys + [nil]
     end
 
-    def sbom_filename
-      # Assuming dependency_list_export.export_type is sbom
-      # as we don't support dependency_list export_type for pipeline yet.
+    def filename
       [
-        'gl-',
-        'pipeline-',
+        exportable.class.name.demodulize.underscore,
+        '_',
         exportable.id,
-        '-merged-',
+        '_dependencies_',
         Time.current.utc.strftime('%FT%H%M'),
-        '-sbom.',
-        'cdx',
         '.',
-        'json'
+        file_extension
       ].join
     end
 
-    def dependency_list_filename
-      [
-        exportable.full_path.parameterize,
-        '_dependencies_',
-        Time.current.utc.strftime('%FT%H%M'),
-        '.',
+    def file_extension
+      case dependency_list_export.export_type
+      when 'sbom'
+        'cdx.json'
+      when 'dependency_list', 'json_array'
         'json'
-      ].join
+      when 'csv'
+        'csv'
+      end
     end
   end
 end
diff --git a/ee/app/services/sbom/exporters/csv_service.rb b/ee/app/services/sbom/exporters/csv_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d74e77c1dff276afe6b68a6a8a5b84b396d9e3e2
--- /dev/null
+++ b/ee/app/services/sbom/exporters/csv_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Sbom
+  module Exporters
+    class CsvService
+      attr_reader :sbom_occurrences
+
+      def initialize(_export, sbom_occurrences)
+        @sbom_occurrences = sbom_occurrences
+      end
+
+      def generate(&block)
+        csv_builder.render(&block)
+      end
+
+      def header
+        CSV.generate_line(mapping.keys)
+      end
+
+      private
+
+      def preloads
+        [
+          :source,
+          :component_version,
+          { project: [namespace: :route] }
+        ]
+      end
+
+      def csv_builder
+        @csv_builder ||= CsvBuilder.new(sbom_occurrences, mapping, preloads,
+          replace_newlines: true)
+      end
+
+      def mapping
+        {
+          s_('DependencyListExport|Name') => 'component_name',
+          s_('DependencyListExport|Version') => 'version',
+          s_('DependencyListExport|Packager') => 'package_manager',
+          s_('DependencyListExport|Location') => ->(occurrence) { occurrence.location[:blob_path] }
+        }
+      end
+    end
+  end
+end
diff --git a/ee/app/services/sbom/exporters/dependency_list_service.rb b/ee/app/services/sbom/exporters/dependency_list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..65023fba73569b6411427c86ecac462b5e614404
--- /dev/null
+++ b/ee/app/services/sbom/exporters/dependency_list_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Sbom
+  module Exporters
+    class DependencyListService
+      include WriteBlob
+
+      attr_reader :export, :relation
+
+      def initialize(export, relation)
+        @export = export
+        @relation = relation
+      end
+
+      def generate(&block)
+        write_json_blob(blob, &block)
+      end
+
+      private
+
+      delegate :project, :author, to: :export
+
+      def blob
+        DependencyListEntity.represent(sbom_occurrences, serializer_parameters)
+      end
+
+      def sbom_occurrences
+        # rubocop:disable CodeReuse/ActiveRecord -- Preloading logic is coupled with DependencyListEntity
+        relation.preload(*preloads)
+        # rubocop:enable CodeReuse/ActiveRecord
+      end
+
+      def preloads
+        [
+          :component,
+          :component_version,
+          :vulnerabilities
+        ]
+      end
+
+      def serializer_parameters
+        {
+          request: EntityRequest.new({ project: project, user: author }),
+          pipeline: pipeline,
+          project: project,
+          include_vulnerabilities: true
+        }
+      end
+
+      def pipeline
+        project.latest_ingested_sbom_pipeline
+      end
+    end
+  end
+end
diff --git a/ee/app/services/dependencies/export_serializers/group_dependencies_service.rb b/ee/app/services/sbom/exporters/json_array_service.rb
similarity index 50%
rename from ee/app/services/dependencies/export_serializers/group_dependencies_service.rb
rename to ee/app/services/sbom/exporters/json_array_service.rb
index 33c05dda7ba67df3c5c189c0132a245a80ab6b08..628c716cc18b3db8252e6847fcbbdb15954b9438 100644
--- a/ee/app/services/dependencies/export_serializers/group_dependencies_service.rb
+++ b/ee/app/services/sbom/exporters/json_array_service.rb
@@ -1,30 +1,30 @@
 # frozen_string_literal: true
 
-module Dependencies
-  module ExportSerializers
-    class GroupDependenciesService
-      def self.execute(dependency_list_export)
-        new(dependency_list_export).execute
+module Sbom
+  module Exporters
+    class JsonArrayService
+      include WriteBlob
+
+      def initialize(_export, sbom_occurrences)
+        @sbom_occurrences = sbom_occurrences
       end
 
-      def initialize(dependency_list_export)
-        @dependency_list_export = dependency_list_export
+      attr_reader :sbom_occurrences
+
+      def generate(&block)
+        write_json_blob(data, &block)
       end
 
-      def execute
+      private
+
+      def data
         [].tap do |list|
-          group_dependencies.in_batches do |batch| # rubocop: disable Cop/InBatches
+          iterator.each_batch do |batch|
             list.concat(build_list_for(batch))
           end
         end
       end
 
-      private
-
-      attr_reader :dependency_list_export
-
-      delegate :group, to: :dependency_list_export, private: true
-
       def build_list_for(batch)
         batch.with_source.with_version.map do |occurrence|
           {
@@ -37,8 +37,8 @@ def build_list_for(batch)
         end
       end
 
-      def group_dependencies
-        group.sbom_occurrences.order_by_id
+      def iterator
+        Gitlab::Pagination::Keyset::Iterator.new(scope: sbom_occurrences)
       end
     end
   end
diff --git a/ee/app/services/sbom/exporters/write_blob.rb b/ee/app/services/sbom/exporters/write_blob.rb
new file mode 100644
index 0000000000000000000000000000000000000000..59704e014924a88965c3cc5e5dfd5c3fe20a88cd
--- /dev/null
+++ b/ee/app/services/sbom/exporters/write_blob.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Sbom
+  module Exporters
+    module WriteBlob
+      def write_blob(blob)
+        Tempfile.open('write_blob') do |file|
+          file.write(blob)
+
+          if block_given?
+            yield file
+          else
+            file.rewind
+            file.read
+          end
+        end
+      end
+
+      def write_json_blob(data, &block)
+        blob = ::Gitlab::Json.dump(data)
+        write_blob(blob, &block)
+      end
+    end
+  end
+end
diff --git a/ee/lib/api/dependency_list_exports.rb b/ee/lib/api/dependency_list_exports.rb
index f9967b6f273afe5bbfe74e5a5fb88d2b5232aa2b..77ea2e5bf4abc59b6e3f621ae3e59197e95dbb2c 100644
--- a/ee/lib/api/dependency_list_exports.rb
+++ b/ee/lib/api/dependency_list_exports.rb
@@ -45,7 +45,7 @@ def present_created_export(result)
       post ':id/dependency_list_exports' do
         authorize! :read_dependency, user_group
 
-        result = ::Dependencies::CreateExportService.new(user_group, current_user).execute
+        result = ::Dependencies::CreateExportService.new(user_group, current_user, :json_array).execute
 
         present_created_export(result)
       end
@@ -63,7 +63,7 @@ def present_created_export(result)
         authorize! :read_dependency, organization
 
         result = ::Dependencies::CreateExportService
-          .new(organization, current_user)
+          .new(organization, current_user, :csv)
           .execute
 
         present_created_export(result)
diff --git a/ee/spec/requests/api/dependency_list_exports_spec.rb b/ee/spec/requests/api/dependency_list_exports_spec.rb
index aa2ad726958ff38ce596a11db504cf439af2f1b2..b3414745e903d042e4d21a5e747c41f972da0298 100644
--- a/ee/spec/requests/api/dependency_list_exports_spec.rb
+++ b/ee/spec/requests/api/dependency_list_exports_spec.rb
@@ -8,7 +8,7 @@
   let_it_be(:project) { create(:project) }
   let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
   let(:export_type) { nil }
-  let(:params) { export_type }
+  let(:params) { { export_type: export_type } }
 
   shared_examples_for 'creating dependency list export' do
     subject(:create_dependency_list_export) { post api(request_path, user), params: params }
@@ -53,12 +53,6 @@
           stub_licensed_features(dependency_scanning: true, security_dashboard: true)
         end
 
-        let(:args) do
-          args = [exportable, user]
-          args << params[:export_type] if Hash(params)[:export_type]
-          args
-        end
-
         it 'creates and returns a dependency_list_export' do
           create_dependency_list_export
 
@@ -67,6 +61,9 @@
           expect(json_response).to have_key('has_finished')
           expect(json_response).to have_key('self')
           expect(json_response).to have_key('download')
+
+          created_export = ::Dependencies::DependencyListExport.find(json_response['id'])
+          expect(created_export.export_type).to eq(export_type)
         end
 
         context 'when export creation fails' do
@@ -108,6 +105,9 @@
           expect(json_response).to have_key('has_finished')
           expect(json_response).to have_key('self')
           expect(json_response).to have_key('download')
+
+          created_export = ::Dependencies::DependencyListExport.find(json_response['id'])
+          expect(created_export.export_type).to eq('csv')
         end
 
         context 'when the `explore_dependencies` feature flag is disabled' do
@@ -129,6 +129,7 @@
     let(:request_path) { "/projects/#{project.id}/dependency_list_exports" }
     let(:resource) { project }
     let(:exportable) { project }
+    let(:export_type) { 'dependency_list' }
 
     it_behaves_like 'creating dependency list export'
   end
@@ -137,6 +138,7 @@
     let(:request_path) { "/groups/#{group.id}/dependency_list_exports" }
     let(:resource) { group }
     let(:exportable) { group }
+    let(:export_type) { 'json_array' }
 
     it_behaves_like 'creating dependency list export'
   end
@@ -145,7 +147,7 @@
     let(:request_path) { "/pipelines/#{pipeline.id}/dependency_list_exports" }
     let(:resource) { project }
     let(:exportable) { pipeline }
-    let(:params) { { export_type: 'sbom' } }
+    let(:export_type) { 'sbom' }
 
     it_behaves_like 'creating dependency list export'
   end
diff --git a/ee/spec/services/dependencies/export_serializers/organization_dependencies_service_spec.rb b/ee/spec/services/dependencies/export_serializers/organization_dependencies_service_spec.rb
deleted file mode 100644
index fca5dc216a69e00a39f9ad380af5e8607710a819..0000000000000000000000000000000000000000
--- a/ee/spec/services/dependencies/export_serializers/organization_dependencies_service_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Dependencies::ExportSerializers::OrganizationDependenciesService, feature_category: :dependency_management do
-  let_it_be(:organization) { create(:organization) }
-  let_it_be(:user) { create(:user) }
-  let_it_be_with_reload(:project) { create(:project, organization: organization) }
-  let_it_be(:export) { create(:dependency_list_export, project: nil, organization: organization, author: user) }
-
-  let(:service_class) { described_class.new(export) }
-
-  describe '#each' do
-    subject(:dependencies) { service_class.enum_for(:each).to_a }
-
-    let(:header) { CSV.generate_line(%w[Name Version Packager Location], force_quotes: true) }
-
-    context 'when the organization does not have dependencies' do
-      it { is_expected.to match_array(header) }
-    end
-
-    context 'when the organization has dependencies' do
-      let_it_be(:bundler) { create(:sbom_component, :bundler) }
-      let_it_be(:bundler_v1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
-
-      let_it_be(:occurrence_1) do
-        create(:sbom_occurrence, :mit, project: project, component: bundler, component_version: bundler_v1)
-      end
-
-      context 'when the user is an organization owner' do
-        let_it_be(:organization_user) { create(:organization_user, :owner, organization: organization, user: user) }
-
-        it 'includes each occurrence', :aggregate_failures do
-          expect(dependencies.count).to eq(2)
-          expect(dependencies).to match_array([
-            header,
-            CSV.generate_line([
-              occurrence_1.component_name,
-              occurrence_1.version,
-              occurrence_1.package_manager,
-              occurrence_1.location[:blob_path]
-            ], force_quotes: true)
-          ])
-        end
-      end
-
-      context 'when the user is an admin', :enable_admin_mode do
-        before_all do
-          user.update!(admin: true)
-        end
-
-        it 'includes each occurrence' do
-          expect(dependencies).to match_array([
-            header,
-            CSV.generate_line([
-              occurrence_1.component_name,
-              occurrence_1.version,
-              occurrence_1.package_manager,
-              occurrence_1.location[:blob_path]
-            ], force_quotes: true)
-          ])
-        end
-      end
-
-      it 'avoids N+1 queries' do
-        control = ActiveRecord::QueryRecorder.new do
-          service_class.enum_for(:each).to_a
-        end
-
-        create_list(:project, 3, organization: organization).each do |project|
-          create(:sbom_occurrence, project: project, source: create(:sbom_source))
-        end
-
-        expect do
-          service_class.enum_for(:each).to_a
-        end.to issue_same_number_of_queries_as(control).or_fewer
-      end
-    end
-  end
-
-  describe '#filename' do
-    let(:timestamp) { Time.current.utc.strftime('%FT%H%M') }
-
-    subject { service_class.filename }
-
-    it { is_expected.to eq("#{organization.to_param}_dependencies_#{timestamp}.csv") }
-  end
-end
diff --git a/ee/spec/services/dependencies/export_serializers/sbom/pipeline_service_spec.rb b/ee/spec/services/dependencies/export_serializers/sbom/pipeline_service_spec.rb
index d93818209b95e0b6b7ad21774dc211fb2583417e..629edce96d2ace5a4a11aa7ded3b573b669de28d 100644
--- a/ee/spec/services/dependencies/export_serializers/sbom/pipeline_service_spec.rb
+++ b/ee/spec/services/dependencies/export_serializers/sbom/pipeline_service_spec.rb
@@ -5,26 +5,12 @@
 RSpec.describe Dependencies::ExportSerializers::Sbom::PipelineService, feature_category: :dependency_management do
   let_it_be(:pipeline) { create(:ee_ci_pipeline, :with_cyclonedx_report) }
 
-  describe '.execute' do
-    let(:dependency_list_export) { instance_double(Dependencies::DependencyListExport, pipeline: pipeline) }
-
-    subject(:execute) { described_class.execute(dependency_list_export) }
-
-    it 'instantiates a service object and sends execute message to it' do
-      expect_next_instance_of(described_class, dependency_list_export) do |service_object|
-        expect(service_object).to receive(:execute)
-      end
-
-      execute
-    end
-  end
-
-  describe '#execute' do
+  describe '#generate' do
     let(:dependency_list_export) { create(:dependency_list_export, project: nil, exportable: pipeline) }
 
-    let(:service_class) { described_class.new(dependency_list_export) }
+    let(:service_class) { described_class.new(dependency_list_export, nil) }
 
-    subject(:components) { service_class.execute.as_json[:components] }
+    subject(:components) { Gitlab::Json.parse(service_class.generate)['components'] }
 
     before do
       stub_licensed_features(dependency_scanning: true)
@@ -56,7 +42,7 @@
       end
 
       it 'raises a SchemaValidationError' do
-        expect { service_class.execute }.to raise_error(
+        expect { service_class.generate }.to raise_error(
           described_class::SchemaValidationError
         ).with_message(/Invalid CycloneDX report: /)
       end
diff --git a/ee/spec/services/dependencies/export_service_spec.rb b/ee/spec/services/dependencies/export_service_spec.rb
index 72140f83b924ec96db519704bf7e047bd1d023f6..0f9d9ce389c727e46b68e3447d0042414e087749 100644
--- a/ee/spec/services/dependencies/export_service_spec.rb
+++ b/ee/spec/services/dependencies/export_service_spec.rb
@@ -3,6 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe Dependencies::ExportService, feature_category: :dependency_management do
+  include ::Sbom::Exporters::WriteBlob
+
   describe '.execute' do
     let(:dependency_list_export) { instance_double(Dependencies::DependencyListExport) }
 
@@ -21,20 +23,22 @@
     let(:created_status) { 0 }
     let(:running_status) { 1 }
     let(:finished_status) { 2 }
+    let(:status) { created_status }
     let(:service_class) { described_class.new(dependency_list_export) }
+    let(:export_content) { dependency_list_export.reload.file.read }
+
+    subject(:execute) { described_class.new(dependency_list_export).execute }
 
     before do
       allow(Time).to receive(:current).and_return(Time.new(2023, 11, 14, 0, 0, 0, '+00:00'))
     end
 
-    shared_examples_for 'export service' do |serializer_service|
-      subject(:export) { service_class.execute }
-
+    shared_examples_for 'writes export using exporter' do |exporter_class|
       context 'when the export is not in `created` status' do
         let(:status) { running_status }
 
         it 'does not run the logic' do
-          expect { export }.not_to change { dependency_list_export.reload.file.file }.from(nil)
+          expect { execute }.not_to change { dependency_list_export.reload.file.file }.from(nil)
         end
       end
 
@@ -47,11 +51,13 @@
 
         context 'when the export fails' do
           before do
-            allow(serializer_service).to receive(:execute).and_raise('Foo')
+            allow_next_instance_of(exporter_class, dependency_list_export, anything) do |instance|
+              allow(instance).to receive(:generate).and_raise('Foo')
+            end
           end
 
           it 'propagates the error, resets the status of the export, and does not schedule deletion job' do
-            expect { export }.to raise_error('Foo')
+            expect { execute }.to raise_error('Foo')
                              .and not_change { dependency_list_export.status }
 
             expect(dependency_list_export).not_to have_received(:schedule_export_deletion)
@@ -60,20 +66,22 @@
 
         context 'when the export succeeds' do
           before do
-            allow(serializer_service).to receive(:execute).with(dependency_list_export).and_return('Foo')
+            allow_next_instance_of(exporter_class, dependency_list_export, anything) do |instance|
+              allow(instance).to receive(:generate) { |&block| write_blob('"Foo"', &block) }
+            end
           end
 
           it 'marks the export as finished' do
-            expect { export }.to change { dependency_list_export.status }.from(created_status).to(finished_status)
+            expect { execute }.to change { dependency_list_export.status }.from(created_status).to(finished_status)
           end
 
           it 'attaches the file to export' do
-            expect { export }.to change { dependency_list_export.file.read }.from(nil).to('"Foo"')
+            expect { execute }.to change { dependency_list_export.file.read }.from(nil).to('"Foo"')
             expect(dependency_list_export.file.filename).to eq(expected_filename)
           end
 
           it 'schedules the export deletion' do
-            export
+            execute
 
             expect(dependency_list_export).to have_received(:schedule_export_deletion)
           end
@@ -81,118 +89,131 @@
       end
     end
 
-    context 'when export type is dependency_list' do
+    context 'when the exportable is an organization' do
+      let_it_be(:organization) { create(:organization) }
+      let_it_be(:project) { create(:project, organization: organization) }
+      let_it_be(:occurrences) { create_list(:sbom_occurrence, 2, project: project) }
+      let_it_be_with_reload(:dependency_list_export) do
+        create(:dependency_list_export, project: nil, exportable: organization, export_type: :csv)
+      end
+
       let(:timestamp) { Time.current.utc.strftime('%FT%H%M') }
-      let(:export_type) { :dependency_list }
+      let(:expected_filename) { "organization_#{organization.id}_dependencies_#{timestamp}.csv" }
 
-      context 'when the exportable is an organization' do
-        subject(:execute) { described_class.new(export).execute }
+      before_all do
+        project.add_developer(dependency_list_export.author)
+      end
 
-        let_it_be(:organization) { create(:organization) }
-        let_it_be(:project) { create(:project, organization: organization) }
-        let_it_be(:occurrences) { create_list(:sbom_occurrence, 2, project: project) }
-        let_it_be_with_reload(:export) { create(:dependency_list_export, project: nil, exportable: organization) }
-        let(:expected_filename) { "#{organization.to_param}_dependencies_#{timestamp}.csv" }
+      it { expect(execute).to be_present }
+      it { expect { execute }.to change { dependency_list_export.file.filename }.to(expected_filename) }
 
-        before_all do
-          project.add_developer(export.author)
-        end
+      it 'includes a header in the export file' do
+        header = 'Name,Version,Packager,Location'
+        expect { execute }.to change { dependency_list_export.file.read }.to(include(header))
+      end
 
-        it { expect(execute).to be_present }
-        it { expect { execute }.to change { export.file.filename }.to(expected_filename) }
+      it 'includes a row for each occurrence' do
+        execute
 
-        it 'includes a header in the export file' do
-          header = '"Name","Version","Packager","Location"'
-          expect { execute }.to change { export.file.read }.to(include(header))
+        occurrences.map do |occurrence|
+          expect(export_content).to include(CSV.generate_line([
+            occurrence.component_name,
+            occurrence.version,
+            occurrence.package_manager,
+            occurrence.send(:input_file_blob_path)
+          ]))
         end
+      end
+    end
 
-        it 'includes a row for each occurrence' do
-          execute
-
-          content = export.file.read
-          occurrences.map do |occurrence|
-            expect(content).to include(CSV.generate_line([
-              occurrence.component_name,
-              occurrence.version,
-              occurrence.package_manager,
-              occurrence.send(:input_file_blob_path)
-            ], force_quotes: true))
-          end
-        end
+    context 'when the exportable is a project' do
+      let_it_be(:project) { create(:project) }
+      let_it_be(:container_scanning_occurrence) { create(:sbom_occurrence, :os_occurrence, project: project) }
+      let_it_be(:registry_occurrence) { create(:sbom_occurrence, :registry_occurrence, project: project) }
+
+      let(:export_type) { :dependency_list }
+      let(:dependency_list_export) do
+        create(:dependency_list_export, project: nil, exportable: project, status: status, export_type: export_type)
       end
 
-      context 'when the exportable is a project' do
-        let_it_be(:project) { create(:project) }
-
-        let(:expected_filename) do
-          [
-            project.full_path.parameterize,
-            '_dependencies_',
-            Time.current.utc.strftime('%FT%H%M'),
-            '.',
-            'json'
-          ].join
-        end
+      let(:expected_filename) do
+        [
+          'project_',
+          project.id,
+          '_dependencies_',
+          Time.current.utc.strftime('%FT%H%M'),
+          '.',
+          'json'
+        ].join
+      end
 
-        it_behaves_like 'export service', Dependencies::ExportSerializers::ProjectDependenciesService do
-          let(:dependency_list_export) do
-            create(:dependency_list_export, project: nil, exportable: project, status: status, export_type: export_type)
-          end
-        end
+      it 'does not include registry occurrences' do
+        execute
+
+        expect(export_content).to include(container_scanning_occurrence.name)
+        expect(export_content).not_to include(registry_occurrence.name)
       end
 
-      context 'when the exportable is a group' do
-        let_it_be(:group) { create(:group) }
-
-        let(:expected_filename) do
-          [
-            group.full_path.parameterize,
-            '_dependencies_',
-            Time.current.utc.strftime('%FT%H%M'),
-            '.',
-            'json'
-          ].join
-        end
+      it_behaves_like 'writes export using exporter', ::Sbom::Exporters::DependencyListService
+    end
 
-        it_behaves_like 'export service', Dependencies::ExportSerializers::GroupDependenciesService do
-          let(:dependency_list_export) do
-            create(:dependency_list_export, project: nil, exportable: group, status: status, export_type: export_type)
-          end
-        end
+    context 'when the exportable is a group' do
+      let_it_be(:group) { create(:group) }
+      let_it_be(:project) { create(:project, group: group) }
+      let_it_be(:archived_project) { create(:project, :archived, group: group) }
+      let_it_be(:occurrence) { create(:sbom_occurrence, project: project) }
+      let_it_be(:archived_occurrence) { create(:sbom_occurrence, project: archived_project) }
+
+      let(:export_type) { :json_array }
+
+      let(:expected_filename) do
+        [
+          'group_',
+          group.id,
+          '_dependencies_',
+          Time.current.utc.strftime('%FT%H%M'),
+          '.',
+          'json'
+        ].join
+      end
+
+      let(:dependency_list_export) do
+        create(:dependency_list_export, project: nil, exportable: group, status: status, export_type: export_type)
       end
+
+      it 'does not include occurrences from archived projects' do
+        execute
+
+        expect(export_content).to include(occurrence.name)
+        expect(export_content).not_to include(archived_occurrence.name)
+      end
+
+      it_behaves_like 'writes export using exporter', ::Sbom::Exporters::JsonArrayService
     end
 
-    context 'when export type is sbom' do
-      let(:export_type) { :sbom }
-
-      context 'when the exportable is a pipeline' do
-        let_it_be(:pipeline) { create(:ci_pipeline) }
-
-        let(:expected_filename) do
-          [
-            'gl-',
-            'pipeline-',
-            pipeline.id,
-            '-merged-',
-            Time.current.utc.strftime('%FT%H%M'),
-            '-sbom.',
-            'cdx',
-            '.',
-            'json'
-          ].join
-        end
+    context 'when the exportable is a pipeline' do
+      let_it_be(:pipeline) { create(:ci_pipeline) }
+
+      let(:expected_filename) do
+        [
+          'pipeline_',
+          pipeline.id,
+          '_dependencies_',
+          Time.current.utc.strftime('%FT%H%M'),
+          '.cdx.json'
+        ].join
+      end
 
-        it_behaves_like 'export service', Dependencies::ExportSerializers::Sbom::PipelineService do
-          let(:dependency_list_export) do
-            create(:dependency_list_export, {
-              project: nil,
-              exportable: pipeline,
-              status: status,
-              export_type: export_type
-            })
-          end
-        end
+      let(:dependency_list_export) do
+        create(:dependency_list_export, {
+          project: nil,
+          exportable: pipeline,
+          status: status,
+          export_type: :sbom
+        })
       end
+
+      it_behaves_like 'writes export using exporter', ::Dependencies::ExportSerializers::Sbom::PipelineService
     end
   end
 end
diff --git a/ee/spec/services/sbom/exporters/csv_service_spec.rb b/ee/spec/services/sbom/exporters/csv_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60dfba84c46833cfe932da5dcaa392821850beb7
--- /dev/null
+++ b/ee/spec/services/sbom/exporters/csv_service_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sbom::Exporters::CsvService, feature_category: :dependency_management do
+  let_it_be(:project) { create(:project) }
+  let_it_be(:export) { build_stubbed(:dependency_list_export) }
+  let_it_be(:sbom_occurrences) { Sbom::Occurrence.all }
+
+  let(:service_class) { described_class.new(export, sbom_occurrences) }
+
+  describe '#header' do
+    subject { service_class.header }
+
+    it { is_expected.to eq("Name,Version,Packager,Location\n") }
+  end
+
+  context 'when block is not given' do
+    it 'renders csv to string' do
+      expect(service_class.generate).to be_a String
+    end
+  end
+
+  context 'when block is given' do
+    it 'returns handle to Tempfile' do
+      expect(service_class.generate { |file| file }).to be_a Tempfile
+    end
+  end
+
+  describe '#generate' do
+    subject(:csv) { CSV.parse(service_class.generate, headers: true) }
+
+    let(:header) { %w[Name Version Packager Location] }
+
+    context 'when the organization does not have dependencies' do
+      it { is_expected.to match_array([header]) }
+    end
+
+    context 'when the organization has dependencies' do
+      let_it_be(:bundler) { create(:sbom_component, :bundler) }
+      let_it_be(:bundler_v1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
+
+      let_it_be(:occurrence) do
+        create(:sbom_occurrence, :mit, project: project, component: bundler, component_version: bundler_v1)
+      end
+
+      it 'returns correct content' do
+        expect(csv[0]['Name']).to eq(occurrence.name)
+        expect(csv[0]['Version']).to eq(occurrence.version)
+        expect(csv[0]['Packager']).to eq(occurrence.package_manager)
+        expect(csv[0]['Location']).to eq(occurrence.location[:blob_path])
+      end
+
+      it 'avoids N+1 queries' do
+        control = ActiveRecord::QueryRecorder.new do
+          service_class.generate
+        end
+
+        create_list(:sbom_occurrence, 3, project: project, source: create(:sbom_source))
+
+        expect do
+          service_class.generate
+        end.to issue_same_number_of_queries_as(control).or_fewer
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/dependencies/export_serializers/project_dependencies_service_spec.rb b/ee/spec/services/sbom/exporters/dependency_list_service_spec.rb
similarity index 70%
rename from ee/spec/services/dependencies/export_serializers/project_dependencies_service_spec.rb
rename to ee/spec/services/sbom/exporters/dependency_list_service_spec.rb
index 7924ba9fe86fa595f03ff98bed0394acd541055c..2853ff9cb73f8b229035f18f49ce31175c538980 100644
--- a/ee/spec/services/dependencies/export_serializers/project_dependencies_service_spec.rb
+++ b/ee/spec/services/sbom/exporters/dependency_list_service_spec.rb
@@ -2,29 +2,16 @@
 
 require 'spec_helper'
 
-RSpec.describe Dependencies::ExportSerializers::ProjectDependenciesService, feature_category: :dependency_management do
-  describe '.execute' do
-    let(:dependency_list_export) { instance_double(Dependencies::DependencyListExport) }
-
-    subject(:execute) { described_class.execute(dependency_list_export) }
-
-    it 'instantiates a service object and sends execute message to it' do
-      expect_next_instance_of(described_class, dependency_list_export) do |service_object|
-        expect(service_object).to receive(:execute)
-      end
-
-      execute
-    end
-  end
-
-  describe '#execute' do
+RSpec.describe Sbom::Exporters::DependencyListService, feature_category: :dependency_management do
+  describe '#generate' do
     let_it_be(:author) { create(:user) }
     let_it_be(:project) { create(:project, :public, developers: [author]) }
     let_it_be(:dependency_list_export) { create(:dependency_list_export, project: project, author: author) }
+    let_it_be(:sbom_occurrences) { project.sbom_occurrences }
 
-    let(:service_class) { described_class.new(dependency_list_export) }
+    let(:service_class) { described_class.new(dependency_list_export, sbom_occurrences) }
 
-    subject(:dependencies) { service_class.execute.as_json[:dependencies] }
+    subject(:dependencies) { Gitlab::Json.parse(service_class.generate)['dependencies'] }
 
     before do
       stub_licensed_features(dependency_scanning: true, license_scanning: true, security_dashboard: true)
@@ -36,9 +23,6 @@
 
     context 'when project has dependencies' do
       let_it_be(:occurrences) { create_list(:sbom_occurrence, 2, :with_vulnerabilities, :mit, project: project) }
-      let_it_be(:unexpected_occurrences) do
-        create(:sbom_occurrence, :registry_occurrence, :with_vulnerabilities, project: project)
-      end
 
       def json_dependency(occurrence)
         vulnerabilities = occurrence.vulnerabilities.map do |vulnerability|
@@ -71,16 +55,12 @@ def json_dependency(occurrence)
       it 'returns expected dependencies' do
         expected_dependencies = occurrences.map { |occurrence| json_dependency(occurrence) }
 
-        expect(dependencies.as_json).to match_array(expected_dependencies)
-      end
-
-      it 'returns data only for DEFAULT_SOURCES' do
-        expect(dependencies.pluck(:name)).not_to include(unexpected_occurrences.component_name)
+        expect(dependencies).to match_array(expected_dependencies)
       end
 
       it 'does not have N+1 queries', :request_store do
         def render
-          entity = described_class.new(dependency_list_export).execute
+          entity = described_class.new(dependency_list_export, sbom_occurrences).generate
           Gitlab::Json.dump(entity)
         end
 
diff --git a/ee/spec/services/dependencies/export_serializers/group_dependencies_service_spec.rb b/ee/spec/services/sbom/exporters/json_array_service_spec.rb
similarity index 73%
rename from ee/spec/services/dependencies/export_serializers/group_dependencies_service_spec.rb
rename to ee/spec/services/sbom/exporters/json_array_service_spec.rb
index 029fe5f89d447c6798f675db05e4c3196ee5efea..5d842176712d6ded11c81596437bcd6fee6f118e 100644
--- a/ee/spec/services/dependencies/export_serializers/group_dependencies_service_spec.rb
+++ b/ee/spec/services/sbom/exporters/json_array_service_spec.rb
@@ -2,28 +2,13 @@
 
 require 'spec_helper'
 
-RSpec.describe Dependencies::ExportSerializers::GroupDependenciesService, feature_category: :dependency_management do
-  describe '.execute' do
-    let(:dependency_list_export) { instance_double(Dependencies::DependencyListExport) }
+RSpec.describe Sbom::Exporters::JsonArrayService, feature_category: :dependency_management do
+  describe '#generate' do
+    let_it_be(:group) { create(:group) }
+    let(:sbom_occurrences) { Sbom::Occurrence.for_namespace_and_descendants(group).order_by_id }
+    let(:service_class) { described_class.new(nil, sbom_occurrences) }
 
-    subject(:execute) { described_class.execute(dependency_list_export) }
-
-    it 'instantiates a service object and sends execute message to it' do
-      expect_next_instance_of(described_class, dependency_list_export) do |service_object|
-        expect(service_object).to receive(:execute)
-      end
-
-      execute
-    end
-  end
-
-  describe '#execute' do
-    let_it_be(:group) { create(:group, :public) }
-    let_it_be(:dependency_list_export) { create(:dependency_list_export, group: group, project: nil) }
-
-    let(:service_class) { described_class.new(dependency_list_export) }
-
-    subject(:dependencies) { service_class.execute.as_json }
+    subject(:dependencies) { Gitlab::Json.parse(service_class.generate) }
 
     before do
       stub_licensed_features(dependency_scanning: true)
@@ -35,8 +20,6 @@
 
     context 'when the group has dependencies' do
       let_it_be(:project) { create(:project, :public, group: group) }
-      let_it_be(:archived_project) { create(:project, :archived, group: group) }
-      let_it_be(:archived_occurrence) { create(:sbom_occurrence, project: archived_project) }
 
       let_it_be(:bundler) { create(:sbom_component, :bundler) }
       let_it_be(:bundler_v1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9ff9f0810a3af0ad706fe8206e03613642a2ab81..b558283377d53fff5d55befd7845a1f0598cc7aa 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19501,6 +19501,18 @@ msgstr ""
 msgid "Dependency list"
 msgstr ""
 
+msgid "DependencyListExport|Location"
+msgstr ""
+
+msgid "DependencyListExport|Name"
+msgstr ""
+
+msgid "DependencyListExport|Packager"
+msgstr ""
+
+msgid "DependencyListExport|Version"
+msgstr ""
+
 msgid "DependencyProxy|%{docLinkStart}See the documentation%{docLinkEnd} for other ways to store Docker images in Dependency Proxy cache."
 msgstr ""