Commit deb72ebb authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch '233810-add-arguments-to-vulnerabilities-severities-count-in-graphql' into 'master'

Add arguments to vulnerabilitySeveritiesCount in GraphQL API

See merge request gitlab-org/gitlab!41067
parents 0adc3cda 06349e6c
...@@ -6863,6 +6863,36 @@ type Group { ...@@ -6863,6 +6863,36 @@ type Group {
last: Int last: Int
): VulnerabilityScannerConnection ): VulnerabilityScannerConnection
"""
Counts for each vulnerability severity in the group and its subgroups
"""
vulnerabilitySeveritiesCount(
"""
Filter vulnerabilities by project
"""
projectId: [ID!]
"""
Filter vulnerabilities by report type
"""
reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
"""
Filter vulnerabilities by severity
"""
severity: [VulnerabilitySeverity!]
"""
Filter vulnerabilities by state
"""
state: [VulnerabilityState!]
): VulnerabilitySeveritiesCount
""" """
Web URL of the group Web URL of the group
""" """
...@@ -7030,6 +7060,36 @@ type InstanceSecurityDashboard { ...@@ -7030,6 +7060,36 @@ type InstanceSecurityDashboard {
""" """
last: Int last: Int
): VulnerabilityScannerConnection ): VulnerabilityScannerConnection
"""
Counts for each vulnerability severity from projects selected in Instance Security Dashboard
"""
vulnerabilitySeveritiesCount(
"""
Filter vulnerabilities by project
"""
projectId: [ID!]
"""
Filter vulnerabilities by report type
"""
reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
"""
Filter vulnerabilities by severity
"""
severity: [VulnerabilitySeverity!]
"""
Filter vulnerabilities by state
"""
state: [VulnerabilityState!]
): VulnerabilitySeveritiesCount
} }
""" """
...@@ -12428,9 +12488,34 @@ type Project { ...@@ -12428,9 +12488,34 @@ type Project {
): VulnerabilityScannerConnection ): VulnerabilityScannerConnection
""" """
Counts for each severity of vulnerability of the project Counts for each vulnerability severity in the project
""" """
vulnerabilitySeveritiesCount: VulnerabilitySeveritiesCount vulnerabilitySeveritiesCount(
"""
Filter vulnerabilities by project
"""
projectId: [ID!]
"""
Filter vulnerabilities by report type
"""
reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
"""
Filter vulnerabilities by severity
"""
severity: [VulnerabilitySeverity!]
"""
Filter vulnerabilities by state
"""
state: [VulnerabilityState!]
): VulnerabilitySeveritiesCount
""" """
Web URL of the project Web URL of the project
......
...@@ -1048,6 +1048,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -1048,6 +1048,7 @@ Autogenerated return type of EpicTreeReorder
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the namespace | | `visibility` | String | Visibility of the namespace |
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade | | `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the group and its subgroups |
| `webUrl` | String! | Web URL of the group | | `webUrl` | String! | Web URL of the group |
## GroupMember ## GroupMember
...@@ -1077,6 +1078,7 @@ Represents a Group Membership ...@@ -1077,6 +1078,7 @@ Represents a Group Membership
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade | | `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity from projects selected in Instance Security Dashboard |
## Issue ## Issue
...@@ -1770,7 +1772,7 @@ Autogenerated return type of PipelineRetry ...@@ -1770,7 +1772,7 @@ Autogenerated return type of PipelineRetry
| `tagList` | String | List of project topics (not Git tags) | | `tagList` | String | List of project topics (not Git tags) |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource | | `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the project | | `visibility` | String | Visibility of the project |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each severity of vulnerability of the project | | `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the project |
| `webUrl` | String | Web URL of the project | | `webUrl` | String | Web URL of the project |
| `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user | | `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user |
......
...@@ -42,6 +42,10 @@ module EE ...@@ -42,6 +42,10 @@ module EE
description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups', description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each vulnerability severity in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitySeveritiesCountResolver
field :vulnerabilities_count_by_day, field :vulnerabilities_count_by_day,
::Types::VulnerabilitiesCountByDayType.connection_type, ::Types::VulnerabilitiesCountByDayType.connection_type,
null: true, null: true,
......
...@@ -42,8 +42,8 @@ module EE ...@@ -42,8 +42,8 @@ module EE
resolver: ::Resolvers::Vulnerabilities::ScannersResolver resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true, field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each severity of vulnerability of the project', description: 'Counts for each vulnerability severity in the project',
resolve: -> (obj, *) { obj.vulnerability_statistic || Hash.new(0) } resolver: ::Resolvers::VulnerabilitySeveritiesCountResolver
field :requirement, ::Types::RequirementsManagement::RequirementType, null: true, field :requirement, ::Types::RequirementsManagement::RequirementType, null: true,
description: 'Find a single requirement. Available only when feature flag `requirements_management` is enabled.', description: 'Find a single requirement. Available only when feature flag `requirements_management` is enabled.',
......
# frozen_string_literal: true
module Resolvers
class VulnerabilitySeveritiesCountResolver < VulnerabilitiesBaseResolver
include Gitlab::Utils::StrongMemoize
type Types::VulnerabilitySeveritiesCountType, null: true
argument :project_id, [GraphQL::ID_TYPE],
required: false,
description: 'Filter vulnerabilities by project'
argument :report_type, [Types::VulnerabilityReportTypeEnum],
required: false,
description: 'Filter vulnerabilities by report type'
argument :severity, [Types::VulnerabilitySeverityEnum],
required: false,
description: 'Filter vulnerabilities by severity'
argument :state, [Types::VulnerabilityStateEnum],
required: false,
description: 'Filter vulnerabilities by state'
argument :scanner, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter vulnerabilities by scanner'
def resolve(**args)
return Vulnerability.none unless vulnerable
Hash.new(0)
.merge(vulnerabilities(args).grouped_by_severity.count)
end
private
def vulnerabilities(filters)
Security::VulnerabilitiesFinder.new(vulnerable, filters).execute
end
end
end
...@@ -18,6 +18,10 @@ module Types ...@@ -18,6 +18,10 @@ module Types
description: 'Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard', description: 'Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each vulnerability severity from projects selected in Instance Security Dashboard',
resolver: ::Resolvers::VulnerabilitySeveritiesCountResolver
field :vulnerability_grades, field :vulnerability_grades,
[Types::VulnerableProjectsByGradeType], [Types::VulnerableProjectsByGradeType],
null: false, null: false,
......
...@@ -71,6 +71,7 @@ class Vulnerability < ApplicationRecord ...@@ -71,6 +71,7 @@ class Vulnerability < ApplicationRecord
scope :with_severities, -> (severities) { where(severity: severities) } scope :with_severities, -> (severities) { where(severity: severities) }
scope :with_states, -> (states) { where(state: states) } scope :with_states, -> (states) { where(state: states) }
scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(Vulnerabilities::Scanner.with_external_id(scanners)) } scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(Vulnerabilities::Scanner.with_external_id(scanners)) }
scope :grouped_by_severity, -> { group(:severity) }
class << self class << self
def parent_class def parent_class
......
---
title: Add ability to filter vulnerabilitiesSeveritiesCount in GraphQL for Project,
Group and Instance Security Dashboard
merge_request: 41067
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::VulnerabilitySeveritiesCountResolver do
include GraphqlHelpers
describe '#resolve' do
subject { resolve(described_class, obj: vulnerable, args: filters, ctx: { current_user: current_user }) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let_it_be(:low_vulnerability) do
create(:vulnerability, :with_findings, :detected, :low, :dast, project: project)
end
let_it_be(:critical_vulnerability) do
create(:vulnerability, :with_findings, :detected, :critical, :sast, project: project)
end
let_it_be(:high_vulnerability) do
create(:vulnerability, :with_findings, :dismissed, :high, :container_scanning, project: project)
end
let(:current_user) { user }
let(:filters) { {} }
let(:vulnerable) { project }
context 'when given severities' do
let(:filters) { { severity: ['low'] } }
it 'only returns count for low severity vulnerability' do
is_expected.to eq('low' => 1)
end
end
context 'when given states' do
let(:filters) { { state: ['dismissed'] } }
it 'only returns count for high severity vulnerability' do
is_expected.to eq('high' => 1)
end
end
context 'when given scanner' do
let(:filters) { { scanner: [high_vulnerability.finding_scanner_external_id] } }
it 'only returns count for high severity vulnerability' do
is_expected.to eq('high' => 1)
end
end
context 'when given report types' do
let(:filters) { { report_type: %i[dast sast] } }
it 'only returns count for vulnerabilities of the given report types' do
is_expected.to eq('critical' => 1, 'low' => 1)
end
end
context 'when resolving vulnerabilities for a project' do
it "returns the project's vulnerabilities" do
is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1)
end
end
context 'when resolving vulnerabilities for an instance security dashboard' do
before do
project.add_developer(user)
end
let(:vulnerable) { nil }
context 'when there is a current user' do
it "returns vulnerabilities for all projects on the current user's instance security dashboard" do
is_expected.to eq('critical' => 1, 'high' => 1, 'low' => 1)
end
end
context 'and there is no current user' do
let(:current_user) { nil }
it 'returns no vulnerabilities' do
is_expected.to be_empty
end
end
end
end
end
...@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['InstanceSecurityDashboard'] do ...@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['InstanceSecurityDashboard'] do
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let(:fields) do let(:fields) do
%i[projects vulnerability_scanners vulnerability_grades] %i[projects vulnerability_scanners vulnerability_severities_count vulnerability_grades]
end end
before do before do
......
...@@ -16,7 +16,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -16,7 +16,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expected_fields = %w[ expected_fields = %w[
vulnerabilities sast_ci_configuration vulnerability_scanners requirement_states_count vulnerabilities sast_ci_configuration vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count
security_dashboard_path iterations security_dashboard_path iterations
] ]
......
...@@ -226,6 +226,21 @@ RSpec.describe Vulnerability do ...@@ -226,6 +226,21 @@ RSpec.describe Vulnerability do
it { is_expected.to match_array(expected_values) } it { is_expected.to match_array(expected_values) }
end end
describe '.grouped_by_severity' do
before do
create_list(:vulnerability, 6, :critical)
create_list(:vulnerability, 4, :high)
create_list(:vulnerability, 2, :medium)
create_list(:vulnerability, 5, :low)
create_list(:vulnerability, 1, :info)
create_list(:vulnerability, 3, :unknown)
end
subject { described_class.grouped_by_severity.count }
it { is_expected.to eq('critical' => 6, 'high' => 4, 'info' => 1, 'low' => 5, 'medium' => 2, 'unknown' => 3) }
end
describe '#finding' do describe '#finding' do
let_it_be(:project) { create(:project, :with_vulnerability) } let_it_be(:project) { create(:project, :with_vulnerability) }
let_it_be(:vulnerability) { project.vulnerabilities.first } let_it_be(:vulnerability) { project.vulnerabilities.first }
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do RSpec.describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:statistic) { create(:vulnerability_statistic, :grade_d, project: project) } let_it_be(:vulnerability) { create(:vulnerability, :high, project: project) }
let_it_be(:query) do let_it_be(:query) do
%( %(
...@@ -30,6 +30,6 @@ RSpec.describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do ...@@ -30,6 +30,6 @@ RSpec.describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do
it "returns counts for each severity of the project's detected or confirmed vulnerabilities" do it "returns counts for each severity of the project's detected or confirmed vulnerabilities" do
high_count = subject.dig('data', 'project', 'vulnerabilitySeveritiesCount', 'high') high_count = subject.dig('data', 'project', 'vulnerabilitySeveritiesCount', 'high')
expect(high_count).to be(statistic.high) expect(high_count).to eq(1)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment