diff --git a/db/docs/sbom_occurrences.yml b/db/docs/sbom_occurrences.yml
index 554c4856eeb5971b0b6772b7d3da817fb5c0c4bf..63461f440f4bef5d119ffee5536d66f28ab960a1 100644
--- a/db/docs/sbom_occurrences.yml
+++ b/db/docs/sbom_occurrences.yml
@@ -1,6 +1,7 @@
 ---
 table_name: sbom_occurrences
 classes:
+- Sbom::DependencyPath
 - Sbom::Occurrence
 feature_categories:
 - dependency_management
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c41e121ded096e767fba511469cddc566e2f5aa5..09eb99c8683eb26293f7019f426f4e0dfc52aed5 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -205,6 +205,8 @@ The following metrics are available:
 | `gitlab_rack_attack_throttle_period_seconds` | Gauge | 17.6 | Reports the duration over which requests for a client are counted before Rack Attack throttles them. | `event_name` |
 | `gitlab_application_rate_limiter_throttle_utilization_ratio` | Histogram | 17.6 | Utilization ratio of a throttle in GitLab Application Rate Limiter. | `throttle_key`, `peek`, `feature_category` |
 | `search_zoekt_task_processing_queue_size` | Gauge | 17.9 | Number of tasks waiting to be processed by Zoekt. | `node_name` |
