Commit ecfbe63f authored by Max Woolf's avatar Max Woolf

Merge branch '10272-create-a-security-vulnerability-from-api' into 'master'

Manually create Vulnerabilities via API

See merge request gitlab-org/gitlab!68158
parents 2e83767c 51205f55
---
name: create_vulnerabilities_via_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68158
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
milestone: '14.3'
type: development
group: group::threat insights
default_enabled: false
This diff is collapsed.
......@@ -33,6 +33,7 @@ module EE
mount_mutation ::Mutations::RequirementsManagement::CreateRequirement
mount_mutation ::Mutations::RequirementsManagement::ExportRequirements
mount_mutation ::Mutations::RequirementsManagement::UpdateRequirement
mount_mutation ::Mutations::Vulnerabilities::Create
mount_mutation ::Mutations::Vulnerabilities::Dismiss
mount_mutation ::Mutations::Vulnerabilities::Resolve
mount_mutation ::Mutations::Vulnerabilities::Confirm
......
# frozen_string_literal: true
module Mutations
module Vulnerabilities
class Create < BaseMutation
graphql_name 'VulnerabilityCreate'
authorize :admin_vulnerability
argument :project, ::Types::GlobalIDType[::Project],
required: true,
description: 'ID of the project to attach the vulnerability to.'
argument :title, GraphQL::Types::String,
required: true,
description: 'Title of the vulnerability.'
argument :description, GraphQL::Types::String,
required: true,
description: 'Description of the vulnerability.'
argument :scanner_type, Types::SecurityScannerTypeEnum,
required: true,
description: 'Type of the security scanner used to discover the vulnerability.'
argument :scanner_name, GraphQL::Types::String,
required: true,
description: 'Name of the security scanner used to discover the vulnerability.'
argument :identifiers, [Types::VulnerabilityIdentifierInputType],
required: true,
description: 'Array of CVE or CWE identifiers for the vulnerability.'
argument :state, Types::VulnerabilityStateEnum,
required: false,
description: 'State of the vulnerability (defaults to `detected`).',
default_value: 'detected'
argument :severity, Types::VulnerabilitySeverityEnum,
required: false,
description: 'Severity of the vulnerability (defaults to `unknown`).',
default_value: 'unknown'
argument :confidence, Types::VulnerabilityConfidenceEnum,
required: false,
description: 'Confidence of the vulnerability (defaults to `unknown`).',
default_value: 'unknown'
argument :solution, GraphQL::Types::String,
required: false,
description: 'How to fix this vulnerability.'
argument :message, GraphQL::Types::String,
required: false,
description: 'Additional information about the vulnerability.'
argument :detected_at, Types::TimeType,
required: false,
description: 'Timestamp of when the vulnerability was first detected (defaults to creation time).'
argument :confirmed_at, Types::TimeType,
required: false,
description: 'Timestamp of when the vulnerability state changed to confirmed (defaults to creation time if status is `confirmed`).'
argument :resolved_at, Types::TimeType,
required: false,
description: 'Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`).'
argument :dismissed_at, Types::TimeType,
required: false,
description: 'Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`).'
field :vulnerability, Types::VulnerabilityType,
null: true,
description: 'Vulnerability created.'
def resolve(**attributes)
project = authorized_find!(id: attributes.fetch(:project))
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:create_vulnerabilities_via_api, project)
params = build_vulnerability_params(attributes)
result = ::Vulnerabilities::ManuallyCreateService.new(
project,
current_user,
params: params
).execute
{
vulnerability: result.payload[:vulnerability],
errors: result.success? ? [] : Array(result.message)
}
end
private
def find_object(id:)
# TODO: remove explicit coercion once compatibility layer has been removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Project].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def build_vulnerability_params(params)
vulnerability_params = params.slice(*%i[
title
state
severity
confidence
message
solution
detected_at
confirmed_at
resolved_at
dismissed_at
identifiers
])
scanner_params = {
name: params.fetch(:scanner_name),
type: params.fetch(:scanner_type)
}
{
vulnerability: vulnerability_params
.merge(scanner: scanner_params)
}
end
end
end
end
# frozen_string_literal: true
module Types
class VulnerabilityConfidenceEnum < BaseEnum
graphql_name 'VulnerabilityConfidence'
description 'Confidence that a given vulnerability is present in the codebase.'
::Enums::Vulnerability.confidence_levels.keys.each do |confidence_level|
value confidence_level.to_s.upcase, value: confidence_level.to_s
end
end
end
# frozen_string_literal: true
module Types
class VulnerabilityIdentifierInputType < BaseInputObject
argument :name, GraphQL::Types::String,
description: 'Name of the vulnerability identifier.',
required: true
argument :url, GraphQL::Types::String,
description: 'URL of the vulnerability identifier.',
required: true
argument :external_type, GraphQL::Types::String,
description: 'External type of the vulnerability identifier.',
required: false
argument :external_id, GraphQL::Types::String,
description: 'External ID of the vulnerability identifier.',
required: false
end
end
# frozen_string_literal: true
module Types
module VulnerabilityLocation
# rubocop: disable Graphql/AuthorizeTypes
class GenericType < BaseObject
graphql_name 'VulnerabilityLocationGeneric'
description 'Represents the location of a vulnerability found by a generic scanner.'
field :description, GraphQL::Types::String, null: true,
description: 'Free-form description of where the vulnerability is located.'
end
end
end
......@@ -12,7 +12,8 @@ module Types
VulnerabilityLocation::DastType,
VulnerabilityLocation::SastType,
VulnerabilityLocation::SecretDetectionType,
VulnerabilityLocation::CoverageFuzzingType
VulnerabilityLocation::CoverageFuzzingType,
VulnerabilityLocation::GenericType
def self.resolve_type(object, context)
case object[:report_type]
......@@ -28,6 +29,8 @@ module Types
VulnerabilityLocation::SecretDetectionType
when 'coverage_fuzzing'
VulnerabilityLocation::CoverageFuzzingType
when 'generic'
VulnerabilityLocation::GenericType
else
raise UnexpectedReportType, "Report type must be one of #{::Enums::Vulnerability.report_types.keys}"
end
......
......@@ -11,7 +11,8 @@ module EE
dast: 3,
coverage_fuzzing: 5,
api_fuzzing: 6,
cluster_image_scanning: 7
cluster_image_scanning: 7,
generic: 99
}.freeze
class_methods do
......
# frozen_string_literal: true
module Vulnerabilities
class ManuallyCreateService
include Gitlab::Allowable
METADATA_VERSION = "manual:1.0"
GENERIC_REPORT_TYPE = ::Enums::Vulnerability.report_types[:generic]
MANUAL_LOCATION_FINGERPRINT = Digest::SHA1.hexdigest("manually added").freeze
CONFIRMED_MESSAGE = "confirmed_at can only be set when state is confirmed"
RESOLVED_MESSAGE = "resolved_at can only be set when state is resolved"
DISMISSED_MESSAGE = "dismissed_at can only be set when state is dismissed"
def initialize(project, author, params:)
@project = project
@author = author
@params = params
end
def execute
unless Feature.enabled?(:create_vulnerabilities_via_api, @project)
return ServiceResponse.error(message: "create_vulnerabilities_via_api feature flag is not enabled for this project")
end
raise Gitlab::Access::AccessDeniedError unless can?(@author, :create_vulnerability, @project)
timestamps_dont_match_state_message = match_state_fields_with_state
return ServiceResponse.error(message: timestamps_dont_match_state_message) if timestamps_dont_match_state_message
vulnerability = initialize_vulnerability(@params[:vulnerability])
identifiers = initialize_identifiers(@params[:vulnerability][:identifiers])
scanner = initialize_scanner(@params[:vulnerability][:scanner])
finding = initialize_finding(vulnerability, identifiers, scanner, @params[:message], @params[:solution])
Vulnerability.transaction do
vulnerability.save!
finding.save!
Statistics::UpdateService.update_for(vulnerability)
HistoricalStatistics::UpdateService.update_for(@project)
ServiceResponse.success(payload: { vulnerability: vulnerability })
end
rescue ActiveRecord::RecordNotUnique => e
Gitlab::AppLogger.error(e.message)
ServiceResponse.error(message: "Vulnerability with those details already exists")
rescue ActiveRecord::RecordInvalid => e
ServiceResponse.error(message: e.message)
end
private
def match_state_fields_with_state
state = @params.dig(:vulnerability, :state)
case state
when "detected"
return CONFIRMED_MESSAGE if exists_in_vulnerability_params?(:confirmed_at)
return RESOLVED_MESSAGE if exists_in_vulnerability_params?(:resolved_at)
return DISMISSED_MESSAGE if exists_in_vulnerability_params?(:dismissed_at)
when "confirmed"
return RESOLVED_MESSAGE if exists_in_vulnerability_params?(:resolved_at)
return DISMISSED_MESSAGE if exists_in_vulnerability_params?(:dismissed_at)
when "resolved"
return CONFIRMED_MESSAGE if exists_in_vulnerability_params?(:confirmed_at)
return DISMISSED_MESSAGE if exists_in_vulnerability_params?(:dismissed_at)
end
end
def exists_in_vulnerability_params?(column_name)
@params.dig(:vulnerability, column_name.to_sym).present?
end
def initialize_vulnerability(vulnerability_hash)
attributes = vulnerability_hash
.slice(*%i[
state
severity
confidence
detected_at
confirmed_at
resolved_at
dismissed_at
])
.merge(
project: @project,
author: @author,
title: vulnerability_hash[:title]&.truncate(::Issuable::TITLE_LENGTH_MAX),
report_type: GENERIC_REPORT_TYPE
)
vulnerability = Vulnerability.new(**attributes)
vulnerability.confirmed_by = @author if vulnerability.confirmed?
vulnerability.resolved_by = @author if vulnerability.resolved?
vulnerability.dismissed_by = @author if vulnerability.dismissed?
vulnerability
end
# rubocop: disable CodeReuse/ActiveRecord
def initialize_identifiers(identifier_hashes)
identifier_hashes.map do |identifier|
name = identifier.dig(:name)
external_type = map_external_type_from_name(name)
external_id = name
fingerprint = Digest::SHA1.hexdigest("#{external_type}:#{external_id}")
url = identifier.dig(:url)
Vulnerabilities::Identifier.find_or_initialize_by(name: name) do |i|
i.fingerprint = fingerprint
i.project = @project
i.external_type = external_type
i.external_id = external_id
i.url = url
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def map_external_type_from_name(name)
return 'cve' if name.match?(/CVE/i)
return 'cwe' if name.match?(/CWE/i)
'other'
end
# rubocop: disable CodeReuse/ActiveRecord
def initialize_scanner(scanner_hash)
name = scanner_hash.dig(:name)
Vulnerabilities::Scanner.find_or_initialize_by(name: name) do |s|
s.project = @project
s.external_id = Gitlab::Utils.slugify(name)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def initialize_finding(vulnerability, identifiers, scanner, message, solution)
uuid = ::Security::VulnerabilityUUID.generate(
report_type: GENERIC_REPORT_TYPE,
primary_identifier_fingerprint: identifiers.first.fingerprint,
location_fingerprint: MANUAL_LOCATION_FINGERPRINT,
project_id: @project.id
)
Vulnerabilities::Finding.new(
project: @project,
identifiers: identifiers,
primary_identifier: identifiers.first,
vulnerability: vulnerability,
name: vulnerability.title,
severity: vulnerability.severity,
confidence: vulnerability.confidence,
report_type: vulnerability.report_type,
project_fingerprint: Digest::SHA1.hexdigest(identifiers.first.name),
location_fingerprint: MANUAL_LOCATION_FINGERPRINT,
metadata_version: METADATA_VERSION,
raw_metadata: {},
scanner: scanner,
uuid: uuid,
message: message,
solution: solution
)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Vulnerabilities::Create do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let(:project) { create(:project) }
let(:mutated_vulnerability) { subject[:vulnerability] }
before do
stub_licensed_features(security_dashboard: true)
end
describe '#resolve' do
using RSpec::Parameterized::TableSyntax
context 'when a vulnerability with the same identifier already exists' do
subject { resolve(described_class, args: attributes, ctx: { current_user: user }) }
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_type: "dast",
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
project.add_developer(user)
resolve(described_class, args: attributes, ctx: { current_user: user })
end
it 'returns the created vulnerability' do
expect(subject[:errors]).to contain_exactly("Vulnerability with those details already exists")
end
end
context 'with valid parameters' do
before do
project.add_developer(user)
end
subject { resolve(described_class, args: attributes, ctx: { current_user: user }) }
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_type: "dast",
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
before do
stub_feature_flags(create_vulnerabilities_via_api: false)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(create_vulnerabilities_via_api: project)
end
it 'returns the created vulnerability' do
expect(mutated_vulnerability).to be_detected
expect(subject[:errors]).to be_empty
end
context 'with custom state' do
let(:custom_timestamp) { Time.new(2020, 6, 21, 14, 22, 20) }
where(:state, :detected_at, :confirmed_at, :confirmed_by, :resolved_at, :resolved_by, :dismissed_at, :dismissed_by) do
[
['confirmed', ref(:custom_timestamp), ref(:custom_timestamp), ref(:user), nil, nil, nil, nil],
['resolved', ref(:custom_timestamp), nil, nil, ref(:custom_timestamp), ref(:user), nil, nil],
['dismissed', ref(:custom_timestamp), nil, nil, nil, nil, ref(:custom_timestamp), ref(:user)]
]
end
with_them do
let(:attributes) do
{
project: project_gid,
title: "Test vulnerability",
description: "Test vulnerability created via GraphQL",
scanner_type: "dast",
scanner_name: "My custom DAST scanner",
identifiers: [identifier_attributes],
state: state,
severity: "unknown",
confidence: "unknown",
detected_at: detected_at,
confirmed_at: confirmed_at,
resolved_at: resolved_at,
dismissed_at: dismissed_at,
solution: "rm -rf --no-preserve-root /",
message: "You can't fix this"
}
end
it "returns a #{params[:state]} vulnerability", :aggregate_failures do
expect(mutated_vulnerability.state).to eq(state)
expect(mutated_vulnerability.detected_at).to eq(detected_at)
expect(mutated_vulnerability.confirmed_at).to eq(confirmed_at)
expect(mutated_vulnerability.confirmed_by).to eq(confirmed_by)
expect(mutated_vulnerability.resolved_at).to eq(resolved_at)
expect(mutated_vulnerability.resolved_by).to eq(resolved_by)
expect(mutated_vulnerability.dismissed_at).to eq(dismissed_at)
expect(mutated_vulnerability.dismissed_by).to eq(dismissed_by)
expect(subject[:errors]).to be_empty
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityConfidence'] do
it 'exposes all vulnerability confidence values' do
expect(described_class.values.keys).to match_array(%w[IGNORE UNKNOWN EXPERIMENTAL LOW MEDIUM HIGH CONFIRMED])
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::VulnerabilityIdentifierInputType do
specify { expect(described_class.graphql_name).to eq('VulnerabilityIdentifierInput') }
it 'has the correct arguments' do
expect(described_class.arguments.keys).to match_array(%w[name url externalType externalId])
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityLocationGeneric'] do
it do
expect(described_class).to have_graphql_fields(
:description
)
end
end
......@@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityReportType'] do
it 'exposes all vulnerability report types' do
expect(described_class.values.keys).to match_array(%w[SAST SECRET_DETECTION DAST CLUSTER_IMAGE_SCANNING CONTAINER_SCANNING DEPENDENCY_SCANNING COVERAGE_FUZZING API_FUZZING])
expect(described_class.values.keys).to match_array(%w[SAST SECRET_DETECTION DAST CLUSTER_IMAGE_SCANNING CONTAINER_SCANNING DEPENDENCY_SCANNING COVERAGE_FUZZING API_FUZZING GENERIC])
end
end
......@@ -11,14 +11,17 @@ RSpec.describe Vulnerability do
end
let(:report_types) do
{ sast: 0,
{
sast: 0,
dependency_scanning: 1,
container_scanning: 2,
dast: 3,
secret_detection: 4,
coverage_fuzzing: 5,
api_fuzzing: 6,
cluster_image_scanning: 7 }
cluster_image_scanning: 7,
generic: 99
}
end
let_it_be(:project) { create(:project) }
......
......@@ -56,6 +56,9 @@ RSpec.describe 'Query.vulnerabilities.location' do
vulnerableMethod
blobPath
}
... on VulnerabilityLocationGeneric {
description
}
}
QUERY
end
......@@ -110,6 +113,32 @@ RSpec.describe 'Query.vulnerabilities.location' do
end
end
context 'when the vulnerability was found by a generic scanner' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :generic)
end
let_it_be(:finding) do
create(
:vulnerabilities_finding,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
let_it_be(:metadata) do
{
description: "Something really bad"
}
end
it 'returns a generic location' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationGeneric')
end
end
context 'when the vulnerability was found by a cluster image scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :cluster_image_scanning)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::ManuallyCreateService do
before do
stub_licensed_features(security_dashboard: true)
end
let_it_be(:user) { create(:user) }
let(:project) { create(:project) } # cannot use let_it_be here: caching causes problems with permission-related tests
subject { described_class.new(project, user, params: params).execute }
context 'with an authorized user with proper permissions' do
before do
project.add_developer(user)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(create_vulnerabilities_via_api: false)
end
let(:scanner_params) do
{
name: "My manual scanner"
}
end
let(:identifier_params) do
{
name: "Test identifier 1",
url: "https://test.com"
}
end
let(:params) do
{
vulnerability: {
title: "Test vulnerability",
state: "detected",
severity: "unknown",
confidence: "unknown",
identifiers: [identifier_params],
scanner: scanner_params
}
}
end
it 'returns an error' do
result = subject
expect(result.success?).to be_falsey
expect(subject.message).to match(/create_vulnerabilities_via_api feature flag is not enabled for this project/)
end
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(create_vulnerabilities_via_api: project)
end
context 'with valid parameters' do
let(:scanner_params) do
{
name: "My manual scanner"
}
end
let(:identifier_params) do
{
name: "Test identifier 1",
url: "https://test.com"
}
end
let(:params) do
{
vulnerability: {
title: "Test vulnerability",
state: "detected",
severity: "unknown",
confidence: "unknown",
identifiers: [identifier_params],
scanner: scanner_params
}
}
end
let(:vulnerability) { subject.payload[:vulnerability] }
it 'does not exceed query limit' do
expect { subject }.not_to exceed_query_limit(20)
end
it 'creates a new Vulnerability' do
expect { subject }.to change(Vulnerability, :count).by(1)
end
it 'creates a Vulnerability with correct attributes' do
expect(vulnerability.report_type).to eq("generic")
expect(vulnerability.state).to eq(params.dig(:vulnerability, :state))
expect(vulnerability.severity).to eq(params.dig(:vulnerability, :severity))
expect(vulnerability.confidence).to eq(params.dig(:vulnerability, :confidence))
end
it 'creates associated objects', :aggregate_failures do
expect { subject }.to change(Vulnerabilities::Finding, :count).by(1)
.and change(Vulnerabilities::Scanner, :count).by(1)
.and change(Vulnerabilities::Identifier, :count).by(1)
end
context 'when Scanner already exists' do
let!(:scanner) { create(:vulnerabilities_scanner, name: scanner_params[:name]) }
it 'does not create a new Scanner' do
expect { subject }.to change(Vulnerabilities::Scanner, :count).by(0)
end
end
context 'when Identifier already exists' do
let!(:identifier) { create(:vulnerabilities_identifier, name: identifier_params[:name]) }
it 'does not create a new Identifier' do
expect { subject }.not_to change(Vulnerabilities::Identifier, :count)
end
end
it 'creates all objects with correct attributes' do
expect(vulnerability.title).to eq(params.dig(:vulnerability, :title))
expect(vulnerability.report_type).to eq("generic")
expect(vulnerability.state).to eq(params.dig(:vulnerability, :state))
expect(vulnerability.severity).to eq(params.dig(:vulnerability, :severity))
expect(vulnerability.confidence).to eq(params.dig(:vulnerability, :confidence))
finding = vulnerability.finding
expect(finding.report_type).to eq("generic")
expect(finding.severity).to eq(params.dig(:vulnerability, :severity))
expect(finding.confidence).to eq(params.dig(:vulnerability, :confidence))
scanner = finding.scanner
expect(scanner.name).to eq(params.dig(:vulnerability, :scanner, :name))
primary_identifier = finding.primary_identifier
expect(primary_identifier.name).to eq(params.dig(:vulnerability, :identifiers, 0, :name))
end
context "when state fields match state" do
let(:params) do
{
vulnerability: {
title: "Test vulnerability",
state: "confirmed",
severity: "unknown",
confidence: "unknown",
confirmed_at: Time.now.iso8601,
identifiers: [identifier_params],
scanner: scanner_params
}
}
end
it 'creates Vulnerability in a different state with timestamps' do
freeze_time do
expect(vulnerability.state).to eq(params.dig(:vulnerability, :state))
expect(vulnerability.confirmed_at).to eq(params.dig(:vulnerability, :confirmed_at))
expect(vulnerability.confirmed_by).to eq(user)
end
end
end
context "when state fields don't match state" do
let(:params) do
{
vulnerability: {
title: "Test vulnerability",
state: "detected",
severity: "unknown",
confidence: "unknown",
confirmed_at: Time.now.iso8601,
identifiers: [identifier_params],
scanner: scanner_params
}
}
end
it 'returns an error' do
result = subject
expect(result.success?).to be_falsey
expect(subject.message).to match(/confirmed_at can only be set/)
end
end
end
context 'with invalid parameters' do
let(:params) do
{
vulnerability: {
identifiers: [{
name: "Test identfier 1",
url: "https://test.com"
}],
scanner: {
name: "My manual scanner"
}
}
}
end
it 'returns an error' do
expect(subject.error?).to be_truthy
end
end
end
end
context 'when user does not have rights to dismiss a vulnerability' do
let(:params) { {} }
before do
project.add_reporter(user)
end
it 'raises an "access denied" error' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
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