diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7ac97f451de40644c318aefb3d5ab2ddb8e1f53e..0a12fac6491a623616dbf605cf02f2d41d29b403 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -63,6 +63,13 @@ class Build < Ci::Processable
     has_many :report_results, class_name: 'Ci::BuildReportResult', foreign_key: :build_id, inverse_of: :build
     has_one :namespace, through: :project
 
+    has_one :build_source,
+      ->(build) { in_partition(build) },
+      class_name: 'Ci::BuildSource',
+      foreign_key: :build_id,
+      inverse_of: :build,
+      partition_foreign_key: :partition_id
+
     # Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts
     # before we delete builds. By doing this, the relation should be empty and not fire any
     # DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`.
@@ -1131,6 +1138,11 @@ def time_in_queue_seconds
     end
     strong_memoize_attr :time_in_queue_seconds
 
+    def source
+      build_source&.source || pipeline.source
+    end
+    strong_memoize_attr :source
+
     protected
 
     def run_status_commit_hooks!
diff --git a/app/models/ci/build_source.rb b/app/models/ci/build_source.rb
index 8edc49b5116016e9f2adcb3effb8dc1feb2d6204..810b9114ee7e85f609ad088f4e2d76cf58c534fc 100644
--- a/app/models/ci/build_source.rb
+++ b/app/models/ci/build_source.rb
@@ -14,10 +14,9 @@ class BuildSource < Ci::ApplicationRecord
     query_constraints :build_id, :partition_id
     partitionable scope: :build, partitioned: true
 
-    # rubocop:disable Rails/InverseOf -- Will be added once association on build is added
     belongs_to :build, ->(build_name) { in_partition(build_name) },
-      class_name: 'Ci::Build', partition_foreign_key: :partition_id
-    # rubocop:enable Rails/InverseOf
+      class_name: 'Ci::Build', partition_foreign_key: :partition_id,
+      inverse_of: :build_source
 
     validates :build, presence: true
     validates :source, presence: true
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5ba4eb09e028a468f6c6d31e51d9faa817ef997f..f838b4cbd02d6fdeaf479a2a830fd3413479469a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -460,6 +460,7 @@ builds:
 - job_annotations
 - job_artifacts_annotations
 - project_mirror
+- build_source
 bridges:
 - user
 - pipeline
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 0fea8f7f6276f33ca15c17e89852b5b1394c997e..7e32c0c7a19fa706e1e9e5a07a0c295f8f1b2e91 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -6004,4 +6004,16 @@ def run_job_without_exception
       expect(ci_build.token).to match(/^glcbt-64_[\w-]{20}$/)
     end
   end
+
+  describe '#source' do
+    it 'defaults to the pipeline source name' do
+      expect(build.source).to eq(build.pipeline.source)
+    end
+
+    it 'returns the associated source name when present' do
+      create(:ci_build_source, build: build, source: 'scan_execution_policy')
+
+      expect(build.source).to eq('scan_execution_policy')
+    end
+  end
 end
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 225e08b65c6020dcfa81547ded75c7f1c9ce6814..a58f1bf2adf8fe7defcb2781c970ee79b04e15de 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -99,7 +99,8 @@
            pipeline_id report_results pending_state pages_deployments
            queuing_entry runtime_metadata trace_metadata
            dast_site_profile dast_scanner_profile stage_id dast_site_profiles_build
-           dast_scanner_profiles_build auto_canceled_by_partition_id execution_config_id execution_config].freeze
+           dast_scanner_profiles_build auto_canceled_by_partition_id execution_config_id execution_config
+           build_source].freeze
       end
 
       before_all do