Commit 06349e6c authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Vitali Tatarintev

Add arguments to vulnerabilitySeveritiesCount in GraphQL API

This change adds arguments to filter values in
vulnerabilitySeveritiesCount. It also adds this field to
instanceSecurityDashboard and group.
parent d3e73baa
......@@ -6863,6 +6863,36 @@ type Group {
last: Int
): 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
"""
......@@ -7030,6 +7060,36 @@ type InstanceSecurityDashboard {
"""
last: Int
): 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 {
): 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
......
......@@ -18891,6 +18891,109 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitySeveritiesCount",
"description": "Counts for each vulnerability severity in the group and its subgroups",
"args": [
{
"name": "projectId",
"description": "Filter vulnerabilities by project",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "reportType",
"description": "Filter vulnerabilities by report type",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityReportType",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "severity",
"description": "Filter vulnerabilities by severity",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilitySeverity",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter vulnerabilities by state",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityState",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitySeveritiesCount",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL of the group",
......@@ -19404,6 +19507,109 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitySeveritiesCount",
"description": "Counts for each vulnerability severity from projects selected in Instance Security Dashboard",
"args": [
{
"name": "projectId",
"description": "Filter vulnerabilities by project",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "reportType",
"description": "Filter vulnerabilities by report type",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityReportType",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "severity",
"description": "Filter vulnerabilities by severity",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilitySeverity",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter vulnerabilities by state",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityState",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitySeveritiesCount",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -36407,9 +36613,98 @@
},
{
"name": "vulnerabilitySeveritiesCount",
"description": "Counts for each severity of vulnerability of the project",
"description": "Counts for each vulnerability severity in the project",
"args": [
{
"name": "projectId",
"description": "Filter vulnerabilities by project",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "reportType",
"description": "Filter vulnerabilities by report type",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityReportType",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "severity",
"description": "Filter vulnerabilities by severity",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilitySeverity",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter vulnerabilities by state",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityState",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
......@@ -1048,6 +1048,7 @@ Autogenerated return type of EpicTreeReorder
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the namespace |
| `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 |
## GroupMember
......@@ -1077,6 +1078,7 @@ Represents a Group Membership
| Name | Type | Description |
| --- | ---- | ---------- |
| `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
......@@ -1770,7 +1772,7 @@ Autogenerated return type of PipelineRetry
| `tagList` | String | List of project topics (not Git tags) |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
| `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 |
| `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user |
......
......@@ -42,6 +42,10 @@ module EE
description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups',
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,
::Types::VulnerabilitiesCountByDayType.connection_type,
null: true,
......
......@@ -42,8 +42,8 @@ module EE
resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each severity of vulnerability of the project',
resolve: -> (obj, *) { obj.vulnerability_statistic || Hash.new(0) }
description: 'Counts for each vulnerability severity in the project',
resolver: ::Resolvers::VulnerabilitySeveritiesCountResolver
field :requirement, ::Types::RequirementsManagement::RequirementType, null: true,
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
description: 'Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard',
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,
[Types::VulnerableProjectsByGradeType],
null: false,
......
......@@ -71,6 +71,7 @@ class Vulnerability < ApplicationRecord
scope :with_severities, -> (severities) { where(severity: severities) }
scope :with_states, -> (states) { where(state: states) }
scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(Vulnerabilities::Scanner.with_external_id(scanners)) }
scope :grouped_by_severity, -> { group(:severity) }
class << self
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
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let(:fields) do
%i[projects vulnerability_scanners vulnerability_grades]
%i[projects vulnerability_scanners vulnerability_severities_count vulnerability_grades]
end
before do
......
......@@ -16,7 +16,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do
expected_fields = %w[
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
]
......
......@@ -226,6 +226,21 @@ RSpec.describe Vulnerability do
it { is_expected.to match_array(expected_values) }
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
let_it_be(:project) { create(:project, :with_vulnerability) }
let_it_be(:vulnerability) { project.vulnerabilities.first }
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do
let_it_be(:project) { create(:project) }
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
%(
......@@ -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
high_count = subject.dig('data', 'project', 'vulnerabilitySeveritiesCount', 'high')
expect(high_count).to be(statistic.high)
expect(high_count).to eq(1)
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