+| `gitlab_dependency_path_cte_real_duration_seconds` | Histogram | 17.10 |  Duration in seconds spent resolving the ancestor dependency paths for a given component. | |
+| `dependency_path_cte_paths_found` | Counter | 17.10 |  Counts the number of ancestor dependency paths found for a given dependency. | `max_depth_reached`, `cyclic` |
 
 ## Metrics controlled by a feature flag
 
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 796c878d352d28c567952fc68aeeaf71d4ab8931..8543875fca40e4a13a2df6b7b110f661dead1fae 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -24220,6 +24220,29 @@ A software dependency used by a project.
 | <a id="dependencyversion"></a>`version` | [`String`](#string) | Version of the dependency. |
 | <a id="dependencyvulnerabilitycount"></a>`vulnerabilityCount` | [`Int!`](#int) | Number of vulnerabilities within the dependency. |
 
+### `DependencyPath`
+
+Ancestor path of a given dependency.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="dependencypathiscyclic"></a>`isCyclic` | [`Boolean!`](#boolean) | Indicates if the path is cyclic. |
+| <a id="dependencypathmaxdepthreached"></a>`maxDepthReached` | [`Boolean!`](#boolean) | Indicates if the path reached the maximum depth (20). |
+| <a id="dependencypathpath"></a>`path` | [`[DependencyPathPartial!]!`](#dependencypathpartial) | Name of the dependency. |
+
+### `DependencyPathPartial`
+
+Ancestor path partial of a given dependency.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="dependencypathpartialname"></a>`name` | [`String!`](#string) | Name of the dependency. |
+| <a id="dependencypathpartialversion"></a>`version` | [`String!`](#string) | Version of the dependency. |
+
 ### `DependencyProxyBlob`
 
 Dependency proxy blob.
@@ -34090,6 +34113,24 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. |
 | <a id="projectdependenciessourcetypes"></a>`sourceTypes` | [`[SbomSourceType!]`](#sbomsourcetype) | Filter dependencies by source type. |
 
+##### `Project.dependencyPaths`
+
+Ancestor dependency paths for a dependency used by the project. \
+          Returns `null` if `dependency_graph_graphql` feature flag is disabled.
+
+{{< details >}}
+**Introduced** in GitLab 17.10.
+**Status**: Experiment.
+{{< /details >}}
+
+Returns [`[DependencyPath!]`](#dependencypath).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectdependencypathscomponent"></a>`component` | [`SbomComponentID!`](#sbomcomponentid) | Dependency path for component. |
+
 ##### `Project.deployment`
 
 Details of the deployment of the project.
diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb
index f5086baa7eaddc6706d751967d0089694f4356c0..fbc26dab8d8a48d310a45517d4912c73fc95eece 100644
--- a/ee/app/graphql/ee/types/project_type.rb
+++ b/ee/app/graphql/ee/types/project_type.rb
@@ -372,6 +372,15 @@ module ProjectType
           description: 'Software dependencies used by the project.',
           resolver: ::Resolvers::Sbom::DependenciesResolver
 
+        field :dependency_paths,
+          [::Types::Sbom::DependencyPathType],
+          null: true,
+          authorize: :read_dependency,
+          description: 'Ancestor dependency paths for a dependency used by the project. \
+          Returns `null` if `dependency_graph_graphql` feature flag is disabled.',
+          resolver: ::Resolvers::Sbom::DependencyPathsResolver,
+          experiment: { milestone: '17.10' }
+
         field :components,
           [::Types::Sbom::ComponentType],
           null: true,
diff --git a/ee/app/graphql/resolvers/sbom/dependency_paths_resolver.rb b/ee/app/graphql/resolvers/sbom/dependency_paths_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..349ab62b057bdc974460df24405cd12e4ee5dc4c
--- /dev/null
+++ b/ee/app/graphql/resolvers/sbom/dependency_paths_resolver.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Sbom
+    class DependencyPathsResolver < BaseResolver
+      include Gitlab::Graphql::Authorize::AuthorizeResource
+
+      type [Types::Sbom::DependencyPathType], null: true
+
+      authorize :read_dependency
+      authorizes_object!
+
+      argument :component, Types::GlobalIDType[::Sbom::Component],
+        required: true,
+        description: 'Dependency path for component.'
+
+      alias_method :project, :object
+
+      def resolve(component:)
+        return if Feature.disabled?(:dependency_graph_graphql, project)
+
+        component_id = resolve_gid(component, ::Sbom::Component)
+        result = Gitlab::Metrics.measure(:dependency_path_cte) do
+          ::Sbom::DependencyPath.find(id: component_id, project_id: project.id)
+        end
+        record_metrics(result)
+        result
+      end
+
+      private
+
+      def resolve_gid(gid, gid_class)
+        Types::GlobalIDType[gid_class].coerce_isolated_input(gid).model_id
+      end
+
+      def record_metrics(result)
+        counter = Gitlab::Metrics.counter(
+          :dependency_path_cte_paths_found,
+          'Count of Dependency Paths found using the recursive CTE'
+        )
+
+        counter.increment(
+          { cyclic: false, max_depth_reached: false },
+          result.count { |r| !r.is_cyclic && !r.max_depth_reached }
+        )
+        counter.increment(
+          { cyclic: false, max_depth_reached: true },
+          result.count { |r| !r.is_cyclic && r.max_depth_reached }
+        )
+        counter.increment(
+          { cyclic: true, max_depth_reached: false },
+          result.count { |r| r.is_cyclic && !r.max_depth_reached }
+        )
+        counter.increment(
+          { cyclic: true, max_depth_reached: true },
+          result.count { |r| r.is_cyclic && r.max_depth_reached }
+        )
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/sbom/dependency_path_partial_type.rb b/ee/app/graphql/types/sbom/dependency_path_partial_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2aac8d2dddfc1b29095c1cb37f40975de9d7960e
--- /dev/null
+++ b/ee/app/graphql/types/sbom/dependency_path_partial_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+  module Sbom
+    class DependencyPathPartialType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- Authorization checks are implemented on the parent object.
+      graphql_name 'DependencyPathPartial'
+      description 'Ancestor path partial of a given dependency.'
+
+      field :name, GraphQL::Types::String,
+        null: false, description: 'Name of the dependency.'
+
+      field :version, GraphQL::Types::String,
+        null: false, description: 'Version of the dependency.'
+    end
+  end
+end
diff --git a/ee/app/graphql/types/sbom/dependency_path_type.rb b/ee/app/graphql/types/sbom/dependency_path_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75b0b74b393c75be3cb7a83812523da363afea7d
--- /dev/null
+++ b/ee/app/graphql/types/sbom/dependency_path_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+  module Sbom
+    class DependencyPathType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- Authorization checks are implemented on the parent object.
+      graphql_name 'DependencyPath'
+      description 'Ancestor path of a given dependency.'
+
+      field :path, [DependencyPathPartialType],
+        null: false, description: 'Name of the dependency.'
+
+      field :is_cyclic, GraphQL::Types::Boolean,
+        null: false, description: 'Indicates if the path is cyclic.'
+
+      field :max_depth_reached, GraphQL::Types::Boolean,
+        null: false,
+        description: "Indicates if the path reached the maximum depth (#{::Sbom::DependencyPath::MAX_DEPTH})."
+    end
+  end
+end
diff --git a/ee/app/models/sbom/dependency_path.rb b/ee/app/models/sbom/dependency_path.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f40b3ee4eb913038bc3f28b8e9011acc54d6ccf5
--- /dev/null
+++ b/ee/app/models/sbom/dependency_path.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module Sbom
+  class DependencyPath < ::Gitlab::Database::SecApplicationRecord
+    include IgnorableColumns
+
+    self.table_name = 'sbom_occurrences'
+    ignore_columns %w[created_at updated_at component_version_id pipeline_id source_id commit_sha
+      component_id uuid package_manager component_name input_file_path licenses highest_severity vulnerability_count
+      source_package_id archived traversal_ids ancestors reachability], remove_never: true
+
+    MAX_DEPTH = 20
+
+    attribute :id, :integer
+    attribute :dependency_name, :string
+    attribute :project_id, :integer
+    attribute :full_path, :string, array: true
+    attribute :version, :string, array: true
+    attribute :is_cyclic, :boolean
+    attribute :max_depth_reached, :boolean
+
+    def self.find(id:, project_id:)
+      query = <<-SQL
+        WITH RECURSIVE dependency_tree AS (
+          SELECT
+              so.component_id as id,
+              so.component_name as dependency_name,
+              so.project_id,
+              ARRAY [a->>'name', so.component_name] as full_path,
+              ARRAY [a->>'version', versions.version] as version,
+              ARRAY [concat_ws('@', a->>'name', a->>'version'), concat_ws('@', so.component_name, versions.version)] as combined_path,
+              false as is_cyclic,
+              false as max_depth_reached
+          FROM
+              sbom_occurrences so
+              inner join sbom_component_versions versions on versions.id = so.component_version_id
+              CROSS JOIN LATERAL jsonb_array_elements(so.ancestors) as a
+          where
+              so.component_id = :id
+              and so.project_id = :project_id
+          UNION
+          ALL
+          SELECT
+              dt.id,
+              dt.dependency_name,
+              dt.project_id,
+              ARRAY [a->>'name'] || dt.full_path,
+              ARRAY [a->>'version'] || dt.version,
+              ARRAY [concat_ws('@', a->>'name', a->>'version')] || dt.combined_path,
+              dt.combined_path && ARRAY[concat_ws('@', a->>'name', a->>'version')],
+              array_length(dt.full_path, 1) = :max_depth
+          FROM
+              dependency_tree dt
+              JOIN sbom_occurrences so ON so.component_name = dt.full_path [1]
+              join sbom_component_versions versions on versions.id = so.component_version_id and versions.version = dt.version [1]
+              CROSS JOIN LATERAL jsonb_array_elements(so.ancestors) as a
+          WHERE
+              array_length(dt.full_path, 1) <= :max_depth
+              and so.project_id = :project_id
+              and not dt.is_cyclic
+        )
+        SELECT
+            id,
+            dependency_name,
+            project_id,
+            full_path,
+            combined_path,
+            version,
+            is_cyclic,
+            max_depth_reached
+        FROM
+          dependency_tree
+        WHERE NOT EXISTS (  -- Remove partial paths
+            SELECT 1
+            FROM dependency_tree dt2
+            WHERE dependency_tree.combined_path <@ dt2.combined_path -- Current path is a sub-path of another path
+            AND dependency_tree.combined_path <> dt2.combined_path -- Don't remove yourself!
+            AND NOT dependency_tree.is_cyclic -- Keep cyclic paths
+        );
+      SQL
+
+      query_params = {
+        project_id: project_id,
+        id: id,
+        max_depth: MAX_DEPTH
+      }
+
+      sql = sanitize_sql_array([query, query_params])
+
+      DependencyPath.find_by_sql(sql)
+    end
+
+    def path
+      full_path.each_with_index.map do |path, index|
+        {
+          name: path,
+          version: version[index]
+        }
+      end
+    end
+  end
+end
diff --git a/ee/config/feature_flags/beta/dependency_graph_graphql.yml b/ee/config/feature_flags/beta/dependency_graph_graphql.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d33b5689a8a2963a13d94bcbc266cd0397aa529a
--- /dev/null
+++ b/ee/config/feature_flags/beta/dependency_graph_graphql.yml
@@ -0,0 +1,9 @@
+---
+name: dependency_graph_graphql
+feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/16815
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182289
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/521318
+milestone: '17.10'
+group: group::security infrastructure
+type: beta
+default_enabled: false
diff --git a/ee/spec/graphql/resolvers/sbom/dependency_paths_resolver_spec.rb b/ee/spec/graphql/resolvers/sbom/dependency_paths_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..afc642a1872f758f443823ef7f69031966e7fdaa
--- /dev/null
+++ b/ee/spec/graphql/resolvers/sbom/dependency_paths_resolver_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Sbom::DependencyPathsResolver, feature_category: :vulnerability_management do
+  include GraphqlHelpers
+
+  before do
+    stub_licensed_features(security_dashboard: true, dependency_scanning: true)
+  end
+
+  let_it_be(:user) { create(:user) }
+  let_it_be(:namespace) { create(:group, developers: user) }
+  let_it_be(:project) { create(:project, namespace: namespace) }
+
+  let_it_be(:component) { create(:sbom_component, name: "activestorage") }
+
+  subject(:get_paths) { sync(resolve_dependency_paths(args: args)) }
+
+  context 'when given a project' do
+    let(:project_or_namespace) { project }
+
+    context 'when feature flag is OFF' do
+      before do
+        stub_feature_flags(dependency_graph_graphql: false)
+      end
+
+      let(:args) do
+        {
+          component: component.to_gid
+        }
+      end
+
+      it { is_expected.to be_nil }
+
+      it 'does not record metrics' do
+        expect(Gitlab::Metrics).not_to receive(:measure)
+      end
+    end
+
+    context 'when feature flag is ON' do
+      before do
+        stub_feature_flags(dependency_graph_graphql: true)
+      end
+
+      let(:args) do
+        {
+          component: component.to_gid
+        }
+      end
+
+      let(:result) do
+        [Sbom::DependencyPath.new(
+          id: component.id,
+          project_id: project.id,
+          dependency_name: component.name,
+          full_path: %w[ancestor_1 ancestor_2 dependency],
+          version: ['0.0.1', '0.0.2', '0.0.3'],
+          is_cyclic: false,
+          max_depth_reached: false
+        )]
+      end
+
+      it 'returns data from DependencyPath.find' do
+        expect(::Sbom::DependencyPath).to receive(:find)
+          .with(id: component.id.to_s, project_id: project.id)
+          .and_return(result)
+        is_expected.to eq(result)
+      end
+
+      it 'records execution time' do
+        expect(::Sbom::DependencyPath).to receive(:find)
+          .with(id: component.id.to_s, project_id: project.id)
+          .and_return(result)
+        expect(Gitlab::Metrics).to receive(:measure)
+          .with(:dependency_path_cte)
+          .and_call_original
+
+        get_paths
+      end
+
+      it 'records metrics' do
+        expect(::Sbom::DependencyPath).to receive(:find)
+          .with(id: component.id.to_s, project_id: project.id)
+          .and_return(result)
+        counter_double = instance_double(Prometheus::Client::Counter)
+        expect(Gitlab::Metrics).to receive(:counter)
+          .with(:dependency_path_cte_paths_found, 'Count of Dependency Paths found using the recursive CTE')
+          .and_return(counter_double)
+
+        expect(counter_double).to receive(:increment)
+          .with({ cyclic: false, max_depth_reached: false }, 1)
+        expect(counter_double).to receive(:increment)
+          .with({ cyclic: false, max_depth_reached: true }, 0)
+        expect(counter_double).to receive(:increment)
+          .with({ cyclic: true, max_depth_reached: false }, 0)
+        expect(counter_double).to receive(:increment)
+          .with({ cyclic: true, max_depth_reached: true }, 0)
+
+        get_paths
+      end
+    end
+  end
+
+  private
+
+  def resolve_dependency_paths(args: {})
+    resolve(
+      described_class,
+      obj: project_or_namespace,
+      args: args,
+      ctx: { current_user: user }
+    )
+  end
+end
diff --git a/ee/spec/graphql/types/sbom/dependency_path_partial_type_spec.rb b/ee/spec/graphql/types/sbom/dependency_path_partial_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..830e8aed02e190d7c9dc7eabed3198fad45fc408
--- /dev/null
+++ b/ee/spec/graphql/types/sbom/dependency_path_partial_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Sbom::DependencyPathPartialType, feature_category: :dependency_management do
+  let(:fields) { %i[name version] }
+
+  it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/ee/spec/graphql/types/sbom/dependency_path_type_spec.rb b/ee/spec/graphql/types/sbom/dependency_path_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe539dc4a3d02663ffe4f1b36db77b0a3415f0f2
--- /dev/null
+++ b/ee/spec/graphql/types/sbom/dependency_path_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Sbom::DependencyPathType, feature_category: :dependency_management do
+  let(:fields) { %i[path isCyclic maxDepthReached] }
+
+  it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/ee/spec/models/sbom/dependency_path_spec.rb b/ee/spec/models/sbom/dependency_path_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7f708692a129afb738b10278182758794a7fb4d
--- /dev/null
+++ b/ee/spec/models/sbom/dependency_path_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sbom::DependencyPath, feature_category: :vulnerability_management do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:namespace) { create(:group, developers: user) }
+  let_it_be(:project) { create(:project, namespace: namespace) }
+
+  let_it_be(:component_1) { create(:sbom_component, name: "activestorage") }
+  let_it_be(:component_version_1) { create(:sbom_component_version, component: component_1, version: '1.2.3') }
+
+  let_it_be(:component_2) { create(:sbom_component, name: "activesupport") }
+  let_it_be(:component_version_2) { create(:sbom_component_version, component: component_2, version: '2.3.4') }
+
+  let_it_be(:component_3) { create(:sbom_component, name: "activejob") }
+  let_it_be(:component_version_3) { create(:sbom_component_version, component: component_3, version: '3.4.5') }
+
+  subject(:find_dependencies) { described_class.find(id: id, project_id: project.id) }
+
+  context 'when given a project' do
+    context 'without cycles or exceeding the max depth' do
+      let_it_be(:occurrence_1) do
+        create(:sbom_occurrence, component: component_1, project: project, component_version: component_version_1)
+      end
+
+      let_it_be(:occurrence_2) do
+        create(:sbom_occurrence,
+          component: component_2,
+          project: project,
+          component_version: component_version_2,
+          ancestors: [{ name: component_1.name, version: component_version_1.version }]
+        )
+      end
+
+      let_it_be(:occurrence_3) do
+        create(:sbom_occurrence,
+          component: component_3,
+          project: project,
+          component_version: component_version_3,
+          ancestors: [{ name: component_2.name, version: component_version_2.version }]
+        )
+      end
+
+      context 'when ancestors can be found' do
+        let(:id) do
+          component_3.id
+        end
+
+        context 'for a dependency with children' do
+          let(:id) do
+            component_2.id
+          end
+
+          it 'traverses until it finds no more ancestors, and skips children' do
+            is_expected.to eq([described_class.new(
+              id: component_2.id,
+              project_id: project.id,
+              dependency_name: component_2.name,
+              full_path: [component_1.name, component_2.name],
+              version: [component_version_1.version, component_version_2.version],
+              is_cyclic: false,
+              max_depth_reached: false
+            )])
+          end
+
+          it 'returns expected ancestors' do
+            expect(find_dependencies[0].path).to eq([
+              { name: component_1.name, version: component_version_1.version },
+              { name: component_2.name, version: component_version_2.version }
+            ])
+          end
+        end
+
+        context 'for a dependency with no children' do
+          let(:id) do
+            component_3.id
+          end
+
+          it 'traverses until it finds no more ancestors' do
+            is_expected.to eq([described_class.new(
+              id: component_3.id,
+              project_id: project.id,
+              dependency_name: component_3.name,
+              full_path: [component_1.name, component_2.name, component_3.name],
+              version: [component_version_1.version, component_version_2.version, component_version_3.version],
+              is_cyclic: false,
+              max_depth_reached: false
+            )])
+          end
+
+          it 'returns expected ancestors' do
+            expect(find_dependencies[0].path).to eq([
+              { name: component_1.name, version: component_version_1.version },
+              { name: component_2.name, version: component_version_2.version },
+              { name: component_3.name, version: component_version_3.version }
+            ])
+          end
+        end
+      end
+
+      context 'when ancestors cannot be found' do
+        let(:id) do
+          component_1.id
+        end
+
+        it 'returns an empty array' do
+          is_expected.to eq([])
+        end
+      end
+    end
+
+    context 'if there is a cycle' do
+      let_it_be(:occurrence_1) do
+        create(:sbom_occurrence,
+          component: component_1,
+          project: project,
+          component_version: component_version_1,
+          ancestors: [{ name: component_3.name, version: component_version_3.version }]
+        )
+      end
+
+      let_it_be(:occurrence_2) do
+        create(:sbom_occurrence,
+          component: component_2,
+          project: project,
+          component_version: component_version_2,
+          ancestors: [{ name: component_1.name, version: component_version_1.version }]
+        )
+      end
+
+      let_it_be(:occurrence_3) do
+        create(:sbom_occurrence,
+          component: component_3,
+          project: project,
+          component_version: component_version_3,
+          ancestors: [{ name: component_2.name, version: component_version_2.version }]
+        )
+      end
+
+      let(:id) do
+        component_3.id
+      end
+
+      it 'traverses until it finds the cycle and stops' do
+        is_expected.to eq([described_class.new(
+          id: component_3.id,
+          project_id: project.id,
+          dependency_name: component_3.name,
+          full_path: [component_3.name, component_1.name, component_2.name, component_3.name],
+          version: [component_version_3.version,
+            component_version_1.version, component_version_2.version, component_version_3.version],
+          is_cyclic: true,
+          max_depth_reached: false
+        )])
+      end
+
+      it 'returns expected ancestors' do
+        expect(find_dependencies[0].path).to eq([
+          { name: component_3.name, version: component_version_3.version },
+          { name: component_1.name, version: component_version_1.version },
+          { name: component_2.name, version: component_version_2.version },
+          { name: component_3.name, version: component_version_3.version }
+        ])
+      end
+    end
+
+    context 'if it exceeds the max depth' do
+      before do
+        stub_const("#{described_class}::MAX_DEPTH", 1)
+      end
+
+      let_it_be(:occurrence_1) do
+        create(:sbom_occurrence,
+          component: component_1,
+          project: project,
+          component_version: component_version_1,
+          ancestors: [{ name: component_3.name, version: component_version_3.version }]
+        )
+      end
+
+      let_it_be(:occurrence_2) do
+        create(:sbom_occurrence,
+          component: component_2,
+          project: project,
+          component_version: component_version_2,
+          ancestors: [{ name: component_1.name, version: component_version_1.version }]
+        )
+      end
+
+      let_it_be(:occurrence_3) do
+        create(:sbom_occurrence,
+          component: component_3,
+          project: project,
+          component_version: component_version_3,
+          ancestors: [{ name: component_2.name, version: component_version_2.version }]
+        )
+      end
+
+      let(:id) do
+        component_3.id
+      end
+
+      it 'traverses until it reaches max depth and stops' do
+        is_expected.to eq([described_class.new(
+          id: component_3.id,
+          project_id: project.id,
+          dependency_name: component_3.name,
+          full_path: [component_2.name, component_3.name],
+          version: [component_version_2.version, component_version_3.version],
+          is_cyclic: false,
+          max_depth_reached: true
+        )])
+      end
+
+      it 'returns expected ancestors' do
+        expect(find_dependencies[0].path).to eq([
+          { name: component_2.name, version: component_version_2.version },
+          { name: component_3.name, version: component_version_3.version }
+        ])
+      end
+    end
+  end
+end