Add policy dismissals to group-level dependency list
What does this MR do and why?
Adds security policy dismissal information to the group-level dependency list JSON response and displays a "Policy violation" badge with a tooltip in the group dependency list UI.
Background
The project-level dependency list displays security policy dismissals (violation indicators) for dependencies that have been bypassed via security policies (#562185 (closed)). However, the group-level dependency list was missing this information.
At the group level, dependencies are aggregated by component+version across projects using Sbom::AggregationsFinder. The previous attempt (!218680 (closed)) used MIN(uuid) to get a single representative occurrence UUID per aggregated row, which missed dismissals for other projects' occurrences of the same component. For example, if Project A has lodash@4.17.21 with a dismissal and Project B has the same dependency without one, the group view would show Project B's occurrence and the dismissal would not be visible.
Solution
Instead of matching by individual occurrence UUID, this MR collects all occurrence UUIDs for each component_version_id within the group namespace, then queries PolicyDismissal against the complete set. This ensures that if Project A and Project B both have lodash@4.17.21, and Project A has a policy dismissal, it correctly appears in the aggregated group-level row.
The policy_dismissals field is only exposed when the user has read_security_resource permission (requires security_dashboard licensed feature).
On the frontend, a "Policy violation" warning badge appears in the vulnerabilities column for any dependency with dismissals. On hover, a tooltip shows which projects have the violation (e.g. "A security policy was bypassed for this dependency in the following projects: Project A, Project B").
Demo
How to set up and validate locally
1. Create test data
Run the following in Rails console (rails c). This creates two projects sharing the same dependency, where only one has a policy dismissal — validating the core fix.
user = User.first
org = Organizations::Organization.default_organization
# Create group
group = Group.new(name: 'test-dismissals-group', path: 'test-dismissals-group', organization: org)
group.save!
group.add_owner(user)
# Create two projects in the group
project_a = Project.create!(name: 'project-a', path: 'project-a', namespace: group, creator: user, organization: org)
project_b = Project.create!(name: 'project-b', path: 'project-b', namespace: group, creator: user, organization: org)
# Shared component + version (lodash@4.17.21)
component = Sbom::Component.find_or_create_by!(name: 'lodash', component_type: 0, organization_id: org.id)
version = Sbom::ComponentVersion.find_or_create_by!(component: component, version: '4.17.21', organization_id: org.id)
source = Sbom::Source.find_or_create_by!(source_type: :dependency_scanning, organization_id: org.id, source: { 'package_manager' => { 'name' => 'npm' }, 'input_file' => { 'path' => 'package-lock.json' } })
# Create occurrence in both projects
[project_a, project_b].each do |project|
pipeline = Ci::Pipeline.create!(project: project, ref: 'main', sha: SecureRandom.hex(20), source: :push)
Sbom::Occurrence.create!(
project: project, pipeline: pipeline, commit_sha: pipeline.sha,
component: component, component_version: version, source: source,
traversal_ids: project.namespace.traversal_ids,
uuid: Sbom::OccurrenceUUID.generate(project_id: project.id, component_id: component.id, component_version_id: version.id, source_id: source.id),
package_manager: 'npm', component_name: 'lodash',
licenses: [{ 'spdx_identifier' => 'MIT', 'name' => 'MIT License', 'url' => 'https://spdx.org/licenses/MIT.html' }]
)
end
# Create security policy + dismissal ONLY on Project A
policy_project = Project.create!(name: 'policy-project', path: 'policy-project', namespace: group, creator: user, organization: org)
config = Security::OrchestrationPolicyConfiguration.create!(
project: project_a,
security_policy_management_project: policy_project
)
policy = Security::Policy.create!(
security_orchestration_policy_configuration: config,
security_policy_management_project_id: policy_project.id,
name: 'test-license-policy',
checksum: Digest::SHA256.hexdigest('test'),
policy_index: 0,
type: Security::Policy.types[:approval_policy],
enabled: true,
content: { actions: [{ type: 'require_approval', approvals_required: 1, user_approvers: ['owner'] }] }
)
# Insert MR via SQL to avoid Gitaly callbacks on empty repos
mr_id = ActiveRecord::Base.connection.execute(
"INSERT INTO merge_requests (source_project_id, target_project_id, source_branch, target_branch, author_id, title, created_at, updated_at, state_id) VALUES (#{project_a.id}, #{project_a.id}, 'feature', 'main', #{user.id}, 'Test MR', NOW(), NOW(), 1) RETURNING id"
).first['id']
occ_a = Sbom::Occurrence.find_by(project: project_a, component_name: 'lodash')
Security::PolicyDismissal.create!(
project: project_a, merge_request_id: mr_id, security_policy: policy,
user: user, dismissal_types: [0], status: :preserved,
license_occurrence_uuids: [occ_a.uuid],
security_findings_uuids: []
)
puts "Group: #{group.full_path}"
puts "Project A: #{project_a.name} (id: #{project_a.id}) | Project B: #{project_b.name} (id: #{project_b.id})"
puts "Dismissal is ONLY on Project A"2. Verify the JSON response
Navigate to:
http://<your-gdk-host>/groups/test-dismissals-group/-/dependencies.json3. Confirm the aggregation fix works
Even though lodash@4.17.21 appears as a single aggregated row, the response should include the dismissal from Project A with project_name and project_path:
{
"dependencies": [
{
"name": "lodash",
"version": "4.17.21",
"occurrence_count": 2,
"project_count": 2,
"policy_dismissals": [
{
"id": 1,
"project_id": "<project_a_id>",
"project_name": "project-a",
"project_path": "test-dismissals-group/project-a",
"dismissal_types": ["Policy false positive"],
"user_name": "Administrator"
}
]
}
]
}This proves the fix — the old code would have shown "policy_dismissals": [] because it only checked one representative occurrence UUID which could have been Project B's.
4. Verify the UI
Navigate to the group dependency list page:
http://<your-gdk-host>/groups/test-dismissals-group/-/dependencies- Confirm a "Policy violation" warning badge appears in the vulnerabilities column for
lodash@4.17.21 - Hover over the badge — the tooltip should read: "A security policy was bypassed for this dependency in the following projects: project-a"
- Dependencies without dismissals should not show the badge
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related issues
Related to #581242 (closed)