diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e53bf6a48e4cb6d405740502f2ae38dfe873433a..1b5f60cb3821571072e179483ad0cafb2685cf0b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -24600,6 +24600,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. | @@ -25292,6 +25293,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. | @@ -31560,6 +31562,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 c1a734f7c988f2affff0f5f8fe814f5714b044bd..71d3f0efa4e2ae13a46f40e3157a90960d044e97 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 ec9e60fd7a72a73a3218235c6653a2a6efa26d4d..65b63395b516121878dec37be6f42f4a4ec5b2b1 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 9b112be94a8c6dd8328dc0f0c42d276adbeb5b12..fad93eb308fafc5fd49121bdc3ecf61cc9a70127 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 d2c1746a280e2844b3319cb4cc31a65c20ff3c7f..40cc57a7fd5f32e6f3737582aacd3ef5ee18a40e 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