Commit c0998dc4 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Jan Provaznik

Add specs and permissions

In order to get permissions working I had to pivot from using the hash
returned by Vulnerability.counted_by_severity to using a
VulnerabilitiesSummary pseudo-model and corresponding policy class.

I'm not sure I love this solution, especially since I'm now using
`public_send` to avoid having to hardcode each severity field in
VulnerabilitiesSummaryType.
parent c2cb1834
......@@ -6433,6 +6433,12 @@ type Project {
state: [VulnerabilityState!]
): 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
"""
......@@ -9110,6 +9116,41 @@ enum VulnerabilityReportType {
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.
"""
......
......@@ -19171,6 +19171,20 @@
"isDeprecated": false,
"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",
"description": "Web URL of the project",
......@@ -27431,6 +27445,103 @@
],
"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",
"name": "VulnerabilitySeverity",
......
......@@ -925,6 +925,7 @@ Information about pagination in a connection.
| `tagList` | String | List of project 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. Available only when feature flag `first_class_vulnerabilities` is enabled |
| `webUrl` | String | Web URL of the project |
| `wikiEnabled` | Boolean | (deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead |
......@@ -1442,3 +1443,16 @@ Represents a vulnerability.
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `title` | String | Title of the vulnerability |
| `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
resolver: Resolvers::VulnerabilitiesResolver,
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,
description: 'Find a single requirement. Available only when feature flag `requirements_management` is enabled.',
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
scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) }
scope :with_states, -> (states) { where(state: states) }
scope :counts_by_severity, -> { group(:severity).count }
delegate :default_branch, to: :project, prefix: :project
......
......@@ -3,10 +3,18 @@
require 'spec_helper'
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
expected_fields = %w[
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)
......@@ -23,7 +31,6 @@ describe GitlabSchema.types['Project'] do
%(
query {
project(fullPath:"#{project.full_path}") {
name
vulnerabilities {
nodes {
title
......@@ -36,10 +43,6 @@ describe GitlabSchema.types['Project'] do
)
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
......
# 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
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
let_it_be(:project) { create(:project, :with_vulnerabilities) }
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