Commit 924e26c9 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'create-vulnerability-summary-type' into 'master'

Create VulnerabilitiesSummaryType for GraphQL

See merge request gitlab-org/gitlab!26346
parents 712bf446 c0998dc4
...@@ -6433,6 +6433,12 @@ type Project { ...@@ -6433,6 +6433,12 @@ type Project {
state: [VulnerabilityState!] state: [VulnerabilityState!]
): VulnerabilityConnection ): VulnerabilityConnection
"""
Counts for each severity of vulnerability of the project. Available only when
feature flag `first_class_vulnerabilities` is enabled
"""
vulnerabilitySeveritiesCount: VulnerabilitySeveritiesCount
""" """
Web URL of the project Web URL of the project
""" """
...@@ -9110,6 +9116,41 @@ enum VulnerabilityReportType { ...@@ -9110,6 +9116,41 @@ enum VulnerabilityReportType {
SAST SAST
} }
"""
Represents vulnerability counts by severity
"""
type VulnerabilitySeveritiesCount {
"""
Number of vulnerabilities of CRITICAL severity of the project
"""
critical: Int
"""
Number of vulnerabilities of HIGH severity of the project
"""
high: Int
"""
Number of vulnerabilities of INFO severity of the project
"""
info: Int
"""
Number of vulnerabilities of LOW severity of the project
"""
low: Int
"""
Number of vulnerabilities of MEDIUM severity of the project
"""
medium: Int
"""
Number of vulnerabilities of UNKNOWN severity of the project
"""
unknown: Int
}
""" """
The severity of the vulnerability. The severity of the vulnerability.
""" """
......
...@@ -19171,6 +19171,20 @@ ...@@ -19171,6 +19171,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "vulnerabilitySeveritiesCount",
"description": "Counts for each severity of vulnerability of the project. Available only when feature flag `first_class_vulnerabilities` is enabled",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitySeveritiesCount",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "webUrl", "name": "webUrl",
"description": "Web URL of the project", "description": "Web URL of the project",
...@@ -27431,6 +27445,103 @@ ...@@ -27431,6 +27445,103 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "VulnerabilitySeveritiesCount",
"description": "Represents vulnerability counts by severity",
"fields": [
{
"name": "critical",
"description": "Number of vulnerabilities of CRITICAL severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "high",
"description": "Number of vulnerabilities of HIGH severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "info",
"description": "Number of vulnerabilities of INFO severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "low",
"description": "Number of vulnerabilities of LOW severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "medium",
"description": "Number of vulnerabilities of MEDIUM severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "unknown",
"description": "Number of vulnerabilities of UNKNOWN severity of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "ENUM", "kind": "ENUM",
"name": "VulnerabilitySeverity", "name": "VulnerabilitySeverity",
......
...@@ -925,6 +925,7 @@ Information about pagination in a connection. ...@@ -925,6 +925,7 @@ Information about pagination in a connection.
| `tagList` | String | List of project tags | | `tagList` | String | List of project 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. Available only when feature flag `first_class_vulnerabilities` is enabled |
| `webUrl` | String | Web URL of the project | | `webUrl` | String | Web URL of the project |
| `wikiEnabled` | Boolean | (deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead | | `wikiEnabled` | Boolean | (deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead |
...@@ -1442,3 +1443,16 @@ Represents a vulnerability. ...@@ -1442,3 +1443,16 @@ Represents a vulnerability.
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) | | `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `title` | String | Title of the vulnerability | | `title` | String | Title of the vulnerability |
| `vulnerabilityPath` | String | URL to the vulnerability's details page | | `vulnerabilityPath` | String | URL to the vulnerability's details page |
## VulnerabilitySeveritiesCount
Represents vulnerability counts by severity
| Name | Type | Description |
| --- | ---- | ---------- |
| `critical` | Int | Number of vulnerabilities of CRITICAL severity of the project |
| `high` | Int | Number of vulnerabilities of HIGH severity of the project |
| `info` | Int | Number of vulnerabilities of INFO severity of the project |
| `low` | Int | Number of vulnerabilities of LOW severity of the project |
| `medium` | Int | Number of vulnerabilities of MEDIUM severity of the project |
| `unknown` | Int | Number of vulnerabilities of UNKNOWN severity of the project |
...@@ -19,6 +19,13 @@ module EE ...@@ -19,6 +19,13 @@ module EE
resolver: Resolvers::VulnerabilitiesResolver, resolver: Resolvers::VulnerabilitiesResolver,
feature_flag: :first_class_vulnerabilities feature_flag: :first_class_vulnerabilities
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each severity of vulnerability of the project',
feature_flag: :first_class_vulnerabilities,
resolve: -> (obj, _args, ctx) do
Hash.new(0).merge(obj.vulnerabilities.counts_by_severity)
end
field :requirement, ::Types::RequirementType, null: true, field :requirement, ::Types::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.',
resolver: ::Resolvers::RequirementsResolver.single resolver: ::Resolvers::RequirementsResolver.single
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerabilitySeveritiesCountType < BaseObject
graphql_name 'VulnerabilitySeveritiesCount'
description 'Represents vulnerability counts by severity'
::Vulnerabilities::Occurrence::SEVERITY_LEVELS.keys.each do |severity|
field severity, GraphQL::INT_TYPE, null: true,
description: "Number of vulnerabilities of #{severity.upcase} severity of the project"
end
end
end
...@@ -59,6 +59,7 @@ class Vulnerability < ApplicationRecord ...@@ -59,6 +59,7 @@ class Vulnerability < ApplicationRecord
scope :with_report_types, -> (report_types) { where(report_type: report_types) } scope :with_report_types, -> (report_types) { where(report_type: report_types) }
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 :counts_by_severity, -> { group(:severity).count }
delegate :default_branch, to: :project, prefix: :project delegate :default_branch, to: :project, prefix: :project
......
...@@ -3,10 +3,18 @@ ...@@ -3,10 +3,18 @@
require 'spec_helper' require 'spec_helper'
describe GitlabSchema.types['Project'] do describe GitlabSchema.types['Project'] do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:vulnerability) { create(:vulnerability, project: project, severity: :high) }
before do
project.add_developer(user)
end
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expected_fields = %w[ expected_fields = %w[
service_desk_enabled service_desk_address vulnerabilities service_desk_enabled service_desk_address vulnerabilities
requirement_states_count requirement_states_count vulnerability_severities_count
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
...@@ -23,7 +31,6 @@ describe GitlabSchema.types['Project'] do ...@@ -23,7 +31,6 @@ describe GitlabSchema.types['Project'] do
%( %(
query { query {
project(fullPath:"#{project.full_path}") { project(fullPath:"#{project.full_path}") {
name
vulnerabilities { vulnerabilities {
nodes { nodes {
title title
...@@ -36,10 +43,6 @@ describe GitlabSchema.types['Project'] do ...@@ -36,10 +43,6 @@ describe GitlabSchema.types['Project'] do
) )
end end
before do
project.add_developer(user)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
context 'when first_class_vulnerabilities is disabled' do context 'when first_class_vulnerabilities is disabled' do
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilitySeveritiesCount'] do
let_it_be(:fields) do
::Vulnerabilities::Occurrence::SEVERITY_LEVELS.keys
end
it { expect(described_class).to have_graphql_fields(fields) }
end
...@@ -149,6 +149,24 @@ describe Vulnerability do ...@@ -149,6 +149,24 @@ describe Vulnerability do
end end
end end
describe '.counts_by_severity' do
before do
create_list(:vulnerability, 2, severity: :critical)
create_list(:vulnerability, 3, severity: :high)
create(:vulnerability, severity: :low)
end
subject { described_class.counts_by_severity }
it 'returns the count for each severity' do
is_expected.to eq({
'critical' => 2,
'high' => 3,
'low' => 1
})
end
end
describe '#finding' do describe '#finding' do
let_it_be(:project) { create(:project, :with_vulnerabilities) } let_it_be(:project) { create(:project, :with_vulnerabilities) }
let_it_be(:vulnerability) { project.vulnerabilities.first } let_it_be(:vulnerability) { project.vulnerabilities.first }
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Query.project(fullPath).vulnerabilitySeveritiesCount' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:vulnerability) { create(:vulnerability, project: project, severity: :high) }
let_it_be(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
vulnerabilitySeveritiesCount {
high
}
}
}
)
end
before do
project.add_developer(user)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
context 'when first_class_vulnerabilities is disabled' do
before do
stub_feature_flags(first_class_vulnerabilities: false)
end
it 'is null' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilitySeveritiesCount')
expect(vulnerabilities).to be_nil
end
end
context 'when first_class_vulnerabilities is enabled' do
before do
stub_feature_flags(first_class_vulnerabilities: true)
stub_licensed_features(security_dashboard: true)
end
it "returns counts for each severity of the project's vulnerabilities" do
high_count = subject.dig('data', 'project', 'vulnerabilitySeveritiesCount', 'high')
expect(high_count).to be(1)
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