Add malware field to Dependency GraphQL API and REST API
Summary
Part of gitlab-org/gitlab#587647 — Phase 1 backend requirement for the Malicious Package UI Representation and Filters epic.
Adds a nullable malware: Boolean field across all four dependency surfaces:
| Surface | Endpoint / Type | Level |
|---|---|---|
| GraphQL | DependencyType via DependencyInterface |
Project |
| GraphQL | DependencyAggregationType via DependencyInterface |
Group |
| Grape REST API | GET /api/v4/projects/:id/dependencies |
Project |
| Controller JSON | GET /projects/:id/-/dependencies.json |
Project |
| Controller JSON | GET /groups/:id/-/dependencies.json |
Group |
Note: This MR depends on the
MalwareDetectionconcern andVulnerability.malware_status_forintroduced in the companion MR for the Vulnerabilities GraphQL API (!228811 (merged)). That MR must be merged first. The minimal shared dependencies (concern, add-on enum, feature flag, factories) are ported onto this branch for local testing.
Approach
Detection logic
All surfaces delegate to Vulnerability.malware_status_for(vulnerabilities, vulnerable) from the MalwareDetection concern. The vulnerable parameter accepts either a Project or a Group — both respond to sscs_malware_detection_feature_flag_enabled? and root_ancestor, so the feature flag and add-on purchase checks work identically for either type.
Sbom::Occurrence#malware_status routes the call:
def malware_status
vulnerable = @malware_vulnerable || project
::Vulnerability.malware_status_for(vulnerabilities, vulnerable)
end- Project-level:
@malware_vulnerableis nil → falls through toprojectassociation - Group-level (aggregated):
@malware_vulnerableis set to the group by the resolver/controller → avoids accessingproject_id(which is absent fromAggregationsFinder's aggregated SELECT)
Field values
| Value | Meaning |
|---|---|
true |
Malware package detected (GLAM identifier present) |
false |
Not a malware package (SSCS add-on active, no GLAM identifier) |
null |
SSCS add-on not active — determination not possible |
Preloading strategy (no N+1 queries)
The preloading is split by level to handle the fact that AggregationsFinder#execute omits project_id and source_id from its SELECT (only aggregated columns are returned).
Base (DependencyInterfaceResolver) — safe default, no :project:
malware: [{ vulnerabilities: [:vulnerability_read] }]Project-level (DependenciesResolver) — adds :project with nested :group:
malware: [{ project: [:group] }, { vulnerabilities: [:vulnerability_read] }]The { project: [:group] } preload avoids a lazy load when sscs_malware_detection_feature_flag_enabled? traverses project.group.
Group-level (DependencyAggregationResolver) — inherits base, excludes unsafe preloads:
def preloads
super.except(:packager, :location) # source_id absent from aggregated SELECT
endWhen the malware field is selected, the resolver force-loads the relation and sets the group reference:
if node_selection.selects?(:malware)
occurrences.load
occurrences.each { |occ| occ.malware_vulnerable = group }
endREST API / Controller preloading:
| Endpoint | Preload |
|---|---|
API::Dependencies (Grape, project) |
.with_vulnerabilities_vulnerability_reads_and_project |
Projects::DependenciesController |
.with_vulnerabilities_vulnerability_reads_and_project |
Groups::DependenciesController |
.with_vulnerabilities_and_reads + prepare_malware_vulnerable! (sets group) |
Authorization fix
DependencyEntity#can_read_vulnerabilities? was checking request.try(:project) which is nil for group-level requests, causing the malware field to never be exposed at group level. Fixed to use subject (which falls back to request.group), consistent with how can_read_security_resource? and can_read_licenses? already work.
Files changed
| File | Change |
|---|---|
| MalwareDetection concern | |
ee/app/models/concerns/vulnerabilities/malware_detection.rb |
Accept Project or Group (vulnerable param), class-aware cache key |
| Occurrence model | |
ee/app/models/sbom/occurrence.rb |
Add malware_vulnerable attr_writer, malware_status uses @malware_vulnerable || project, add with_vulnerabilities_and_reads scope, update with_vulnerabilities_vulnerability_reads_and_project to preload { project: [:group] } |
| GraphQL | |
ee/app/graphql/types/sbom/dependency_interface.rb |
Add malware field (experiment, milestone 19.0) |
ee/app/graphql/resolvers/sbom/dependency_interface_resolver.rb |
Base malware preload without :project |
ee/app/graphql/resolvers/sbom/dependencies_resolver.rb |
Project-level: adds { project: [:group] } to malware preloads |
ee/app/graphql/resolvers/sbom/dependency_aggregation_resolver.rb |
Group-level: super.except(:packager, :location), sets malware_vulnerable = group |
| REST API (Grape) | |
ee/lib/api/dependencies.rb |
Apply with_vulnerabilities_vulnerability_reads_and_project scope |
ee/lib/api/entities/dependency.rb |
Add malware field gated on can_read_vulnerabilities? |
| Controllers | |
ee/app/controllers/projects/dependencies_controller.rb |
Chain with_vulnerabilities_vulnerability_reads_and_project |
ee/app/controllers/groups/dependencies_controller.rb |
Chain with_vulnerabilities_and_reads, add prepare_malware_vulnerable! |
| Serializer | |
ee/app/serializers/dependency_entity.rb |
Add malware field, fix can_read_vulnerabilities? to use subject |
| Shared dependencies (from !228811 (merged)) | |
ee/app/models/ee/vulnerability.rb |
Include MalwareDetection concern |
ee/app/models/gitlab_subscriptions/add_on.rb |
Add sscs_malware_detection enum |
app/models/project.rb, app/models/group.rb |
Add sscs_malware_detection_feature_flag_enabled? |
config/feature_flags/wip/sscs_malware_detection.yml |
Feature flag definition |
Related
- Companion MR (Vulnerabilities GraphQL API): !228811 (merged)
- Issue: #587647
- Parent epic: gitlab-org#18456