diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 5b69438637656c3cb1bac8fb2c10ae5cfb9cde9c..bb71338f412e1d9f3da320f9f296b4197c26f547 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -840,29 +840,24 @@ def code_owner_approval_required_available? end def sbom_occurrences + our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where( + project_id: all_projects_except_soft_deleted.select(:id) + )) + Sbom::Occurrence.includes(project: :route).select('sbom_occurrences.*, agg_occurrences.occurrence_count, agg_occurrences.project_count') - .from('sbom_occurrences') + .with(our_occurrences.to_arel) + .where(Sbom::Occurrence.arel_table[:id].in(our_occurrences.table.project(our_occurrences.table[:id]))) .joins( <<-SQL INNER JOIN ( SELECT component_id, COUNT(DISTINCT id) AS occurrence_count, COUNT(DISTINCT project_id) AS project_count - FROM sbom_occurrences - WHERE project_id IN ( - SELECT "projects"."id" FROM "projects" - WHERE "projects"."namespace_id" IN ( - SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id - FROM "namespaces" - WHERE "namespaces"."type" = 'Group' - AND (traversal_ids @> (\'{#{id}}\')) - ) - ) + FROM #{our_occurrences.table.name} GROUP BY component_id ) agg_occurrences ON sbom_occurrences.component_id = agg_occurrences.component_id SQL ) - .where(project_id: all_projects.select(:id)) .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") end diff --git a/ee/spec/factories/sbom/components.rb b/ee/spec/factories/sbom/components.rb index d1cf8fc456cb42831c59c818fc6b9690df3d7776..8ae125b514ba041acf16c82ffff1fc48b0422324 100644 --- a/ee/spec/factories/sbom/components.rb +++ b/ee/spec/factories/sbom/components.rb @@ -6,5 +6,20 @@ purl_type { :npm } sequence(:name) { |n| "component-#{n}" } + + trait :bundler do + name { "bundler" } + purl_type { :gem } + end + + trait :caddy do + name { "caddy" } + purl_type { :golang } + end + + trait :webpack do + name { "webpack" } + purl_type { :npm } + end end end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 517b68ff0a8628d711d85017ff7dc5732c200819..6c64ce7db8b2cf4cabd2b3023edadf968e2830c9 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -2878,6 +2878,53 @@ def webhook_headers end end end + + context 'with multiple sub groups' do + let_it_be(:group) { create(:group) } + let_it_be(:group_a) { create(:group, parent: group) } + let_it_be(:group_a_a) { create(:group, parent: group_a) } + + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:project_a) { create(:project, namespace: group_a) } + let_it_be(:project_a_a) { create(:project, namespace: group_a_a) } + + 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(:webpack) { create(:sbom_component, :webpack) } + let_it_be(:webpack_v4) { create(:sbom_component_version, component: webpack, version: "4.46.0") } + + let_it_be(:caddy) { create(:sbom_component, :caddy) } + let_it_be(:caddy_v1) { create(:sbom_component_version, component: caddy, version: "1.0.0") } + + let_it_be(:bundler_in_root_project) { create(:sbom_occurrence, project: project, component: bundler, component_version: bundler_v1) } + let_it_be(:webpack_in_project_a) { create(:sbom_occurrence, project: project_a, component: webpack, component_version: webpack_v4) } + let_it_be(:caddy_in_project_a_a) { create(:sbom_occurrence, project: project_a_a, component: caddy, component_version: caddy_v1) } + + it 'returns an occurrence for each version of each component' do + expect(subject).to match_array([ + bundler_in_root_project, + caddy_in_project_a_a, + webpack_in_project_a + ]) + end + + it 'returns the project count for each component' do + expect(subject.pluck(:component_name, :component_version_id, :project_count)).to match_array([ + [bundler.name, bundler_v1.id, 1], + [caddy.name, caddy_v1.id, 1], + [webpack.name, webpack_v4.id, 1] + ]) + end + + it 'returns the occurrence count for each component' do + expect(subject.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([ + [bundler.name, bundler_v1.id, 1], + [caddy.name, caddy_v1.id, 1], + [webpack.name, webpack_v4.id, 1] + ]) + end + end end describe '#reached_project_access_token_limit?' do diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb index c20a37e1e14f1a4f784ddc2b86320eadbd3156b1..5b0a8e2d196d669a07dfda308fb7bca37886b9bb 100644 --- a/ee/spec/requests/groups/dependencies_controller_spec.rb +++ b/ee/spec/requests/groups/dependencies_controller_spec.rb @@ -196,7 +196,7 @@ end expect(project_routes_count).to eq(1) - expect(project_count).to eq(1) + expect(project_count).to eq(2) end context 'with sorting params' do