diff --git a/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb b/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb index 71242abdddfb4268f849d15021917972d4b40b1e..a2109dabf9a2e3f860752971078d6f53f00eea8c 100644 --- a/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb +++ b/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb @@ -6,6 +6,7 @@ class FindingBuilder ERROR_SBOM_MISSING_PIPELINE = 'SBOM occurrence must have a pipeline.' ERROR_SBOM_MISSING_SOURCE_ARG = 'Missing SBOM source argument.' ERROR_PIPELINE_USER_NIL = 'Pipeline must have a corresponding user to use as vulnerability author.' + UNKNOWN_VENDOR = 'Unknown' Error = Class.new(StandardError) ArgumentError = Class.new(Error) @@ -71,6 +72,7 @@ def finding scan: nil, identifiers: identifiers, links: links, + cvss: cvss_vectors_with_vendor, original_data: original_data, metadata_version: metadata_version, details: details, @@ -159,6 +161,22 @@ def links advisory.urls.map { |url| ::Gitlab::Ci::Reports::Security::Link.new(name: nil, url: url) } end + def cvss_vectors_with_vendor + [advisory.cvss_v3, advisory.cvss_v2].compact.map do |cvss| + { + 'vendor' => vendor_from_identifiers, + 'vector' => cvss.vector + } + end + end + + def vendor_from_identifiers + identifier = identifiers.find { |identifier| identifier.vendor != UNKNOWN_VENDOR } + + identifier&.vendor || UNKNOWN_VENDOR + end + strong_memoize_attr :vendor_from_identifiers + def severity advisory.cvss_v3&.severity&.downcase || advisory.cvss_v2&.severity&.downcase end diff --git a/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb b/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb index 09d46a1fb1af123fe210260f55ffc6f95d12b957..577e70917698d3cd32363fac90c6895ea097b13e 100644 --- a/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb +++ b/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb @@ -3,26 +3,31 @@ require "spec_helper" RSpec.describe Gitlab::VulnerabilityScanning::FindingBuilder, feature_category: :software_composition_analysis do - let_it_be(:sbom_source) { build(:ci_reports_sbom_source) } - let_it_be(:scanner) { Gitlab::VulnerabilityScanning::SecurityScanner.fabricate } - let_it_be(:location) { build(:ci_reports_security_locations_sast) } + let(:sbom_source) { build(:ci_reports_sbom_source) } + let(:scanner) { Gitlab::VulnerabilityScanning::SecurityScanner.fabricate } + let(:location) { build(:ci_reports_security_locations_sast) } + let(:cvss_v3) { "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" } + let(:cvss_v2) { "AV:N/AC:L/Au:N/C:N/I:N/A:P" } + let(:identifiers) do + [ + build(:pm_identifier, type: "cve", name: "CVE-2018-1000538", + url: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000538", value: "CVE-2018-1000538") + ] + end - let_it_be(:advisory) do + let(:advisory) do build(:vs_advisory, title: "Allocation of File Descriptors or Handles Without Limits or Throttling", description: "Minio a Allocation of Memory Without Limits or Throttling vulnerability in write-to-RAM.", - cvss_v2: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - cvss_v3: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + cvss_v3: cvss_v3, + cvss_v2: cvss_v2, urls: ["https://nvd.nist.gov/vuln/detail/CVE-2018-1000538", "https://github.com/minio/minio/pull/5957"], - identifiers: [ - build(:pm_identifier, type: "cve", name: "CVE-2018-1000538", - url: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000538", value: "CVE-2018-1000538") - ], + identifiers: identifiers, solution: "Unfortunately, there is no solution available yet." ) end - let_it_be(:affected_component) { build(:vs_possibly_affected_component) } + let(:affected_component) { build(:vs_possibly_affected_component) } let(:user) { build(:user) } let(:pipeline) { build(:ci_pipeline, user: user) } @@ -128,6 +133,54 @@ end end + context 'when all arguments are valid' do + before do + allow(builder).to receive(:report_type).and_return("dependency_scanning") + allow(builder).to receive(:location_fingerprint).and_return("01234567890abcdef") + allow(builder).to receive(:location).and_return(location) + allow(builder).to receive(:original_data).and_return({}) + end + + it 'populates finding cvss data' do + expect(builder.finding.cvss).to eq([ + { + 'vendor' => 'NVD', + 'vector' => cvss_v3 + }, + { + 'vendor' => 'NVD', + 'vector' => cvss_v2 + } + ]) + end + + context 'when vectors are missing' do + let(:cvss_v3) { nil } + let(:cvss_v2) { nil } + + it 'does not include a JSON object for missing vectors' do + expect(builder.finding.cvss).to eq([]) + end + end + + context 'when identifiers are unknown' do + let(:identifiers) { [build(:pm_identifier, type: "???")] } + + it 'reports vendor as Unknown' do + expect(builder.finding.cvss).to eq([ + { + 'vendor' => 'Unknown', + 'vector' => cvss_v3 + }, + { + 'vendor' => 'Unknown', + 'vector' => cvss_v2 + } + ]) + end + end + end + context 'when invalid arguments' do before do allow(builder).to receive(:report_type).and_return("dependency_scanning") diff --git a/lib/gitlab/ci/reports/security/identifier.rb b/lib/gitlab/ci/reports/security/identifier.rb index 0ff6be6acc419c4a6328268d9a59bf9650af3d60..221dd8133e5a53d19e3d9ab9712035a28912d090 100644 --- a/lib/gitlab/ci/reports/security/identifier.rb +++ b/lib/gitlab/ci/reports/security/identifier.rb @@ -57,6 +57,28 @@ def wasc? external_type.to_s.casecmp?('wasc') end + def vendor + # https://gitlab.com/gitlab-org/security-products/analyzers/report/-/blob/902c7dcb5f3a0e551223167931ebf39588a0193a/identifier.go#L46 + case external_type.downcase + when 'cve' + 'NVD' + when 'elsa' + 'Oracle' + when 'ghsa' + 'GitHub' + when 'hackerone' + 'HackerOne' + when 'osvdb' + 'OSVDB' + when 'rhsa' + 'RedHat' + when 'usn' + 'Ubuntu' + else + 'Unknown' + end + end + private def generate_fingerprint diff --git a/spec/lib/gitlab/ci/reports/security/identifier_spec.rb b/spec/lib/gitlab/ci/reports/security/identifier_spec.rb index 123730b6ee631c27e4acc67999f2f9fb50bc564d..b34c415a9236e66a32db9e9f38d4b0c9a3d4d897 100644 --- a/spec/lib/gitlab/ci/reports/security/identifier_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/identifier_spec.rb @@ -122,4 +122,25 @@ end end end + + describe '#vendor' do + where(:external_type, :expected) do + 'cve' | 'NVD' + 'elsa' | 'Oracle' + 'ghsa' | 'GitHub' + 'hackerone' | 'HackerOne' + 'osvdb' | 'OSVDB' + 'rhsa' | 'RedHat' + 'usn' | 'Ubuntu' + '???' | 'Unknown' + end + + let(:identifier) { create(:ci_reports_security_identifier, external_type: external_type) } + + subject { identifier.vendor } + + with_them do + it { is_expected.to eq(expected) } + end + end end