Commit 531de311 authored by Sean McGivern's avatar Sean McGivern

Merge branch...

Merge branch '339507-determine-if-manual-vulnerability-creation-mutation-should-respect-security-scanners-schema' into 'master'

Adjust Mutations::Vulnerabilities::Create to respect security scanner schema

See merge request gitlab-org/gitlab!69961
parents d96cf95f b7ea3dc2
...@@ -4715,13 +4715,13 @@ Input type: `VulnerabilityCreateInput` ...@@ -4715,13 +4715,13 @@ Input type: `VulnerabilityCreateInput`
| <a id="mutationvulnerabilitycreatedismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`). | | <a id="mutationvulnerabilitycreatedismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`). |
| <a id="mutationvulnerabilitycreateidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifierInput!]!`](#vulnerabilityidentifierinput) | Array of CVE or CWE identifiers for the vulnerability. | | <a id="mutationvulnerabilitycreateidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifierInput!]!`](#vulnerabilityidentifierinput) | Array of CVE or CWE identifiers for the vulnerability. |
| <a id="mutationvulnerabilitycreatemessage"></a>`message` | [`String`](#string) | Additional information about the vulnerability. | | <a id="mutationvulnerabilitycreatemessage"></a>`message` | [`String`](#string) | Additional information about the vulnerability. |
| <a id="mutationvulnerabilitycreatename"></a>`name` | [`String!`](#string) | Name of the vulnerability. |
| <a id="mutationvulnerabilitycreateproject"></a>`project` | [`ProjectID!`](#projectid) | ID of the project to attach the vulnerability to. | | <a id="mutationvulnerabilitycreateproject"></a>`project` | [`ProjectID!`](#projectid) | ID of the project to attach the vulnerability to. |
| <a id="mutationvulnerabilitycreateresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`). | | <a id="mutationvulnerabilitycreateresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`). |
| <a id="mutationvulnerabilitycreatescannername"></a>`scannerName` | [`String!`](#string) | Name of the security scanner used to discover the vulnerability. | | <a id="mutationvulnerabilitycreatescanner"></a>`scanner` | [`VulnerabilityScannerInput!`](#vulnerabilityscannerinput) | Information about the scanner used to discover the vulnerability. |
| <a id="mutationvulnerabilitycreateseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability (defaults to `unknown`). | | <a id="mutationvulnerabilitycreateseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability (defaults to `unknown`). |
| <a id="mutationvulnerabilitycreatesolution"></a>`solution` | [`String`](#string) | How to fix this vulnerability. | | <a id="mutationvulnerabilitycreatesolution"></a>`solution` | [`String`](#string) | How to fix this vulnerability. |
| <a id="mutationvulnerabilitycreatestate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). | | <a id="mutationvulnerabilitycreatestate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). |
| <a id="mutationvulnerabilitycreatetitle"></a>`title` | [`String!`](#string) | Title of the vulnerability. |
#### Fields #### Fields
...@@ -18313,3 +18313,23 @@ A time-frame defined as a closed inclusive range of two dates. ...@@ -18313,3 +18313,23 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="vulnerabilityidentifierinputexternaltype"></a>`externalType` | [`String`](#string) | External type of the vulnerability identifier. | | <a id="vulnerabilityidentifierinputexternaltype"></a>`externalType` | [`String`](#string) | External type of the vulnerability identifier. |
| <a id="vulnerabilityidentifierinputname"></a>`name` | [`String!`](#string) | Name of the vulnerability identifier. | | <a id="vulnerabilityidentifierinputname"></a>`name` | [`String!`](#string) | Name of the vulnerability identifier. |
| <a id="vulnerabilityidentifierinputurl"></a>`url` | [`String!`](#string) | URL of the vulnerability identifier. | | <a id="vulnerabilityidentifierinputurl"></a>`url` | [`String!`](#string) | URL of the vulnerability identifier. |
### `VulnerabilityScannerInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilityscannerinputid"></a>`id` | [`String!`](#string) | Unique ID that identifies the scanner. |
| <a id="vulnerabilityscannerinputname"></a>`name` | [`String!`](#string) | Human readable value that identifies the analyzer, not required to be unique. |
| <a id="vulnerabilityscannerinputurl"></a>`url` | [`String!`](#string) | Link to more information about the analyzer. |
| <a id="vulnerabilityscannerinputvendor"></a>`vendor` | [`VulnerabilityScannerVendorInput`](#vulnerabilityscannervendorinput) | Information about vendor/maintainer of the scanner. |
| <a id="vulnerabilityscannerinputversion"></a>`version` | [`String!`](#string) | Version of the scanner. |
### `VulnerabilityScannerVendorInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilityscannervendorinputname"></a>`name` | [`String!`](#string) | Name of the vendor/maintainer. |
...@@ -11,17 +11,17 @@ module Mutations ...@@ -11,17 +11,17 @@ module Mutations
required: true, required: true,
description: 'ID of the project to attach the vulnerability to.' description: 'ID of the project to attach the vulnerability to.'
argument :title, GraphQL::Types::String, argument :name, GraphQL::Types::String,
required: true, required: true,
description: 'Title of the vulnerability.' description: 'Name of the vulnerability.'
argument :description, GraphQL::Types::String, argument :description, GraphQL::Types::String,
required: true, required: true,
description: 'Description of the vulnerability.' description: 'Description of the vulnerability.'
argument :scanner_name, GraphQL::Types::String, argument :scanner, Types::VulnerabilityScannerInputType,
required: true, required: true,
description: 'Name of the security scanner used to discover the vulnerability.' description: 'Information about the scanner used to discover the vulnerability.'
argument :identifiers, [Types::VulnerabilityIdentifierInputType], argument :identifiers, [Types::VulnerabilityIdentifierInputType],
required: true, required: true,
...@@ -100,7 +100,7 @@ module Mutations ...@@ -100,7 +100,7 @@ module Mutations
def build_vulnerability_params(params) def build_vulnerability_params(params)
vulnerability_params = params.slice(*%i[ vulnerability_params = params.slice(*%i[
title name
state state
severity severity
confidence confidence
...@@ -111,15 +111,11 @@ module Mutations ...@@ -111,15 +111,11 @@ module Mutations
resolved_at resolved_at
dismissed_at dismissed_at
identifiers identifiers
scanner
]) ])
scanner_params = {
name: params.fetch(:scanner_name)
}
{ {
vulnerability: vulnerability_params vulnerability: vulnerability_params
.merge(scanner: scanner_params)
} }
end end
end end
......
# frozen_string_literal: true
module Types
class VulnerabilityScannerInputType < BaseInputObject
argument :id, GraphQL::Types::String,
description: 'Unique ID that identifies the scanner.',
required: true
argument :name, GraphQL::Types::String,
description: 'Human readable value that identifies the analyzer, not required to be unique.',
required: true
argument :url, GraphQL::Types::String,
description: 'Link to more information about the analyzer.',
required: true
argument :vendor, Types::VulnerabilityScannerVendorInputType,
description: 'Information about vendor/maintainer of the scanner.',
required: false
argument :version, GraphQL::Types::String,
description: 'Version of the scanner.',
required: true
end
end
# frozen_string_literal: true
module Types
class VulnerabilityScannerVendorInputType < BaseInputObject
argument :name, GraphQL::Types::String,
description: 'Name of the vendor/maintainer.',
required: true
end
end
...@@ -44,7 +44,12 @@ module Vulnerabilities ...@@ -44,7 +44,12 @@ module Vulnerabilities
.merge( .merge(
project: @project, project: @project,
author: @author, author: @author,
title: vulnerability_hash[:title]&.truncate(::Issuable::TITLE_LENGTH_MAX), # Our security report schema has name
# https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/src/security-report-format.json#L369
# Our database has title
# https://gitlab.com/gitlab-org/gitlab/blob/master/db/structure.sql#L20164
# We want the GraphQL mutation arguments to reflect the security scanner schema
title: vulnerability_hash[:name]&.truncate(::Issuable::TITLE_LENGTH_MAX),
report_type: report_type report_type: report_type
) )
...@@ -61,8 +66,8 @@ module Vulnerabilities ...@@ -61,8 +66,8 @@ module Vulnerabilities
def initialize_identifiers(identifier_hashes) def initialize_identifiers(identifier_hashes)
identifier_hashes.map do |identifier| identifier_hashes.map do |identifier|
name = identifier[:name] name = identifier[:name]
external_type = map_external_type_from_name(name) external_type = identifier[:external_type] || map_external_type_from_name(name)
external_id = name external_id = identifier[:external_id] || name
fingerprint = Digest::SHA1.hexdigest("#{external_type}:#{external_id}") fingerprint = Digest::SHA1.hexdigest("#{external_type}:#{external_id}")
url = identifier[:url] url = identifier[:url]
......
...@@ -66,11 +66,14 @@ module Vulnerabilities ...@@ -66,11 +66,14 @@ module Vulnerabilities
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def initialize_scanner(scanner_hash) def initialize_scanner(scanner_hash)
name = scanner_hash[:name] # In database Vulnerabilities::Scanner#id is autoincrementing primary key
# In the GraphQL mutation mutation arguments we want to respect the security scanner schema:
Vulnerabilities::Scanner.find_or_initialize_by(name: name) do |s| # https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/src/security-report-format.json#L339
# So the id provided to the mutation is actually external_id in our database
Vulnerabilities::Scanner.find_or_initialize_by(external_id: scanner_hash[:id]) do |s|
s.name = scanner_hash[:name]
s.vendor = scanner_hash.dig(:vendor, :name).to_s
s.project = @project s.project = @project
s.external_id = Gitlab::Utils.slugify(name)
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -15,33 +15,49 @@ RSpec.describe Mutations::Vulnerabilities::Create do ...@@ -15,33 +15,49 @@ RSpec.describe Mutations::Vulnerabilities::Create do
describe '#resolve' do describe '#resolve' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:identifier_attributes) do
{
name: "Test identifier",
url: "https://vulnerabilities.com/test"
}
end
let(:scanner_attributes) do
{
id: "my-custom-scanner",
name: "My Custom Scanner",
url: "https://superscanner.com",
vendor: vendor_attributes,
version: "21.37.00"
}
end
let(:vendor_attributes) do
{
name: "Custom Scanner Vendor"
}
end
let(:attributes) do
{
project: project_gid,
name: "Test vulnerability",
description: "Test vulnerability created via GraphQL",
scanner: scanner_attributes,
identifiers: [identifier_attributes],
state: "detected",
severity: "unknown",
confidence: "unknown",
solution: "rm -rf --no-preserve-root /",
message: "You can't fix this"
}
end
context 'when a vulnerability with the same identifier already exists' do context 'when a vulnerability with the same identifier already exists' do
subject { resolve(described_class, args: attributes, ctx: { current_user: user }) } subject { resolve(described_class, args: attributes, ctx: { current_user: user }) }
let(:project_gid) { GitlabSchema.id_from_object(project) } let(:project_gid) { GitlabSchema.id_from_object(project) }
let(:identifier_attributes) do
{
name: "Test identifier",
url: "https://vulnerabilities.com/test"
}
end
let(:attributes) do
{
project: project_gid,
title: "Test vulnerability",
description: "Test vulnerability created via GraphQL",
scanner_name: "My custom DAST scanner",
identifiers: [identifier_attributes],
state: "detected",
severity: "unknown",
confidence: "unknown",
solution: "rm -rf --no-preserve-root /",
message: "You can't fix this"
}
end
before do before do
project.add_developer(user) project.add_developer(user)
resolve(described_class, args: attributes, ctx: { current_user: user }) resolve(described_class, args: attributes, ctx: { current_user: user })
...@@ -61,28 +77,6 @@ RSpec.describe Mutations::Vulnerabilities::Create do ...@@ -61,28 +77,6 @@ RSpec.describe Mutations::Vulnerabilities::Create do
let(:project_gid) { GitlabSchema.id_from_object(project) } let(:project_gid) { GitlabSchema.id_from_object(project) }
let(:identifier_attributes) do
{
name: "Test identifier",
url: "https://vulnerabilities.com/test"
}
end
let(:attributes) do
{
project: project_gid,
title: "Test vulnerability",
description: "Test vulnerability created via GraphQL",
scanner_name: "My custom DAST scanner",
identifiers: [identifier_attributes],
state: "detected",
severity: "unknown",
confidence: "unknown",
solution: "rm -rf --no-preserve-root /",
message: "You can't fix this"
}
end
context 'when feature flag is disabled' do context 'when feature flag is disabled' do
before do before do
stub_feature_flags(create_vulnerabilities_via_api: false) stub_feature_flags(create_vulnerabilities_via_api: false)
...@@ -118,9 +112,9 @@ RSpec.describe Mutations::Vulnerabilities::Create do ...@@ -118,9 +112,9 @@ RSpec.describe Mutations::Vulnerabilities::Create do
let(:attributes) do let(:attributes) do
{ {
project: project_gid, project: project_gid,
title: "Test vulnerability", name: "Test vulnerability",
description: "Test vulnerability created via GraphQL", description: "Test vulnerability created via GraphQL",
scanner_name: "My custom DAST scanner", scanner: scanner_attributes,
identifiers: [identifier_attributes], identifiers: [identifier_attributes],
state: state, state: state,
severity: "unknown", severity: "unknown",
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::VulnerabilityScannerInputType do
specify { expect(described_class.graphql_name).to eq('VulnerabilityScannerInput') }
it 'has the correct arguments' do
expect(described_class.arguments.keys).to match_array(%w[id name url vendor version])
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::VulnerabilityScannerVendorInputType do
specify { expect(described_class.graphql_name).to eq('VulnerabilityScannerVendorInput') }
it 'has the correct arguments' do
expect(described_class.arguments.keys).to match_array(%w[name])
end
end
...@@ -23,13 +23,23 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -23,13 +23,23 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
stub_feature_flags(create_vulnerabilities_via_api: false) stub_feature_flags(create_vulnerabilities_via_api: false)
end end
let(:scanner_params) do let(:scanner_attributes) do
{ {
name: "My manual scanner" id: "my-custom-scanner",
name: "My Custom Scanner",
url: "https://superscanner.com",
vendor: vendor_attributes,
version: "21.37.00"
} }
end end
let(:identifier_params) do let(:vendor_attributes) do
{
name: "Custom Scanner Vendor"
}
end
let(:identifier_attributes) do
{ {
name: "Test identifier 1", name: "Test identifier 1",
url: "https://test.com" url: "https://test.com"
...@@ -39,12 +49,12 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -39,12 +49,12 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
let(:params) do let(:params) do
{ {
vulnerability: { vulnerability: {
title: "Test vulnerability", name: "Test vulnerability",
state: "detected", state: "detected",
severity: "unknown", severity: "unknown",
confidence: "unknown", confidence: "unknown",
identifiers: [identifier_params], identifiers: [identifier_attributes],
scanner: scanner_params scanner: scanner_attributes
} }
} }
end end
...@@ -62,34 +72,70 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -62,34 +72,70 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
end end
context 'with valid parameters' do context 'with valid parameters' do
let(:scanner_params) do let(:scanner_attributes) do
{
id: "my-custom-scanner",
name: "My Custom Scanner",
url: "https://superscanner.com",
vendor: vendor_attributes,
version: "21.37.00"
}
end
let(:vendor_attributes) do
{ {
name: "My manual scanner" name: "Custom Scanner Vendor"
} }
end end
let(:identifier_params) do let(:identifier_attributes) do
{ {
name: "Test identifier 1", name: "Test identifier 1",
url: "https://test.com" url: "https://test.com"
} }
end end
let(:identifier_fingerprint) do
Digest::SHA1.hexdigest("other:#{identifier_attributes[:name]}")
end
let(:params) do let(:params) do
{ {
vulnerability: { vulnerability: {
title: "Test vulnerability", name: "Test vulnerability",
state: "detected", state: "detected",
severity: "unknown", severity: "unknown",
confidence: "unknown", confidence: "unknown",
identifiers: [identifier_params], identifiers: [identifier_attributes],
scanner: scanner_params scanner: scanner_attributes
} }
} }
end end
let(:vulnerability) { subject.payload[:vulnerability] } let(:vulnerability) { subject.payload[:vulnerability] }
context 'with custom external_type and external_id' do
let(:identifier_attributes) do
{
name: "Test identifier 1",
url: "https://test.com",
external_id: "my external id",
external_type: "my external type"
}
end
let(:identifier_fingerprint) do
Digest::SHA1.hexdigest("#{identifier_attributes[:external_type]}:#{identifier_attributes[:external_id]}")
end
it 'uses them to create a Vulnerabilities::Identifier' do
primary_identifier = vulnerability.finding.primary_identifier
expect(primary_identifier.external_id).to eq(identifier_attributes.dig(:external_id))
expect(primary_identifier.external_type).to eq(identifier_attributes.dig(:external_type))
expect(primary_identifier.fingerprint).to eq(identifier_fingerprint)
end
end
it 'does not exceed query limit' do it 'does not exceed query limit' do
expect { subject }.not_to exceed_query_limit(20) expect { subject }.not_to exceed_query_limit(20)
end end
...@@ -112,7 +158,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -112,7 +158,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
end end
context 'when Scanner already exists' do context 'when Scanner already exists' do
let!(:scanner) { create(:vulnerabilities_scanner, name: scanner_params[:name]) } let!(:scanner) { create(:vulnerabilities_scanner, external_id: scanner_attributes[:id]) }
it 'does not create a new Scanner' do it 'does not create a new Scanner' do
expect { subject }.to change(Vulnerabilities::Scanner, :count).by(0) expect { subject }.to change(Vulnerabilities::Scanner, :count).by(0)
...@@ -120,7 +166,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -120,7 +166,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
end end
context 'when Identifier already exists' do context 'when Identifier already exists' do
let!(:identifier) { create(:vulnerabilities_identifier, name: identifier_params[:name]) } let!(:identifier) { create(:vulnerabilities_identifier, name: identifier_attributes[:name]) }
it 'does not create a new Identifier' do it 'does not create a new Identifier' do
expect { subject }.not_to change(Vulnerabilities::Identifier, :count) expect { subject }.not_to change(Vulnerabilities::Identifier, :count)
...@@ -128,7 +174,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -128,7 +174,7 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
end end
it 'creates all objects with correct attributes' do it 'creates all objects with correct attributes' do
expect(vulnerability.title).to eq(params.dig(:vulnerability, :title)) expect(vulnerability.title).to eq(params.dig(:vulnerability, :name))
expect(vulnerability.report_type).to eq("generic") expect(vulnerability.report_type).to eq("generic")
expect(vulnerability.state).to eq(params.dig(:vulnerability, :state)) expect(vulnerability.state).to eq(params.dig(:vulnerability, :state))
expect(vulnerability.severity).to eq(params.dig(:vulnerability, :severity)) expect(vulnerability.severity).to eq(params.dig(:vulnerability, :severity))
...@@ -146,19 +192,23 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -146,19 +192,23 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
primary_identifier = finding.primary_identifier primary_identifier = finding.primary_identifier
expect(primary_identifier.name).to eq(params.dig(:vulnerability, :identifiers, 0, :name)) expect(primary_identifier.name).to eq(params.dig(:vulnerability, :identifiers, 0, :name))
expect(primary_identifier.url).to eq(params.dig(:vulnerability, :identifiers, 0, :url))
expect(primary_identifier.external_id).to eq(params.dig(:vulnerability, :identifiers, 0, :name))
expect(primary_identifier.external_type).to eq("other")
expect(primary_identifier.fingerprint).to eq(identifier_fingerprint)
end end
context "when state fields match state" do context "when state fields match state" do
let(:params) do let(:params) do
{ {
vulnerability: { vulnerability: {
title: "Test vulnerability", name: "Test vulnerability",
state: "confirmed", state: "confirmed",
severity: "unknown", severity: "unknown",
confidence: "unknown", confidence: "unknown",
confirmed_at: Time.now.iso8601, confirmed_at: Time.now.iso8601,
identifiers: [identifier_params], identifiers: [identifier_attributes],
scanner: scanner_params scanner: scanner_attributes
} }
} }
end end
...@@ -176,13 +226,13 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do ...@@ -176,13 +226,13 @@ RSpec.describe Vulnerabilities::ManuallyCreateService do
let(:params) do let(:params) do
{ {
vulnerability: { vulnerability: {
title: "Test vulnerability", name: "Test vulnerability",
state: "detected", state: "detected",
severity: "unknown", severity: "unknown",
confidence: "unknown", confidence: "unknown",
confirmed_at: Time.now.iso8601, confirmed_at: Time.now.iso8601,
identifiers: [identifier_params], identifiers: [identifier_attributes],
scanner: scanner_params scanner: scanner_attributes
} }
} }
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