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 MalwareDetection concern and Vulnerability.malware_status_for introduced 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_vulnerable is nil → falls through to project association
  • Group-level (aggregated): @malware_vulnerable is set to the group by the resolver/controller → avoids accessing project_id (which is absent from AggregationsFinder'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
end

When 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 }
end

REST 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
Edited by Bala Kumar

Merge request reports

Loading