DependencyVulnerabilitiesResolver returns incomplete vulnerabilities for group-level aggregated dependencies
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
Summary
DependencyVulnerabilitiesResolver uses object.id to batch-load vulnerabilities for each dependency. For group-level aggregated dependencies, object.id is MIN(id) from AggregationsFinder#execute, so only vulnerabilities linked to one representative occurrence are returned — not all vulnerabilities across the group for that component.
This means the vulnerabilities field on DependencyAggregationType may show an incomplete list of vulnerabilities at group level.
Affected surface
| Surface | Level | Vulnerabilities list exposed? | Uses MIN(id)? | Affected? |
|---|---|---|---|---|
GraphQL vulnerabilities field |
Project | Yes | No (real ID) | No |
GraphQL vulnerabilities field |
Group | Yes (DependencyVulnerabilitiesResolver) |
Yes (MIN(id)) | Yes |
REST API vulnerabilities array |
Project | Yes (include_vulnerabilities) |
No (real ID) | No |
REST API vulnerabilities array |
Group | No (not rendered) | N/A | No |
Grape API vulnerabilities array |
Project | Yes | No (real ID) | No |
Grape API vulnerabilities array |
Group | N/A (no group endpoint) | N/A | No |
Only the group-level GraphQL vulnerabilities field is affected. All other surfaces either use real occurrence IDs (project level) or don't expose the vulnerabilities list (group REST API).
Root cause
DependencyVulnerabilitiesResolver (line 16) batches by object.id:
BatchLoader::GraphQL.for(object.id).batch do |ids, loader|
occurrence_vulnerabilities = ::Sbom::OccurrencesVulnerability
.for_occurrence_ids(ids)
...
endFor aggregated dependencies, object.id is MIN(outer_occurrences.id)::bigint AS id from AggregationsFinder#execute. This is a single representative occurrence — not all occurrences of that component across the group.
Proposed fix
When the parent object is an aggregated dependency, the resolver should batch by component_version_id instead of id, querying across all occurrences in the group's namespace:
def resolve(**_args)
return [] unless object
if aggregated_dependency?
resolve_for_aggregation
else
resolve_for_occurrence
end
end
def resolve_for_aggregation
BatchLoader::GraphQL.for(object.component_version_id).batch do |cv_ids, loader|
# Load all distinct vulnerabilities across the group for these component versions
...
end
endPerformance considerations
Production data shows the vulnerability count per component at group level:
| p50 | p90 | p95 | p99 | Max |
|---|---|---|---|---|
| 3 | 21 | 42 | 195 | 399,399 |
The max (399K) is an extreme outlier (linux-libc-dev across 32 projects). The fix should include pagination or a cap to avoid loading excessive data.