diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 63a531733ae54602626c0350b12fab42aebb4d75..7a5cc74479a4772a8a96fa57866f957c024fe900 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -17476,6 +17476,21 @@ Represents a product analytics dashboard visualization. | <a id="customizablepermissionrequirements"></a>`requirements` | [`[MemberRolePermission!]`](#memberrolepermission) | Requirements of the permission. | | <a id="customizablepermissionvalue"></a>`value` | [`MemberRolePermission!`](#memberrolepermission) | Value of the permission. | +### `CvssType` + +Represents a vulnerability's CVSS score. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cvsstypebasescore"></a>`baseScore` | [`Float!`](#float) | Base score of the CVSS. | +| <a id="cvsstypeoverallscore"></a>`overallScore` | [`Float!`](#float) | Overall score of the CVSS. | +| <a id="cvsstypeseverity"></a>`severity` | [`CvssSeverity!`](#cvssseverity) | Severity calculated from the overall score. | +| <a id="cvsstypevector"></a>`vector` | [`String!`](#string) | CVSS vector string. | +| <a id="cvsstypevendor"></a>`vendor` | [`String!`](#string) | Vendor who assigned the CVSS score. | +| <a id="cvsstypeversion"></a>`version` | [`Float!`](#float) | Version of the CVSS. | + ### `DastPreScanVerification` Represents a DAST Pre Scan Verification. @@ -28572,6 +28587,7 @@ Represents a vulnerability. | <a id="vulnerabilitycommenters"></a>`commenters` | [`UserCoreConnection!`](#usercoreconnection) | All commenters on this noteable. (see [Connections](#connections)) | | <a id="vulnerabilityconfirmedat"></a>`confirmedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to confirmed. | | <a id="vulnerabilityconfirmedby"></a>`confirmedBy` | [`UserCore`](#usercore) | User that confirmed the vulnerability. | +| <a id="vulnerabilitycvss"></a>`cvss` | [`[CvssType!]!`](#cvsstype) | CVSS information for the vulnerability. | | <a id="vulnerabilitydescription"></a>`description` | [`String`](#string) | Description of the vulnerability. | | <a id="vulnerabilitydescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. | | <a id="vulnerabilitydetails"></a>`details` | [`[VulnerabilityDetail!]!`](#vulnerabilitydetail) | Details of the vulnerability. | @@ -30505,6 +30521,18 @@ Categories for customizable dashboards. | ----- | ----------- | | <a id="customizabledashboardcategoryanalytics"></a>`ANALYTICS` | Analytics category for customizable dashboards. | +### `CvssSeverity` + +Values for a CVSS severity. + +| Value | Description | +| ----- | ----------- | +| <a id="cvssseveritycritical"></a>`CRITICAL` | Critical severity. | +| <a id="cvssseverityhigh"></a>`HIGH` | High severity. | +| <a id="cvssseveritylow"></a>`LOW` | Low severity. | +| <a id="cvssseveritymedium"></a>`MEDIUM` | Medium severity. | +| <a id="cvssseveritynone"></a>`NONE` | Not a vulnerability. | + ### `DastPreScanVerificationCheckType` Check type of the pre scan verification step. diff --git a/ee/app/graphql/types/vulnerabilities/cvss_severity_enum.rb b/ee/app/graphql/types/vulnerabilities/cvss_severity_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1dbb45d69bcbe8aff4562533538b7b8bf053537 --- /dev/null +++ b/ee/app/graphql/types/vulnerabilities/cvss_severity_enum.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Vulnerabilities + class CvssSeverityEnum < BaseEnum + graphql_name 'CvssSeverity' + description 'Values for a CVSS severity' + + # Strings from https://github.com/0llirocks/cvss-suite/blob/1cb50933744b7f5bac1fbab5e80bf9b214a24f3d/lib/cvss_suite/cvss.rb#L49 + value 'NONE', 'Not a vulnerability.', value: 'None' + %w[Low Medium High Critical].each do |severity| + value severity.upcase, description: "#{severity} severity.", value: severity + end + end + end +end diff --git a/ee/app/graphql/types/vulnerabilities/cvss_type.rb b/ee/app/graphql/types/vulnerabilities/cvss_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..1471f35d7865e56e00ca0001df369605dfc80a83 --- /dev/null +++ b/ee/app/graphql/types/vulnerabilities/cvss_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Vulnerabilities + # rubocop: disable Graphql/AuthorizeTypes -- object is a hash. + class CvssType < BaseObject + graphql_name 'CvssType' + description "Represents a vulnerability's CVSS score." + + include ::Gitlab::Utils::StrongMemoize + + field :vector, ::GraphQL::Types::String, + null: false, description: 'CVSS vector string.', hash_key: "vector" + + field :vendor, ::GraphQL::Types::String, + null: false, description: 'Vendor who assigned the CVSS score.', hash_key: "vendor" + + field :version, ::GraphQL::Types::Float, + null: false, description: 'Version of the CVSS.' + + field :base_score, ::GraphQL::Types::Float, + null: false, description: 'Base score of the CVSS.' + + field :overall_score, ::GraphQL::Types::Float, + null: false, description: 'Overall score of the CVSS.' + + field :severity, ::Types::Vulnerabilities::CvssSeverityEnum, + null: false, description: 'Severity calculated from the overall score.' + + delegate :base_score, :overall_score, :severity, :version, to: :cvss + + private + + def cvss + ::CvssSuite.new(object['vector']) + end + strong_memoize_attr :cvss + end + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/ee/app/graphql/types/vulnerability_type.rb b/ee/app/graphql/types/vulnerability_type.rb index 57b6f8774024d3d3c9cf5cfe5c308e1e9033da42..750198c979939f258a9f8e722c2605f5c2884e79 100644 --- a/ee/app/graphql/types/vulnerability_type.rb +++ b/ee/app/graphql/types/vulnerability_type.rb @@ -141,6 +141,9 @@ class VulnerabilityType < BaseObject field :present_on_default_branch, GraphQL::Types::Boolean, null: false, description: "Indicates whether the vulnerability is present on the default branch or not." + field :cvss, [::Types::Vulnerabilities::CvssType], + null: false, description: "CVSS information for the vulnerability." + def dismissal_reason object.vulnerability_read.dismissal_reason end diff --git a/ee/spec/graphql/types/vulnerabilities/cvss_type_spec.rb b/ee/spec/graphql/types/vulnerabilities/cvss_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e29acf73c1f74c11d038154059994ea5660c83b6 --- /dev/null +++ b/ee/spec/graphql/types/vulnerabilities/cvss_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CvssType'], feature_category: :vulnerability_management do + let(:expected_fields) { %i[vector vendor version base_score overall_score severity] } + + subject { described_class } + + it { is_expected.to have_graphql_fields(expected_fields) } +end diff --git a/ee/spec/graphql/types/vulnerability_type_spec.rb b/ee/spec/graphql/types/vulnerability_type_spec.rb index 973d56add23eaf852408f63f904ef87153035ff8..8f1d2d34a443e421f370fa7e22932f82cdca56cc 100644 --- a/ee/spec/graphql/types/vulnerability_type_spec.rb +++ b/ee/spec/graphql/types/vulnerability_type_spec.rb @@ -5,7 +5,7 @@ RSpec.describe GitlabSchema.types['Vulnerability'], feature_category: :vulnerability_management do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } - let_it_be(:vulnerability) { create(:vulnerability, :with_remediation, project: project) } + let_it_be_with_reload(:vulnerability) { create(:vulnerability, :with_remediation, project: project) } let(:vulnerabilities) { graphql_response.dig('data', 'project', 'vulnerabilities', 'nodes') } let(:query) do %( @@ -67,6 +67,7 @@ state_transitions dismissal_reason present_on_default_branch + cvss ] end @@ -281,4 +282,37 @@ end end end + + describe 'cvss' do + let(:query_field) { 'cvss { vendor vector version baseScore overallScore severity }' } + + subject(:cvss) { vulnerabilities.first['cvss'] } + + before do + vulnerability.update!(cvss: cvss_data) + end + + context 'when vulnerability has cvss data' do + let(:cvss_data) { [{ vendor: 'GitLab', vector: 'CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N' }] } + + it 'returns all attributes' do + data = cvss.first + + expect(data['vendor']).to eq('GitLab') + expect(data['vector']).to eq('CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N') + expect(data['version']).to eq(3.1) + expect(data['baseScore']).to eq(3.8) + expect(data['overallScore']).to eq(3.8) + expect(data['severity']).to eq('LOW') + end + end + + context 'when vulnerability does not have cvss data' do + let(:cvss_data) { [] } + + it 'returns an empty array' do + expect(cvss).to be_empty + end + end + end end