From c32f32d49ed3f496b1580e44935e49e11227d921 Mon Sep 17 00:00:00 2001 From: Michael Becker <mbecker@gitlab.com> Date: Thu, 3 Oct 2024 03:37:43 +0000 Subject: [PATCH] Add a hasAiResolution filter to vulnerabilitySeveritiesCountResolver We need to be able to filter vulnerabilities on whether they have the "Resolve with Duo" button enabled. This button is enabled if a finding's `CWE` value is included in this [hard-coded list][0] of `CWE` values. In a previous MRs we exposed this filter in the `VulnerabilitiesResolver`[^1] This commit updates the `vulnerabilitySeveritiesCountResolver` to make use of the same filter. [^1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166312 --- Changelog: changed EE: true MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167433 epic: https://gitlab.com/groups/gitlab-org/-/epics/15036 Related to: https://gitlab.com/gitlab-org/gitlab/-/issues/481529 --- doc/api/graphql/reference/index.md | 3 + .../resolvers/vulnerabilities_resolver.rb | 4 +- ...vulnerability_severities_count_resolver.rb | 12 ++++ ...rability_severities_count_resolver_spec.rb | 59 +++++++++++++++++++ .../vulnerability_severities_count_spec.rb | 45 +++++++++++++- 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7ac27c20ae0b..2646bd61e62c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -24597,6 +24597,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | <a id="groupvulnerabilityseveritiescountcapped"></a>`capped` | [`Boolean`](#boolean) | Default value is false. When set to true, the count returned for each severity is capped at a maximum of 1001. | | <a id="groupvulnerabilityseveritiescountclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="groupvulnerabilityseveritiescountdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. | +| <a id="groupvulnerabilityseveritiescounthasairesolution"></a>`hasAiResolution` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.5. **Status**: Experiment. Filters vulnerabilities which can or can not be resolved by GitLab Duo Vulnerability Resolution. Requires the `vulnerability_report_vr_filter` feature flag to be enabled, otherwise the argument is ignored. | | <a id="groupvulnerabilityseveritiescounthasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have issues. | | <a id="groupvulnerabilityseveritiescounthasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have a merge request. | | <a id="groupvulnerabilityseveritiescounthasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have remediations. | @@ -25289,6 +25290,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | <a id="instancesecuritydashboardvulnerabilityseveritiescountcapped"></a>`capped` | [`Boolean`](#boolean) | Default value is false. When set to true, the count returned for each severity is capped at a maximum of 1001. | | <a id="instancesecuritydashboardvulnerabilityseveritiescountclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="instancesecuritydashboardvulnerabilityseveritiescountdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. | +| <a id="instancesecuritydashboardvulnerabilityseveritiescounthasairesolution"></a>`hasAiResolution` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.5. **Status**: Experiment. Filters vulnerabilities which can or can not be resolved by GitLab Duo Vulnerability Resolution. Requires the `vulnerability_report_vr_filter` feature flag to be enabled, otherwise the argument is ignored. | | <a id="instancesecuritydashboardvulnerabilityseveritiescounthasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have issues. | | <a id="instancesecuritydashboardvulnerabilityseveritiescounthasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have a merge request. | | <a id="instancesecuritydashboardvulnerabilityseveritiescounthasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have remediations. | @@ -31553,6 +31555,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | <a id="projectvulnerabilityseveritiescountcapped"></a>`capped` | [`Boolean`](#boolean) | Default value is false. When set to true, the count returned for each severity is capped at a maximum of 1001. | | <a id="projectvulnerabilityseveritiescountclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="projectvulnerabilityseveritiescountdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. | +| <a id="projectvulnerabilityseveritiescounthasairesolution"></a>`hasAiResolution` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.5. **Status**: Experiment. Filters vulnerabilities which can or can not be resolved by GitLab Duo Vulnerability Resolution. Requires the `vulnerability_report_vr_filter` feature flag to be enabled, otherwise the argument is ignored. | | <a id="projectvulnerabilityseveritiescounthasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have issues. | | <a id="projectvulnerabilityseveritiescounthasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have a merge request. | | <a id="projectvulnerabilityseveritiescounthasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Filter vulnerabilities that do or do not have remediations. | diff --git a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb index c1a734f7c988..71d3f0efa4e2 100644 --- a/ee/app/graphql/resolvers/vulnerabilities_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerabilities_resolver.rb @@ -115,12 +115,12 @@ def preloads def vulnerabilities(params) finder_params = params.merge(before_severity: before_severity, after_severity: after_severity) - finder_params.delete(:has_ai_resolution) unless resolve_by_duo_filtering_enabled? + finder_params.delete(:has_ai_resolution) unless resolve_with_duo_filtering_enabled? apply_lookahead(::Security::VulnerabilityReadsFinder.new(vulnerable, finder_params).execute.as_vulnerabilities) end - def resolve_by_duo_filtering_enabled? + def resolve_with_duo_filtering_enabled? return false if vulnerable.is_a?(::InstanceSecurityDashboard) Feature.enabled?(:vulnerability_report_vr_filter, vulnerable) diff --git a/ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb b/ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb index ec9e60fd7a72..65b63395b516 100644 --- a/ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb +++ b/ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb @@ -48,6 +48,11 @@ class VulnerabilitySeveritiesCountResolver < VulnerabilitiesBaseResolver required: false, description: 'Filter vulnerabilities that do or do not have a resolution.' + argument :has_ai_resolution, GraphQL::Types::Boolean, + required: false, + alpha: { milestone: '17.5' }, + description: 'Filters vulnerabilities which can or can not be resolved by GitLab Duo Vulnerability Resolution. Requires the `vulnerability_report_vr_filter` feature flag to be enabled, otherwise the argument is ignored.' + argument :image, [GraphQL::Types::String], required: false, description: "Filter vulnerabilities by location image. When this filter is present, "\ @@ -94,7 +99,14 @@ def resolve(**args) private def vulnerabilities(filters) + filters.delete(:has_ai_resolution) unless resolve_with_duo_filtering_enabled? ::Security::VulnerabilityReadsFinder.new(vulnerable, filters).execute end + + def resolve_with_duo_filtering_enabled? + return false if vulnerable.is_a?(::InstanceSecurityDashboard) + + Feature.enabled?(:vulnerability_report_vr_filter, vulnerable) + end end end diff --git a/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb b/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb index 9b112be94a8c..fad93eb308fa 100644 --- a/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb @@ -36,6 +36,7 @@ let_it_be(:critical_vulnerability) do create(:vulnerability, :with_findings, :detected, :critical, :sast, :with_merge_request_links, project: project) + .tap { |v| v.vulnerability_read.update!(has_vulnerability_resolution: true) } end let_it_be(:high_vulnerability) do @@ -200,6 +201,46 @@ end end + context 'when filtering vulnerabilities with AI resolution' do + context 'when has_ai_resolution is set to true' do + let(:filters) { { has_ai_resolution: true } } + + it 'only returns count for vulnerabilities with AI resolutions' do + is_expected.to eq('critical' => 1) + end + end + + context 'when has_ai_resolution is set to false' do + let(:filters) { { has_ai_resolution: false } } + + it 'only returns count for vulnerabilities without AI resolutions' do + is_expected.to eq('high' => 1, 'low' => 1, 'medium' => 1) + end + end + + context 'when vulnerability_report_vr_filter FF is disabled' do + before do + stub_feature_flags(vulnerability_report_vr_filter: false) + end + + context 'when has_ai_resolution is set to true' do + let(:filters) { { has_ai_resolution: true } } + + it 'only ignores filter and returns all count' do + is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1, 'medium' => 1) + end + end + + context 'when has_ai_resolution is set to false' do + let(:filters) { { has_ai_resolution: false } } + + it 'only ignores filter and returns all count' do + is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1, 'medium' => 1) + end + end + end + end + context 'when filtering vulnerabilities with remediations' do let(:filters) { { has_remediations: true } } let_it_be(:vulnerability_read_with_remediations) { create(:vulnerability_read, :with_remediations, project: project) } @@ -344,6 +385,24 @@ end end + context 'when filtering vulnerabilities with AI resolution' do + context 'when has_ai_resolution is set to true' do + let(:filters) { { has_ai_resolution: true } } + + it 'only ignores filter and returns all count' do + is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1, 'medium' => 1) + end + end + + context 'when has_ai_resolution is set to false' do + let(:filters) { { has_ai_resolution: false } } + + it 'only ignores filter and returns all count' do + is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1, 'medium' => 1) + end + end + end + it_behaves_like 'vulnerability filterable', :filters end diff --git a/ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb b/ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb index d2c1746a280e..40cc57a7fd5f 100644 --- a/ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb +++ b/ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb @@ -23,6 +23,7 @@ stub_licensed_features(security_dashboard: true) create_list(:vulnerability, 2, :high, :with_issue_links, :with_finding, resolved_on_default_branch: true, project: project) + .tap { |vulns| vulns.each { |v| v.vulnerability_read.update!(has_vulnerability_resolution: true) } } project.add_developer(user) end @@ -103,7 +104,7 @@ context 'when counting vulnerabilities without resolution' do let(:has_resolution) { false } - it 'counts vulnerabilities with resolution' do + it 'counts vulnerabilities without resolution' do expect(count_issues).to eq(1) end end @@ -117,6 +118,48 @@ end end + context 'with hasAiResolution filter' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + vulnerabilitySeveritiesCount(hasAiResolution: #{has_ai_resolution}) { + high + } + } + } + ) + end + + context 'when counting vulnerabilities without ai resolution' do + let(:has_ai_resolution) { false } + + it 'counts vulnerabilities without ai resolution' do + expect(count_issues).to eq(1) + end + end + + context 'when counting vulnerabilities with ai resolution' do + let(:has_ai_resolution) { true } + + it 'counts vulnerabilities with ai resolution' do + expect(count_issues).to eq(2) + end + end + + context 'when vulnerability_report_vr_filter FF is disabled' do + let(:has_ai_resolution) { true } + + before do + stub_feature_flags(vulnerability_report_vr_filter: false) + end + + it 'ignores the has_ai_resolution parameter' do + expect(count_issues).to eq(3) + end + end + end + def count_issues subject.dig('data', 'project', 'vulnerabilitySeveritiesCount', 'high') end -- GitLab