Add malware filter to vulnerability GraphQL APIs
Summary
Part of gitlab-org/gitlab#587647 — Phase 1 backend requirement for the Malicious Package UI Representation and Filters epic.
Adds a malware: Boolean filter argument to:
project.vulnerabilitiesGraphQL querygroup.vulnerabilitiesGraphQL queryvulnerabilitySeveritiesCountGraphQL query
The filter uses Elasticsearch to query the identifier_names field, matching vulnerabilities with GLAM-* prefix identifiers.
Depends on: !228811 (merged) (merged) — introduces the
MalwareDetectionconcern withsscs_addon_active_for?and theMALWARE_PACKAGE_IDENTIFIER_PREFIXconstant.
Filter Behavior
| Parameter Value | Expected Result |
|---|---|
true |
Returns only malware vulnerabilities (GLAM-* identifier present) |
false |
Returns only non-malware vulnerabilities |
null/omitted |
Returns all vulnerabilities (current behavior) |
Approach
Gating
The filter requires both conditions to be met:
-
SSCS add-on active — checked via
Vulnerability.sscs_addon_active_for?(vulnerable)from theMalwareDetectionconcern, which delegates tovulnerable.sscs_malware_detection_feature_flag_enabled?(WIP feature flag). Accepts both Project and Group. -
Filter feature flag enabled — at least one of:
malicious_vulnerability_filter_group(scoped to group) — checked when vulnerable is a Groupmalicious_vulnerability_filter_project(scoped to project) — checked when vulnerable is a Project
ES Query
VulnerabilityFilters.by_malware builds an ES prefix query against identifier_names.keyword using the downcased MALWARE_PACKAGE_IDENTIFIER_PREFIX constant (glam-) as the single source of truth for GLAM identifier detection.
malware: true→must: [prefix query]malware: false→must_not: [prefix query]
Integration with existing infrastructure
The filter is wired into the existing advanced vulnerability management ES pipeline:
VulnerabilitiesResolver/VulnerabilitySeveritiesCountResolver— newmalwareargumentVulnerabilityFilterable— added toADVANCED_FILTERS, validated byvalidate_malware!BaseFinder#initialize_search_params— passesmalwareparam to ESVulnerabilityQueryBuilder— includes:by_malwareinQUERY_COMPONENTSVulnerabilityFilters.by_malware— builds the ES query
Elasticsearch
ES query details
The by_malware filter generates an ES query against the identifier_names.keyword field. The identifier_names field is a denormalized array on the vulnerability ES document, indexed from vulnerability_reads.identifier_names at ingestion time. The glam- prefix is derived from Vulnerabilities::MalwareDetection::MALWARE_PACKAGE_IDENTIFIER_PREFIX.downcase.
When malware: true — return only malware vulnerabilities
A prefix query is added to the filter array, matching documents where any identifier_names entry starts with glam-:
{
"query": {
"bool": {
"filter": [
{ "prefix": { "traversal_ids": { "value": "95-" } } },
{
"bool": {
"_name": "filters:malware",
"must": [
{
"prefix": {
"identifier_names.keyword": {
"value": "glam-"
}
}
}
]
}
}
]
}
}
}When malware: false — return non-malware vulnerabilities
The GLAM prefix exclusion is added to the outer must_not of the query bool (not nested in a sub-bool inside filter). This ensures ES correctly returns all documents that don't match the GLAM prefix — including those where identifier_names is empty or absent:
{
"query": {
"bool": {
"filter": [
{ "prefix": { "traversal_ids": { "value": "95-" } } }
],
"must_not": [
{
"prefix": {
"identifier_names.keyword": {
"value": "glam-"
}
}
}
]
}
}
}Key design decision: For malware: false, the exclusion is placed in must_not at the outer bool level rather than as a nested bool inside filter. A bool with only must_not inside filter does not reliably match documents where the field is absent, leading to incorrect counts. The outer must_not approach ensures true + false = total for all severity levels.
No new ES index migrations are required — the identifier_names field is already indexed and available.
Feature Flags
Feature flag details
| Flag | Type | Scope | Purpose |
|---|---|---|---|
sscs_malware_detection |
WIP | group | Gates add-on availability (from !228811 (merged)) |
malicious_vulnerability_filter_group |
WIP | group | Gates filter rollout at group level |
malicious_vulnerability_filter_project |
WIP | project | Gates filter rollout at project level |
Local testing
Setup steps, queries, and verification
Prerequisites
- Elasticsearch must be running and configured in your GDK
- Advanced vulnerability management must be enabled
Setup
- Import the project https://gitlab.com/gitlab-org/govern/threat-insights-demos/verification-projects/bala-test-group/test-malicious-dependency-badge into your GDK. This project stubs the dependency scanner report with GLAM identifier vulnerabilities.
- After import, run a pipeline on the project to populate vulnerabilities and dependencies.
- All three feature flags (
sscs_malware_detection,malicious_vulnerability_filter_group,malicious_vulnerability_filter_project) are WIP type and enabled by default. If any were previously disabled, re-enable:# In rails console Feature.enable(:sscs_malware_detection) Feature.enable(:malicious_vulnerability_filter_group) Feature.enable(:malicious_vulnerability_filter_project) - Ensure vulnerabilities are indexed in Elasticsearch:
# In rails console project = Project.find_by_full_path('bala-test-group/test-malicious-dependency-badge') project.vulnerabilities.find_each do |v| ElasticAssociationIndexerWorker.perform_async(v.class.name, v.id, ['search_index_vulnerability_reads']) end - Wait for indexing to complete, then verify:
Vulnerability.__elasticsearch__.refresh_index!
Vulnerabilities GraphQL — project level
Query: malware: true
{
project(fullPath: "bala-test-group/test-malicious-dependency-badge") {
vulnerabilities(malware: true) {
nodes {
id
title
severity
malware
}
}
}
}Query: malware: false
{
project(fullPath: "bala-test-group/test-malicious-dependency-badge") {
vulnerabilities(malware: false) {
nodes {
id
title
severity
malware
}
}
}
}| Filter Value | Expected Result | Screenshot |
|---|---|---|
malware: true |
Only vulnerabilities with GLAM-* identifiers | |
malware: false |
Only vulnerabilities without GLAM-* identifiers | |
| omitted | All vulnerabilities |
Vulnerabilities GraphQL — group level
Query: malware: true
{
group(fullPath: "bala-test-group") {
vulnerabilities(malware: true) {
nodes {
id
title
severity
malware
}
}
}
}| Filter Value | Expected Result | Screenshot |
|---|---|---|
malware: true |
Only vulnerabilities with GLAM-* identifiers | |
malware: false |
Only vulnerabilities without GLAM-* identifiers |
Severity counts with malware filter — project level
Query
{
project(fullPath: "bala-test-group/test-malicious-dependency-badge") {
vulnerabilitySeveritiesCount(malware: true) {
critical
high
medium
low
}
}
}| Filter Value | Expected Result | Screenshot |
|---|---|---|
malware: true |
Severity counts for malware vulnerabilities only | |
malware: false |
Severity counts excluding malware vulnerabilities |
Severity counts with malware filter — group level
Query
{
group(fullPath: "bala-test-group") {
vulnerabilitySeveritiesCount(malware: true) {
critical
high
medium
low
}
}
}| Filter Value | Expected Result | Screenshot |
|---|---|---|
malware: true |
Severity counts for malware vulnerabilities only | |
malware: false |
Severity counts excluding malware vulnerabilities |
Feature flag gating verification
- Disable the filter feature flag:
group = Group.find_by_full_path('bala-test-group') Feature.disable(:malicious_vulnerability_filter_group, group) Feature.disable(:malicious_vulnerability_filter_project) - Run any query with
malware: true— expect error:"The malware filter is not available." - Re-enable:
Feature.enable(:malicious_vulnerability_filter_group, group) - Run the same query — expect success
Files changed
12 files changed
| File | Change |
|---|---|
ee/app/graphql/resolvers/vulnerabilities_resolver.rb |
Add malware argument |
ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb |
Add malware argument |
ee/app/graphql/resolvers/vulnerability_filterable.rb |
Add :malware to ADVANCED_FILTERS, add validate_malware! |
ee/app/models/concerns/vulnerabilities/malware_detection.rb |
Update sscs_addon_active_for? to accept Project or Group; move MALWARE_PACKAGE_IDENTIFIER_PREFIX constant here |
ee/app/models/ee/vulnerability.rb |
Update has_glam_identifier? to use fully qualified constant from concern |
ee/lib/search/advanced_finders/security/vulnerability/base_finder.rb |
Pass malware param to ES |
ee/lib/search/elastic/vulnerability_filters.rb |
Add by_malware filter method using concern constant |
ee/lib/search/elastic/vulnerability_query_builder.rb |
Add :by_malware to QUERY_COMPONENTS |
config/feature_flags/wip/malicious_vulnerability_filter_group.yml |
New — WIP FF |
config/feature_flags/wip/malicious_vulnerability_filter_project.yml |
New — WIP FF |
doc/api/graphql/reference/_index.md |
Regenerated GraphQL reference docs |
public/-/graphql/introspection_result.json |
Regenerated introspection schema |
Related
- Companion MR (malware field on VulnerabilityType): !228811 (merged) (merged)
- Companion MR (malware field on Dependencies): !231747
- Issue: #587647
- Parent epic: gitlab-org#18456