Skip to content
Snippets Groups Projects
Unverified Commit 51205f55 authored by Michał Zając's avatar Michał Zając
Browse files

Add VulnerabilityCreate GraphQL mutation

This mutation allows users to create Vulnerability objects manually via
GraphQL query.

* Add create_vulnerabilities_via_api feature flag
* Update GraphQL descriptions
* Add specs
* Add GraphQL specs
* Add VulnerabilityIdentifierInputType
* Add VulnerabilityConfidenceEnum
* Add VulnerabilityLocation::GenericType

Changelog: added
EE: true
parent baf84a03
No related branches found
No related tags found
No related merge requests found
Showing
with 888 additions and 6 deletions
---
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
Loading
Loading
@@ -4421,6 +4421,39 @@ Input type: `VulnerabilityConfirmInput`
| <a id="mutationvulnerabilityconfirmerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationvulnerabilityconfirmvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after state change. |
 
### `Mutation.vulnerabilityCreate`
Input type: `VulnerabilityCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationvulnerabilitycreateconfidence"></a>`confidence` | [`VulnerabilityConfidence`](#vulnerabilityconfidence) | Confidence of the vulnerability (defaults to `unknown`). |
| <a id="mutationvulnerabilitycreateconfirmedat"></a>`confirmedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to confirmed (defaults to creation time if status is `confirmed`). |
| <a id="mutationvulnerabilitycreatedescription"></a>`description` | [`String!`](#string) | Description of the vulnerability. |
| <a id="mutationvulnerabilitycreatedetectedat"></a>`detectedAt` | [`Time`](#time) | Timestamp of when the vulnerability was first detected (defaults to creation time). |
| <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="mutationvulnerabilitycreatemessage"></a>`message` | [`String`](#string) | Additional information about the vulnerability. |
| <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="mutationvulnerabilitycreatescannername"></a>`scannerName` | [`String!`](#string) | Name of the security scanner used to discover the vulnerability. |
| <a id="mutationvulnerabilitycreatescannertype"></a>`scannerType` | [`SecurityScannerType!`](#securityscannertype) | Type of the security scanner used to discover the vulnerability. |
| <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="mutationvulnerabilitycreatestate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). |
| <a id="mutationvulnerabilitycreatetitle"></a>`title` | [`String!`](#string) | Title of the vulnerability. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationvulnerabilitycreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationvulnerabilitycreatevulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability created. |
### `Mutation.vulnerabilityDismiss`
 
Input type: `VulnerabilityDismissInput`
Loading
Loading
@@ -13200,6 +13233,7 @@ Represents summary of a security report.
| <a id="securityreportsummarycoveragefuzzing"></a>`coverageFuzzing` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `coverage_fuzzing` scan. |
| <a id="securityreportsummarydast"></a>`dast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dast` scan. |
| <a id="securityreportsummarydependencyscanning"></a>`dependencyScanning` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dependency_scanning` scan. |
| <a id="securityreportsummarygeneric"></a>`generic` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `generic` scan. |
| <a id="securityreportsummarysast"></a>`sast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `sast` scan. |
| <a id="securityreportsummarysecretdetection"></a>`secretDetection` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `secret_detection` scan. |
 
Loading
Loading
@@ -14080,7 +14114,7 @@ Represents a vulnerability.
| <a id="vulnerabilitynotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="vulnerabilityprimaryidentifier"></a>`primaryIdentifier` | [`VulnerabilityIdentifier`](#vulnerabilityidentifier) | Primary identifier of the vulnerability. |
| <a id="vulnerabilityproject"></a>`project` | [`Project`](#project) | The project on which the vulnerability was found. |
| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING). `Scan Type` in the UI. |
| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING, GENERIC). `Scan Type` in the UI. |
| <a id="vulnerabilityresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to resolved. |
| <a id="vulnerabilityresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | The user that resolved the vulnerability. |
| <a id="vulnerabilityresolvedondefaultbranch"></a>`resolvedOnDefaultBranch` | [`Boolean!`](#boolean) | Indicates whether the vulnerability is fixed on the default branch or not. |
Loading
Loading
@@ -14374,6 +14408,16 @@ Represents the location of a vulnerability found by a dependency security scan.
| <a id="vulnerabilitylocationdependencyscanningdependency"></a>`dependency` | [`VulnerableDependency`](#vulnerabledependency) | Dependency containing the vulnerability. |
| <a id="vulnerabilitylocationdependencyscanningfile"></a>`file` | [`String`](#string) | Path to the vulnerable file. |
 
### `VulnerabilityLocationGeneric`
Represents the location of a vulnerability found by a generic scanner.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilitylocationgenericdescription"></a>`description` | [`String`](#string) | Free-form description of where the vulnerability is located. |
### `VulnerabilityLocationSast`
 
Represents the location of a vulnerability found by a SAST scan.
Loading
Loading
@@ -15811,6 +15855,20 @@ Possible states of a user.
| <a id="visibilityscopesenumprivate"></a>`private` | The snippet is visible only to the snippet creator. |
| <a id="visibilityscopesenumpublic"></a>`public` | The snippet can be accessed without any authentication. |
 
### `VulnerabilityConfidence`
Confidence that a given vulnerability is present in the codebase.
| Value | Description |
| ----- | ----------- |
| <a id="vulnerabilityconfidenceconfirmed"></a>`CONFIRMED` | |
| <a id="vulnerabilityconfidenceexperimental"></a>`EXPERIMENTAL` | |
| <a id="vulnerabilityconfidencehigh"></a>`HIGH` | |
| <a id="vulnerabilityconfidenceignore"></a>`IGNORE` | |
| <a id="vulnerabilityconfidencelow"></a>`LOW` | |
| <a id="vulnerabilityconfidencemedium"></a>`MEDIUM` | |
| <a id="vulnerabilityconfidenceunknown"></a>`UNKNOWN` | |
### `VulnerabilityDismissalReason`
 
The dismissal reason of the Vulnerability.
Loading
Loading
@@ -15872,6 +15930,7 @@ The type of the security scan that found the vulnerability.
| <a id="vulnerabilityreporttypecoverage_fuzzing"></a>`COVERAGE_FUZZING` | |
| <a id="vulnerabilityreporttypedast"></a>`DAST` | |
| <a id="vulnerabilityreporttypedependency_scanning"></a>`DEPENDENCY_SCANNING` | |
| <a id="vulnerabilityreporttypegeneric"></a>`GENERIC` | |
| <a id="vulnerabilityreporttypesast"></a>`SAST` | |
| <a id="vulnerabilityreporttypesecret_detection"></a>`SECRET_DETECTION` | |
 
Loading
Loading
@@ -16512,6 +16571,7 @@ One of:
- [`VulnerabilityLocationCoverageFuzzing`](#vulnerabilitylocationcoveragefuzzing)
- [`VulnerabilityLocationDast`](#vulnerabilitylocationdast)
- [`VulnerabilityLocationDependencyScanning`](#vulnerabilitylocationdependencyscanning)
- [`VulnerabilityLocationGeneric`](#vulnerabilitylocationgeneric)
- [`VulnerabilityLocationSast`](#vulnerabilitylocationsast)
- [`VulnerabilityLocationSecretDetection`](#vulnerabilitylocationsecretdetection)
 
Loading
Loading
@@ -17290,3 +17350,14 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="updatediffimagepositioninputwidth"></a>`width` | [`Int`](#int) | Total width of the image. |
| <a id="updatediffimagepositioninputx"></a>`x` | [`Int`](#int) | X position of the note. |
| <a id="updatediffimagepositioninputy"></a>`y` | [`Int`](#int) | Y position of the note. |
### `VulnerabilityIdentifierInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilityidentifierinputexternalid"></a>`externalId` | [`String`](#string) | External ID 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="vulnerabilityidentifierinputurl"></a>`url` | [`String!`](#string) | URL of the vulnerability identifier. |
Loading
Loading
@@ -33,6 +33,7 @@ module MutationType
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
Loading
Loading
# 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
Loading
Loading
@@ -12,7 +12,8 @@ class VulnerabilityLocationType < BaseUnion
VulnerabilityLocation::DastType,
VulnerabilityLocation::SastType,
VulnerabilityLocation::SecretDetectionType,
VulnerabilityLocation::CoverageFuzzingType
VulnerabilityLocation::CoverageFuzzingType,
VulnerabilityLocation::GenericType
 
def self.resolve_type(object, context)
case object[:report_type]
Loading
Loading
@@ -28,6 +29,8 @@ def self.resolve_type(object, context)
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
Loading
Loading
Loading
Loading
@@ -11,7 +11,8 @@ module Vulnerability
dast: 3,
coverage_fuzzing: 5,
api_fuzzing: 6,
cluster_image_scanning: 7
cluster_image_scanning: 7,
generic: 99
}.freeze
 
class_methods do
Loading
Loading
# 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
Loading
Loading
@@ -4,6 +4,6 @@
 
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
Loading
Loading
@@ -11,14 +11,17 @@
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) }
Loading
Loading
Loading
Loading
@@ -56,6 +56,9 @@
vulnerableMethod
blobPath
}
... on VulnerabilityLocationGeneric {
description
}
}
QUERY
end
Loading
Loading
@@ -110,6 +113,32 @@
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)
Loading
Loading
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment