From 037bd59644010b04429662e18585c959c3ae38f1 Mon Sep 17 00:00:00 2001 From: mo khan <mo@mokhan.ca> Date: Fri, 1 Mar 2024 18:39:04 +0000 Subject: [PATCH] Add Export to Explore Dependency list page --- ...anization_id_to_dependency_list_exports.rb | 9 ++ ...anization_id_on_dependency_list_exports.rb | 16 +++ ...anization_id_on_dependency_list_exports.rb | 17 +++ db/schema_migrations/20240206224725 | 1 + db/schema_migrations/20240206225046 | 1 + db/schema_migrations/20240208235322 | 1 + db/structure.sql | 6 + .../dependencies/dependency_list_export.rb | 31 ++--- .../organization_dependencies_service.rb | 55 ++++++++ .../services/dependencies/export_service.rb | 13 +- .../explore/dependencies/index.html.haml | 2 +- ee/lib/api/dependency_list_exports.rb | 18 +++ .../dependency_list_export_spec.rb | 126 +++++++----------- .../api/dependency_list_exports_spec.rb | 37 +++++ .../organization_dependencies_service_spec.rb | 64 +++++++++ .../sbom/pipeline_service_spec.rb | 2 +- .../dependencies/export_service_spec.rb | 44 +++++- 17 files changed, 334 insertions(+), 109 deletions(-) create mode 100644 db/migrate/20240206224725_add_organization_id_to_dependency_list_exports.rb create mode 100644 db/post_migrate/20240206225046_index_organization_id_on_dependency_list_exports.rb create mode 100644 db/post_migrate/20240208235322_add_foreign_key_to_organization_id_on_dependency_list_exports.rb create mode 100644 db/schema_migrations/20240206224725 create mode 100644 db/schema_migrations/20240206225046 create mode 100644 db/schema_migrations/20240208235322 create mode 100644 ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb create mode 100644 ee/spec/services/dependencies/export_serializers/organization_dependencies_service_spec.rb diff --git a/db/migrate/20240206224725_add_organization_id_to_dependency_list_exports.rb b/db/migrate/20240206224725_add_organization_id_to_dependency_list_exports.rb new file mode 100644 index 0000000000000..153c03ad6d23c --- /dev/null +++ b/db/migrate/20240206224725_add_organization_id_to_dependency_list_exports.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddOrganizationIdToDependencyListExports < Gitlab::Database::Migration[2.2] + milestone '16.10' + + def change + add_column :dependency_list_exports, :organization_id, :bigint + end +end diff --git a/db/post_migrate/20240206225046_index_organization_id_on_dependency_list_exports.rb b/db/post_migrate/20240206225046_index_organization_id_on_dependency_list_exports.rb new file mode 100644 index 0000000000000..a5e37f1adacfd --- /dev/null +++ b/db/post_migrate/20240206225046_index_organization_id_on_dependency_list_exports.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class IndexOrganizationIdOnDependencyListExports < Gitlab::Database::Migration[2.2] + INDEX_NAME = 'index_dependency_list_exports_on_organization_id' + + disable_ddl_transaction! + milestone '16.10' + + def up + add_concurrent_index :dependency_list_exports, :organization_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :dependency_list_exports, INDEX_NAME + end +end diff --git a/db/post_migrate/20240208235322_add_foreign_key_to_organization_id_on_dependency_list_exports.rb b/db/post_migrate/20240208235322_add_foreign_key_to_organization_id_on_dependency_list_exports.rb new file mode 100644 index 0000000000000..c4a4d869f6b23 --- /dev/null +++ b/db/post_migrate/20240208235322_add_foreign_key_to_organization_id_on_dependency_list_exports.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToOrganizationIdOnDependencyListExports < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.10' + + def up + add_concurrent_foreign_key :dependency_list_exports, :organizations, + column: :organization_id, + on_delete: :cascade, + reverse_lock_order: true + end + + def down + remove_foreign_key_if_exists :dependency_list_exports, column: :organization_id + end +end diff --git a/db/schema_migrations/20240206224725 b/db/schema_migrations/20240206224725 new file mode 100644 index 0000000000000..edd8af605411a --- /dev/null +++ b/db/schema_migrations/20240206224725 @@ -0,0 +1 @@ +ba88b86b5331d02fa7cfa4416fc2de5dbd451dc1fc03f5c6550f981b91d7ed96 \ No newline at end of file diff --git a/db/schema_migrations/20240206225046 b/db/schema_migrations/20240206225046 new file mode 100644 index 0000000000000..23e6557599453 --- /dev/null +++ b/db/schema_migrations/20240206225046 @@ -0,0 +1 @@ +d1c427131b7cddcab2891069a03af3c615e6126f3d7a3a5a8e6b30b7f1875d5e \ No newline at end of file diff --git a/db/schema_migrations/20240208235322 b/db/schema_migrations/20240208235322 new file mode 100644 index 0000000000000..5cd023fda08b4 --- /dev/null +++ b/db/schema_migrations/20240208235322 @@ -0,0 +1 @@ +0e53c0f6004aad8fee7323e5652dbabc9823561dd73ed017c7bfa34678ff9527 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 241efc083256c..098dfae7223ed 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -7759,6 +7759,7 @@ CREATE TABLE dependency_list_exports ( group_id bigint, pipeline_id bigint, export_type smallint DEFAULT 0 NOT NULL, + organization_id bigint, CONSTRAINT check_fff6fc9b2f CHECK ((char_length(file) <= 255)) ); @@ -24684,6 +24685,8 @@ CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON CREATE INDEX index_dependency_list_exports_on_group_id ON dependency_list_exports USING btree (group_id); +CREATE INDEX index_dependency_list_exports_on_organization_id ON dependency_list_exports USING btree (organization_id); + CREATE INDEX index_dependency_list_exports_on_pipeline_id ON dependency_list_exports USING btree (pipeline_id); CREATE INDEX index_dependency_list_exports_on_project_id ON dependency_list_exports USING btree (project_id); @@ -30111,6 +30114,9 @@ ALTER TABLE ONLY sbom_occurrences ALTER TABLE ONLY issues ADD CONSTRAINT fk_c34dd2b036 FOREIGN KEY (tmp_epic_id) REFERENCES epics(id) ON DELETE CASCADE; +ALTER TABLE ONLY dependency_list_exports + ADD CONSTRAINT fk_c348f16f10 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ALTER TABLE ONLY user_group_callouts ADD CONSTRAINT fk_c366e12ec3 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/ee/app/models/dependencies/dependency_list_export.rb b/ee/app/models/dependencies/dependency_list_export.rb index a45a7763bd306..d976844fae922 100644 --- a/ee/app/models/dependencies/dependency_list_export.rb +++ b/ee/app/models/dependencies/dependency_list_export.rb @@ -5,6 +5,7 @@ class DependencyListExport < ApplicationRecord mount_file_store_uploader AttachmentUploader + belongs_to :organization, class_name: 'Organizations::Organization' belongs_to :project belongs_to :group belongs_to :pipeline, class_name: 'Ci::Pipeline' @@ -49,17 +50,19 @@ def retrieve_upload(_identifier, paths) end def exportable - pipeline || project || group + pipeline || project || group || organization end def exportable=(value) case value when Project - make_project_level_export(value) + self.project = value when Group - make_group_level_export(value) + self.group = value + when Organizations::Organization + self.organization = value when Ci::Pipeline - make_pipeline_level_export(value) + self.pipeline = value else raise "Can not assign #{value.class} as exportable" end @@ -67,26 +70,8 @@ def exportable=(value) private - def make_project_level_export(project) - self.project = project - self.group = nil - self.pipeline = nil - end - - def make_group_level_export(group) - self.project = nil - self.group = group - self.pipeline = nil - end - - def make_pipeline_level_export(pipeline) - self.project = nil - self.group = nil - self.pipeline = pipeline - end - def only_one_exportable - errors.add(:base, 'Only one exportable is required') unless [project, group, pipeline].one? + errors.add(:base, 'Only one exportable is required') unless [project, group, pipeline, organization].one? end end end diff --git a/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb b/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb new file mode 100644 index 0000000000000..4194623778c00 --- /dev/null +++ b/ee/app/services/dependencies/export_serializers/organization_dependencies_service.rb @@ -0,0 +1,55 @@ +# 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 + + 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 each_batch + Gitlab::Pagination::Keyset::Iterator + .new(scope: export.organization.sbom_occurrences) + .each_batch { |batch| yield batch } + 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_service.rb b/ee/app/services/dependencies/export_service.rb index dfd340f5a40f0..b9da33fe266a7 100644 --- a/ee/app/services/dependencies/export_service.rb +++ b/ee/app/services/dependencies/export_service.rb @@ -4,6 +4,7 @@ module Dependencies class ExportService SERIALIZER_SERVICES = { dependency_list: { + Organizations::Organization => ExportSerializers::OrganizationDependenciesService, Project => ExportSerializers::ProjectDependenciesService, Group => ExportSerializers::GroupDependenciesService }, @@ -36,7 +37,17 @@ def execute def create_export dependency_list_export.start! - create_export_file + 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 dependency_list_export.finish! rescue StandardError, Dependencies::ExportSerializers::Sbom::PipelineService::SchemaValidationError diff --git a/ee/app/views/explore/dependencies/index.html.haml b/ee/app/views/explore/dependencies/index.html.haml index 67a84e0577168..2ed4ae9bd1df5 100644 --- a/ee/app/views/explore/dependencies/index.html.haml +++ b/ee/app/views/explore/dependencies/index.html.haml @@ -6,7 +6,7 @@ endpoint: explore_dependencies_path(format: :json), licenses_endpoint: nil, locations_endpoint: nil, - export_endpoint: nil, + export_endpoint: expose_path(api_v4_organizations_dependency_list_exports_path(id: @organization.id)), vulnerabilities_endpoint: nil, documentation_path: help_page_path('user/application_security/dependency_list/index'), support_documentation_path: help_page_path('user/application_security/dependency_scanning/index', anchor: 'supported-languages-and-package-managers'), diff --git a/ee/lib/api/dependency_list_exports.rb b/ee/lib/api/dependency_list_exports.rb index b79a6067c36a2..dfbe6dc0bdb5a 100644 --- a/ee/lib/api/dependency_list_exports.rb +++ b/ee/lib/api/dependency_list_exports.rb @@ -37,6 +37,24 @@ class DependencyListExports < ::API::Base end end + resource :organizations do + params do + requires :id, types: [String, Integer], desc: 'The ID of the organization' + end + desc 'Generate a dependency list export on an organization-level' + post ':id/dependency_list_exports' do + not_found! unless Feature.enabled?(:explore_dependencies, current_user) + + organization = find_organization!(params[:id]) + authorize! :read_dependency, organization + + export = ::Dependencies::CreateExportService + .new(organization, current_user) + .execute + present export, with: EE::API::Entities::DependencyListExport + end + end + resource :pipelines do params do requires :id, types: [String, Integer], desc: 'The ID of the pipeline' diff --git a/ee/spec/models/dependencies/dependency_list_export_spec.rb b/ee/spec/models/dependencies/dependency_list_export_spec.rb index 8edaa81cd398e..26a180589ec04 100644 --- a/ee/spec/models/dependencies/dependency_list_export_spec.rb +++ b/ee/spec/models/dependencies/dependency_list_export_spec.rb @@ -27,6 +27,8 @@ end describe 'only one exportable can be set' do + using RSpec::Parameterized::TableSyntax + let(:expected_error) { { error: 'Only one exportable is required' } } subject { export.errors.details[:base] } @@ -35,52 +37,40 @@ export.validate end - context 'when project and group is set' do - let(:export) { build(:dependency_list_export, project: project, group: group) } - - it { is_expected.to include(expected_error) } - end - - context 'when project and pipeline is set' do - let(:export) { build(:dependency_list_export, project: project, pipeline: pipeline) } - - it { is_expected.to include(expected_error) } - end - - context 'when pipeline and group is set' do - let(:export) { build(:dependency_list_export, pipeline: pipeline, group: group) } - - it { is_expected.to include(expected_error) } - end - - context 'when project, group and pipeline is set' do - let(:export) { build(:dependency_list_export, project: project, group: group, pipeline: pipeline) } - - it { is_expected.to include(expected_error) } - end - - context 'when none is set' do - let(:export) { build(:dependency_list_export, project: nil, group: nil, pipeline: nil) } - - it { is_expected.to include(expected_error) } - end - - context 'when only project is set' do - let(:export) { build(:dependency_list_export, project: project, group: nil) } - - it { is_expected.not_to include(expected_error) } - end - - context 'when only group is set' do - let(:export) { build(:dependency_list_export, project: nil, group: group, pipeline: nil) } - - it { is_expected.not_to include(expected_error) } - end - - context 'when only pipeline is set' do - let(:export) { build(:dependency_list_export, project: nil, group: nil, pipeline: pipeline) } - - it { is_expected.not_to include(expected_error) } + where(:args, :valid) do + organization = build_stubbed(:organization) + group = build_stubbed(:group, organization: organization) + project = build_stubbed(:project, organization: organization, group: group) + pipeline = build_stubbed(:ci_pipeline, project: project) + + [ + [{ organization: organization, group: group, project: project, pipeline: pipeline }, false], + [{ organization: organization, group: group, project: project, pipeline: nil }, false], + [{ organization: organization, group: group, project: nil, pipeline: pipeline }, false], + [{ organization: organization, group: group, project: nil, pipeline: nil }, false], + [{ organization: organization, group: nil, project: project, pipeline: pipeline }, false], + [{ organization: organization, group: nil, project: project, pipeline: nil }, false], + [{ organization: organization, group: nil, project: nil, pipeline: pipeline }, false], + [{ organization: organization, group: nil, project: nil, pipeline: nil }, true], + [{ organization: nil, group: group, project: project, pipeline: pipeline }, false], + [{ organization: nil, group: group, project: project, pipeline: nil }, false], + [{ organization: nil, group: group, project: nil, pipeline: pipeline }, false], + [{ organization: nil, group: group, project: nil, pipeline: nil }, true], + [{ organization: nil, group: nil, project: project, pipeline: pipeline }, false], + [{ organization: nil, group: nil, project: project, pipeline: nil }, true], + [{ organization: nil, group: nil, project: nil, pipeline: pipeline }, true], + [{ organization: nil, group: nil, project: nil, pipeline: nil }, false] + ] + end + + with_them do + let(:export) { build(:dependency_list_export, args) } + + if params[:valid] + it { is_expected.not_to include(expected_error) } + else + it { is_expected.to include(expected_error) } + end end end end @@ -177,42 +167,18 @@ end describe '#exportable=' do - context 'when the given argument is a project' do - let(:export) { build(:dependency_list_export, group: group, pipeline: pipeline) } - - it 'assigns the project and unassigns the group' do - expect { export.exportable = project }.to change { export.project }.to(project) - .and change { export.group }.to(nil) - .and change { export.pipeline }.to(nil) - end - end - - context 'when the given argument is a group' do - let(:export) { build(:dependency_list_export, project: project, pipeline: pipeline) } - - it 'assigns the group and unassigns the project' do - expect { export.exportable = group }.to change { export.group }.to(group) - .and change { export.project }.to(nil) - .and change { export.pipeline }.to(nil) - end + let(:export) { build(:dependency_list_export) } + let(:organization) { build_stubbed(:organization) } + + it 'sets the correct association' do + expect { export.exportable = project }.to change { export.project }.to(project) + expect { export.exportable = group }.to change { export.group }.to(group) + expect { export.exportable = pipeline }.to change { export.pipeline }.to(pipeline) + expect { export.exportable = organization }.to change { export.organization }.to(organization) end - context 'when the given argument is a pipeline' do - let(:export) { build(:dependency_list_export, group: group, project: project) } - - it 'assigns the pipeline and unassigns the group' do - expect { export.exportable = pipeline }.to change { export.pipeline }.to(pipeline) - .and change { export.project }.to(nil) - .and change { export.group }.to(nil) - end - end - - context 'when the given argument is neither a project, group or pipeline' do - let(:export) { build(:dependency_list_export) } - - it 'raises an error' do - expect { export.exportable = nil }.to raise_error(RuntimeError) - end + it 'raises when exportable is an unknown type' do + expect { export.exportable = nil }.to raise_error(RuntimeError) end end end diff --git a/ee/spec/requests/api/dependency_list_exports_spec.rb b/ee/spec/requests/api/dependency_list_exports_spec.rb index a2798b62debe5..4a5b630ff4635 100644 --- a/ee/spec/requests/api/dependency_list_exports_spec.rb +++ b/ee/spec/requests/api/dependency_list_exports_spec.rb @@ -74,6 +74,43 @@ end end + describe 'POST /organizations/:id/dependency_list_exports' do + let_it_be(:organization) { create(:organization) } + + context 'when admin mode is enabled', :enable_admin_mode do + context 'when the user is an admin' do + let_it_be(:current_user) { create(:user, :admin) } + + before do + stub_licensed_features(dependency_scanning: true, security_dashboard: true) + end + + it 'generates an export' do + post api("/organizations/#{organization.id}/dependency_list_exports", current_user) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_present + expect(json_response).to have_key('id') + expect(json_response).to have_key('has_finished') + expect(json_response).to have_key('self') + expect(json_response).to have_key('download') + end + + context 'when the `explore_dependencies` feature flag is disabled' do + before do + stub_feature_flags(explore_dependencies: false) + end + + it 'does not generate an export' do + post api("/organizations/#{organization.id}/dependency_list_exports", current_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + end + describe 'POST /projects/:id/dependency_list_exports' do let(:request_path) { "/projects/#{project.id}/dependency_list_exports" } let(:resource) { project } 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 new file mode 100644 index 0000000000000..a1d93ec7d70d7 --- /dev/null +++ b/ee/spec/services/dependencies/export_serializers/organization_dependencies_service_spec.rb @@ -0,0 +1,64 @@ +# 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_with_reload(:project) { create(:project, organization: organization) } + let_it_be(:export) { create(:dependency_list_export, project: nil, organization: organization) } + + 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 + + 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 + + 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 994d5f9e962e9..d93818209b95e 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 @@ -20,7 +20,7 @@ end describe '#execute' do - let(:dependency_list_export) { create(:dependency_list_export, exportable: pipeline) } + let(:dependency_list_export) { create(:dependency_list_export, project: nil, exportable: pipeline) } let(:service_class) { described_class.new(dependency_list_export) } diff --git a/ee/spec/services/dependencies/export_service_spec.rb b/ee/spec/services/dependencies/export_service_spec.rb index 9fd853d985bd0..279aa2019dd14 100644 --- a/ee/spec/services/dependencies/export_service_spec.rb +++ b/ee/spec/services/dependencies/export_service_spec.rb @@ -83,8 +83,41 @@ end context 'when export type is dependency_list' do + let(:timestamp) { Time.current.utc.strftime('%FT%H%M') } let(:export_type) { :dependency_list } + context 'when the exportable is an organization' do + subject(:execute) { described_class.new(export).execute } + + 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(: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 { export.file.filename }.to(expected_filename) } + + it 'includes a header in the export file' do + header = '"Name","Version","Packager","Location"' + expect { execute }.to change { export.file.read }.to(include(header)) + 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 + end + context 'when the exportable is a project' do let_it_be(:project) { create(:project) } @@ -100,7 +133,7 @@ it_behaves_like 'export service', Dependencies::ExportSerializers::ProjectDependenciesService do let(:dependency_list_export) do - create(:dependency_list_export, exportable: project, status: status, export_type: export_type) + create(:dependency_list_export, project: nil, exportable: project, status: status, export_type: export_type) end end end @@ -120,7 +153,7 @@ it_behaves_like 'export service', Dependencies::ExportSerializers::GroupDependenciesService do let(:dependency_list_export) do - create(:dependency_list_export, exportable: group, status: status, export_type: export_type) + create(:dependency_list_export, project: nil, exportable: group, status: status, export_type: export_type) end end end @@ -148,7 +181,12 @@ it_behaves_like 'export service', Dependencies::ExportSerializers::Sbom::PipelineService do let(:dependency_list_export) do - create(:dependency_list_export, exportable: pipeline, status: status, export_type: export_type) + create(:dependency_list_export, { + project: nil, + exportable: pipeline, + status: status, + export_type: export_type + }) end end end -- GitLab