Commit bb69a402 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Sean McGivern

Add creating Vulnerability External Issue Link using GraphQL

This change adds mutation that allows to create external issue link with
issue in external provider in GraphQL API.
parent 48a39cc6
......@@ -14681,6 +14681,8 @@ type Mutation {
updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload
vulnerabilityConfirm(input: VulnerabilityConfirmInput!): VulnerabilityConfirmPayload
vulnerabilityDismiss(input: VulnerabilityDismissInput!): VulnerabilityDismissPayload
vulnerabilityExternalIssueLinkCreate(input: VulnerabilityExternalIssueLinkCreateInput!): VulnerabilityExternalIssueLinkCreatePayload
vulnerabilityExternalIssueLinkDestroy(input: VulnerabilityExternalIssueLinkDestroyInput!): VulnerabilityExternalIssueLinkDestroyPayload
vulnerabilityResolve(input: VulnerabilityResolveInput!): VulnerabilityResolvePayload
vulnerabilityRevertToDetected(input: VulnerabilityRevertToDetectedInput!): VulnerabilityRevertToDetectedPayload
}
......@@ -25226,6 +25228,81 @@ type VulnerabilityExternalIssueLinkConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of VulnerabilityExternalIssueLinkCreate
"""
input VulnerabilityExternalIssueLinkCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
External tracker type of the external issue link.
"""
externalTracker: VulnerabilityExternalIssueLinkExternalTracker!
"""
ID of the vulnerability.
"""
id: VulnerabilityID!
"""
Type of the external issue link.
"""
linkType: VulnerabilityExternalIssueLinkType!
}
"""
Autogenerated return type of VulnerabilityExternalIssueLinkCreate
"""
type VulnerabilityExternalIssueLinkCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The created external issue link.
"""
externalIssueLink: VulnerabilityExternalIssueLink
}
"""
Autogenerated input type of VulnerabilityExternalIssueLinkDestroy
"""
input VulnerabilityExternalIssueLinkDestroyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global ID of the vulnerability external issue link.
"""
id: VulnerabilitiesExternalIssueLinkID!
}
"""
Autogenerated return type of VulnerabilityExternalIssueLinkDestroy
"""
type VulnerabilityExternalIssueLinkDestroyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
An edge in a connection.
"""
......@@ -25241,6 +25318,16 @@ type VulnerabilityExternalIssueLinkEdge {
node: VulnerabilityExternalIssueLink
}
"""
The external tracker of the external issue link related to a vulnerability
"""
enum VulnerabilityExternalIssueLinkExternalTracker {
"""
Jira external tracker
"""
JIRA
}
"""
The type of the external issue link related to a vulnerability
"""
......
......@@ -43739,6 +43739,60 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityExternalIssueLinkCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "VulnerabilityExternalIssueLinkCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLinkCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityExternalIssueLinkDestroy",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "VulnerabilityExternalIssueLinkDestroyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLinkDestroyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityResolve",
"description": null,
......@@ -73408,6 +73462,224 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "VulnerabilityExternalIssueLinkCreateInput",
"description": "Autogenerated input type of VulnerabilityExternalIssueLinkCreate",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "ID of the vulnerability.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "VulnerabilityID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "linkType",
"description": "Type of the external issue link.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityExternalIssueLinkType",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "externalTracker",
"description": "External tracker type of the external issue link.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VulnerabilityExternalIssueLinkExternalTracker",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLinkCreatePayload",
"description": "Autogenerated return type of VulnerabilityExternalIssueLinkCreate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "externalIssueLink",
"description": "The created external issue link.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLink",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "VulnerabilityExternalIssueLinkDestroyInput",
"description": "Autogenerated input type of VulnerabilityExternalIssueLinkDestroy",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global ID of the vulnerability external issue link.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "VulnerabilitiesExternalIssueLinkID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLinkDestroyPayload",
"description": "Autogenerated return type of VulnerabilityExternalIssueLinkDestroy",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityExternalIssueLinkEdge",
......@@ -73453,6 +73725,23 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VulnerabilityExternalIssueLinkExternalTracker",
"description": "The external tracker of the external issue link related to a vulnerability",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "JIRA",
"description": "Jira external tracker",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VulnerabilityExternalIssueLinkType",
......@@ -3806,6 +3806,25 @@ Represents an external issue link of a vulnerability.
| `id` | VulnerabilitiesExternalIssueLinkID! | GraphQL ID of the external issue link |
| `linkType` | VulnerabilityExternalIssueLinkType! | Type of the external issue link |
### VulnerabilityExternalIssueLinkCreatePayload
Autogenerated return type of VulnerabilityExternalIssueLinkCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `externalIssueLink` | VulnerabilityExternalIssueLink | The created external issue link. |
### VulnerabilityExternalIssueLinkDestroyPayload
Autogenerated return type of VulnerabilityExternalIssueLinkDestroy.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### VulnerabilityIdentifier
Represents a vulnerability identifier.
......@@ -4812,6 +4831,14 @@ Possible states of a user.
| `private` | |
| `public` | |
### VulnerabilityExternalIssueLinkExternalTracker
The external tracker of the external issue link related to a vulnerability.
| Value | Description |
| ----- | ----------- |
| `JIRA` | Jira external tracker |
### VulnerabilityExternalIssueLinkType
The type of the external issue link related to a vulnerability.
......
......@@ -29,6 +29,8 @@ module EE
mount_mutation ::Mutations::Vulnerabilities::Resolve
mount_mutation ::Mutations::Vulnerabilities::Confirm
mount_mutation ::Mutations::Vulnerabilities::RevertToDetected
mount_mutation ::Mutations::Vulnerabilities::CreateExternalIssueLink
mount_mutation ::Mutations::Vulnerabilities::DestroyExternalIssueLink
mount_mutation ::Mutations::Boards::Update
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences
......
# frozen_string_literal: true
module Mutations
module Vulnerabilities
class CreateExternalIssueLink < BaseMutation
graphql_name 'VulnerabilityExternalIssueLinkCreate'
authorize :admin_vulnerability
field :external_issue_link, Types::Vulnerability::ExternalIssueLinkType,
null: true,
description: 'The created external issue link.'
argument :id,
::Types::GlobalIDType[::Vulnerability],
required: true,
description: 'ID of the vulnerability.'
argument :link_type,
::Types::Vulnerability::ExternalIssueLinkTypeEnum,
required: true,
description: 'Type of the external issue link.'
argument :external_tracker,
::Types::Vulnerability::ExternalIssueLinkExternalTrackerEnum,
required: true,
description: 'External tracker type of the external issue link.'
def resolve(id:, link_type:, external_tracker:)
vulnerability = authorized_find!(id: id)
result = create_external_issue_link(vulnerability, link_type, external_tracker)
{
external_issue_link: result.success? ? result.payload[:record] : nil,
errors: result.errors
}
end
private
def create_external_issue_link(vulnerability, link_type, external_tracker)
::VulnerabilityExternalIssueLinks::CreateService.new(current_user, vulnerability, external_tracker, link_type: link_type).execute
end
def find_object(id:)
# TODO: remove this line once the compatibility layer is removed.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Vulnerability].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Vulnerabilities
class DestroyExternalIssueLink < BaseMutation
graphql_name 'VulnerabilityExternalIssueLinkDestroy'
authorize :admin_vulnerability_external_issue_link
ERROR_MSG = 'Error deleting the vulnerability external issue link'
argument :id, ::Types::GlobalIDType[::Vulnerabilities::ExternalIssueLink],
required: true,
description: 'The global ID of the vulnerability external issue link.'
def resolve(id:)
vulnerability_external_issue_link = authorized_find!(id: id)
response = ::VulnerabilityExternalIssueLinks::DestroyService.new(vulnerability_external_issue_link).execute
errors = response.destroyed? ? [] : [ERROR_MSG]
{
errors: errors
}
end
private
def find_object(id:)
# TODO: remove this line once the compatibility layer is removed.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Vulnerabilities::ExternalIssueLink].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
......@@ -7,13 +7,15 @@ module Resolvers
type Types::ExternalIssueType, null: true
def resolve
return serialize_external_issue(object.external_issue, object.external_type) if object.external_issue.present?
BatchLoader::GraphQL.for(object.external_issue_key).batch(key: object.external_type) do |external_issue_keys, loader, args|
case args[:key]
when 'jira'
jira_issues(external_issue_keys).each do |external_issue|
loader.call(
external_issue.id,
::Integrations::Jira::IssueSerializer.new.represent(external_issue, project: object.vulnerability.project)
serialize_external_issue(external_issue, args[:key])
)
end
end
......@@ -30,5 +32,11 @@ module Resolvers
result[:issues]
end
def serialize_external_issue(external_issue, external_type)
case external_type
when 'jira' then ::Integrations::Jira::IssueSerializer.new.represent(external_issue, project: object.vulnerability.project)
end
end
end
end
# frozen_string_literal: true
module Types
module Vulnerability
class ExternalIssueLinkExternalTrackerEnum < BaseEnum
graphql_name 'VulnerabilityExternalIssueLinkExternalTracker'
description 'The external tracker of the external issue link related to a vulnerability'
::Vulnerabilities::ExternalIssueLink.external_types.keys.each do |external_type|
value external_type.to_s.upcase, value: external_type.to_s, description: "#{external_type.titleize} external tracker"
end
end
end
end
......@@ -26,6 +26,10 @@ module EE
issues_enabled || vulnerabilities_enabled
end
def configured_to_create_issues_from_vulnerabilities?
active? && project_key.present? && vulnerabilities_issuetype.present? && jira_vulnerabilities_integration_enabled?
end
def issue_types
client
.Issuetype
......@@ -54,7 +58,7 @@ module EE
jira_request do
issue = client.Issue.build
issue.save!(
issue.save(
fields: {
project: { id: jira_project_id },
issuetype: { id: vulnerabilities_issuetype },
......@@ -63,9 +67,6 @@ module EE
}
)
issue
rescue JIRA::HTTPError => e
issue.attrs[:errors] = ::Gitlab::Json.parse(e.response.read_body)
issue
end
end
......
......@@ -18,5 +18,7 @@ module Vulnerabilities
message: N_('already has a "created" issue link')
},
if: :created?
attr_accessor :external_issue
end
end
......@@ -45,6 +45,13 @@ class VulnerabilityPresenter < Gitlab::View::Presenter::Delegated
finding.scan || {}
end
def jira_issue_description
ApplicationController.render(
template: 'vulnerabilities/jira_issue_description.md.erb',
locals: { vulnerability: self }
)
end
private
def location_link_for(path)
......
# frozen_string_literal: true
module VulnerabilityExternalIssueLinks
class CreateService < BaseService
def initialize(user, vulnerability, external_provider, link_type: Vulnerabilities::ExternalIssueLink.link_types[:created])
@user = user
@vulnerability = vulnerability
@external_provider = external_provider
@link_type = link_type
@external_provider_service = prepare_external_provider_service
@external_issue_link = prepare_external_issue_link
end
def execute
return error(['External provider service is not configured to create issues.']) unless external_provider_service&.configured_to_create_issues_from_vulnerabilities?
return error(external_issue_link.errors.full_messages) unless external_issue_link.valid?
external_issue = create_external_issue
if external_issue.has_errors?
error(external_issue.errors.values)
else
if external_issue_link.update(external_issue_key: external_issue.id)
external_issue_link.external_issue = external_issue
success
else
error(external_issue_link.errors.full_messages)
end
end
end
private
attr_reader :user, :vulnerability, :link_type, :external_provider, :external_provider_service, :external_issue_link
delegate :project, to: :vulnerability
def create_external_issue
summary = _('Investigate vulnerability: %{title}') % { title: vulnerability.title }
description = vulnerability.present.jira_issue_description
external_provider_service.create_issue(summary, description)
end
def prepare_external_provider_service
case external_provider
when 'jira' then project&.jira_service
end
end
def prepare_external_issue_link
Vulnerabilities::ExternalIssueLink.new(
author: user,
vulnerability: vulnerability,
link_type: link_type,
external_type: external_provider,
external_project_key: external_provider_service&.project_key,
external_issue_key: 0
)
end
def success
ServiceResponse.success(payload: { record: external_issue_link })
end
def error(message)
ServiceResponse.error(message: message)
end
def result_payload
{ record: issue_link }
end
end
end
# frozen_string_literal: true
module VulnerabilityExternalIssueLinks
class DestroyService < BaseService
def initialize(vulnerability_external_issue_link)
@vulnerability_external_issue_link = vulnerability_external_issue_link
end
def execute
vulnerability_external_issue_link.destroy
end
private
attr_reader :vulnerability_external_issue_link
end
end
---
title: Add creating Vulnerability External Issue Link using GraphQL
merge_request: 48687
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Vulnerabilities::CreateExternalIssueLink do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be(:vulnerability) { create(:vulnerability, :with_findings) }
let_it_be(:user) { create(:user) }
context 'for JIRA external tracker and CREATED issue link' do
subject { mutation.resolve(id: GitlabSchema.id_from_object(vulnerability), link_type: 'created', external_tracker: 'jira') }
context 'when the project can have external issue links' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'when user does not have access to the project' do
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user has access to the project' do
before do
vulnerability.project.add_developer(user)
allow_next_instance_of(::VulnerabilityExternalIssueLinks::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(result)
end
end
context 'when issue creation fails' do
let(:result) { double(success?: false, payload: {}, errors: ['Error when creating issue in Jira']) }
it 'returns empty external issue link' do
expect(subject[:external_issue_link]).to be_nil
end
it 'returns error collection' do
expect(subject[:errors]).to eq(['Error when creating issue in Jira'])
end
end
context 'when issue creation succeeds' do
let_it_be(:external_issue_link) { build(:vulnerabilities_external_issue_link) }
let(:result) { double(success?: true, payload: { record: external_issue_link }, errors: []) }
it 'returns empty external issue link' do
expect(subject[:external_issue_link]).to eq(external_issue_link)
end
it 'returns empty error collection' do
expect(subject[:errors]).to be_empty
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Vulnerabilities::DestroyExternalIssueLink do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be(:vulnerability_external_issue_link) { create(:vulnerabilities_external_issue_link) }
let_it_be(:user) { create(:user) }
subject { mutation.resolve(id: GitlabSchema.id_from_object(vulnerability_external_issue_link)) }
before do
stub_licensed_features(security_dashboard: true)
end
context 'when user does not have permissions to destroy external issue link' do
it { expect {subject}.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
end
context 'when user has permission to destroy external issue link' do
before do
vulnerability_external_issue_link.vulnerability.project.add_developer(user)
end
context 'when destroy succeeds' do
before do
allow_next_instance_of(::VulnerabilityExternalIssueLinks::DestroyService) do |destroy_service|
allow(destroy_service).to receive(:execute).and_return(double(destroyed?: true))
end
end
it { is_expected.to eq(errors: []) }
end
context 'when destroy fails' do
before do
allow_next_instance_of(::VulnerabilityExternalIssueLinks::DestroyService) do |destroy_service|
allow(destroy_service).to receive(:execute).and_return(double(destroyed?: false))
end
end
it { is_expected.to eq(errors: ['Error deleting the vulnerability external issue link']) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityExternalIssueLinkExternalTracker'] do
let(:expected_values) { %w[JIRA] }
subject { described_class.values.keys }
it { is_expected.to contain_exactly(*expected_values) }
end
......@@ -172,7 +172,69 @@ RSpec.describe JiraService do
expect(WebMock).to have_requested(:post, 'http://jira.example.com/rest/api/2/issue').with(
body: { fields: { project: { id: '11223' }, issuetype: { id: '10001' }, summary: '', description: "*ID*: 2\n_Issue_: !" } }.to_json
).once
expect(issue.attrs[:errors]).to eq(errors)
expect(issue.errors).to eq('summary' => 'You must specify a summary of the issue.')
end
end
end
describe '#configured_to_create_issues_from_vulnerabilities?' do
subject(:configured_to_create_issues_from_vulnerabilities) { jira_service.configured_to_create_issues_from_vulnerabilities? }
context 'when is not active' do
before do
allow(jira_service).to receive(:active?).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'when is active' do
before do
allow(jira_service).to receive(:active?).and_return(true)
end
context 'and jira_vulnerabilities_integration is disabled' do
before do
allow(jira_service).to receive(:jira_vulnerabilities_integration_enabled?).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'and jira_vulnerabilities_integration is enabled' do
before do
allow(jira_service).to receive(:jira_vulnerabilities_integration_enabled?).and_return(true)
end
context 'and project key is missing' do
before do
allow(jira_service).to receive(:project_key).and_return('')
end
it { is_expected.to be_falsey }
end
context 'and project key is not missing' do
before do
allow(jira_service).to receive(:project_key).and_return('GV')
end
context 'and vulnerabilities issue type is missing' do
before do
allow(jira_service).to receive(:vulnerabilities_issuetype).and_return('')
end
it { is_expected.to be_falsey }
end
context 'and vulnerabilities issue type is not missing' do
before do
allow(jira_service).to receive(:vulnerabilities_issuetype).and_return('10001')
end
it { is_expected.to be_truthy }
end
end
end
end
end
......
......@@ -28,4 +28,34 @@ RSpec.describe VulnerabilityPresenter do
expect(path).to include("#L#{finding.location['start_line']}")
end
end
describe '#jira_issue_description' do
let(:expected_jira_issue_description) do
<<-JIRA.strip_heredoc
Issue created from vulnerability [#{finding.vulnerability.id}|http://localhost/#{project.full_path}/-/security/vulnerabilities/#{finding.vulnerability.id}]
h3. Description:
Description of #{finding.vulnerability.title}
* Severity: high
* Confidence: medium
* Location: [aws-key.py:5|http://localhost/#{project.full_path}/-/blob/b83d6e391c22777fca1ed3012fce84f633d7fed0/aws-key.py#L5]
h3. Scanner:
* Name: Find Security Bugs
JIRA
end
it 'returns the jira description in string format' do
jira_issue_description = subject.jira_issue_description
expect(jira_issue_description).to eq(expected_jira_issue_description)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Creating an External Issue Link' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) }
let_it_be(:vulnerability) { create(:vulnerability, title: 'My vulnerability', project: project, findings: [finding]) }
let_it_be(:external_provider) { 'jira' }
let(:mutation) do
params = { id: vulnerability.to_global_id.to_s, link_type: 'CREATED', external_tracker: 'JIRA' }
graphql_mutation(:vulnerability_external_issue_link_create, params)
end
def mutation_response
graphql_mutation_response(:vulnerability_external_issue_link_create)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(security_dashboard: true, jira_vulnerabilities_integration: true)
end
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not create external issue link' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Vulnerabilities::ExternalIssueLink, :count)
end
end
context 'when the user has permission' do
before do
vulnerability.project.add_developer(current_user)
end
context 'when security_dashboard is disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ['The resource that you are attempting to access does not '\
'exist or you don\'t have permission to perform this action']
end
context 'when security_dashboard is enabled' do
before do
stub_licensed_features(security_dashboard: true, jira_vulnerabilities_integration: true)
end
context 'when jira is not configured' do
it 'responds with error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to eq(['External provider service is not configured to create issues.'])
end
end
context 'when jira is configured' do
let!(:jira_service) { create(:jira_service, project: vulnerability.project, vulnerabilities_enabled: true, project_key: 'GV', vulnerabilities_issuetype: '10000') }
context 'when issue creation succeeds' do
before do
stub_request(:get, 'https://jira.example.com/rest/api/2/project/GV').to_return(status: 200, body: { 'id' => '10000' }.to_json)
stub_request(:post, 'https://jira.example.com/rest/api/2/issue')
.to_return(
status: 200,
body: jira_created_issue.to_json
)
end
let(:jira_created_issue) do
{
'id' => external_issue_id,
fields: {
project: { id: '11223' },
issuetype: { id: '10001' },
summary: 'Special Summary!?',
description: "*ID*: 2\n_Issue_: !",
created: '2020-06-25T15:39:30.000+0000',
updated: '2020-06-26T15:38:32.000+0000',
resolutiondate: '2020-06-27T13:23:51.000+0000',
labels: ['backend'],
status: { name: 'To Do' },
key: 'GV-5',
assignee: nil,
reporter: { name: 'admin', displayName: 'Admin' }
}
}
end
context 'and saving external issue link succeeds' do
let(:external_issue_id) { '10000' }
it 'creates the external issue link', :aggregate_failures do
expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(Vulnerabilities::ExternalIssueLink, :count).by(1)
expect(mutation_response['errors']).to be_empty
expect(mutation_response.dig('externalIssueLink', 'externalIssue', 'relativeReference')).to eq('GV-5')
end
end
context 'and saving external issue link fails' do
let(:external_issue_id) { nil }
it 'creates the external issue link' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Vulnerabilities::ExternalIssueLink, :count)
end
end
end
context 'when issue creation fails' do
before do
stub_request(:get, 'https://jira.example.com/rest/api/2/project/GV').to_return(status: 200, body: { 'id' => '10000' }.to_json)
stub_request(:post, 'https://jira.example.com/rest/api/2/issue').to_return(status: 400, body: { 'errors' => ['bad request'] }.to_json)
end
it 'does not create the external issue link' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Vulnerabilities::ExternalIssueLink, :count)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Creating an External Issue Link' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:vulnerability_external_issue_link) { create(:vulnerabilities_external_issue_link) }
let(:mutation) do
params = { id: vulnerability_external_issue_link.to_global_id.to_s }
graphql_mutation(:vulnerability_external_issue_link_destroy, params)
end
def mutation_response
graphql_mutation_response(:vulnerability_external_issue_link_destroy)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(security_dashboard: true)
end
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not destroy external issue link' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Vulnerabilities::ExternalIssueLink, :count)
end
end
context 'when the user has permission' do
before do
vulnerability_external_issue_link.vulnerability.project.add_developer(current_user)
end
context 'when security_dashboard is disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ['The resource that you are attempting to access does not '\
'exist or you don\'t have permission to perform this action']
end
context 'when security_dashboard is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'destroys the external issue link' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(Vulnerabilities::ExternalIssueLink, :count).by(-1)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe VulnerabilityExternalIssueLinks::CreateService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) }
let(:vulnerability) { create(:vulnerability, title: 'My vulnerability', project: project, findings: [finding]) }
let(:external_provider) { 'jira' }
let(:service) { described_class.new(user, vulnerability, external_provider) }
subject(:create_external_issue_link) { service.execute }
context 'for jira' do
let(:configured_to_create_issues_from_vulnerabilities) { false }
let(:jira_service) { double(configured_to_create_issues_from_vulnerabilities?: configured_to_create_issues_from_vulnerabilities, project_key: 'GV') }
before do
allow(project).to receive(:jira_service).and_return(jira_service)
project.add_developer(user)
end
context 'when jira service is not configured to create issues' do
it { is_expected.not_to be_success }
it 'returns response with error messages' do
expect(subject.message).to eq(['External provider service is not configured to create issues.'])
end
end
context 'when jira service is configured to create issues' do
let(:configured_to_create_issues_from_vulnerabilities) { true }
context 'and there is already created external issue link for given vulnerability' do
before do
create(:vulnerabilities_external_issue_link, vulnerability: vulnerability)
end
it { is_expected.not_to be_success }
it 'returns response with error messages' do
expect(subject.message).to eq(['Vulnerability already has a "created" issue link'])
end
end
context 'and there is no external issue link created for given vulnerability' do
let(:jira_issue_id) { nil }
let(:errors) { {} }
let(:jira_issue) { double(has_errors?: errors.present?, id: jira_issue_id, errors: errors) }
before do
allow(jira_service).to receive(:create_issue).and_return(jira_issue)
end
it 'creates issue using jira service' do
expect(jira_service).to receive(:create_issue).with("Investigate vulnerability: #{vulnerability.title}", kind_of(String))
subject
end
context 'and issue creation fails in Jira' do
let(:errors) { { 'summary' => 'The issue summary is invalid.' } }
it { is_expected.not_to be_success }
it 'returns response with error messages' do
expect(subject.message).to eq(['The issue summary is invalid.'])
end
end
context 'and issue creation succeeds in Jira' do
context 'and external issue link creation fails' do
let(:jira_issue_id) { nil }
it { is_expected.not_to be_success }
it 'returns response with error messages' do
expect(subject.message).to eq(['External issue key can\'t be blank'])
end
end
context 'and external issue link creation succeeds' do
let(:jira_issue_id) { '10001' }
it { is_expected.to be_success }
it 'creates external issue link in database' do
expect {subject}.to change {::Vulnerabilities::ExternalIssueLink.count}.by(1)
end
it 'responds with created external issue link in payload', :aggregate_failures do
external_issue_link = subject.payload[:record]
expect(external_issue_link).to be_a(::Vulnerabilities::ExternalIssueLink)
expect(external_issue_link).to have_attributes(
external_type: 'jira',
external_project_key: 'GV',
external_issue_key: '10001',
link_type: 'created',
author_id: user.id,
vulnerability_id: vulnerability.id
)
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe VulnerabilityExternalIssueLinks::DestroyService do
let_it_be(:project) { create(:project) }
let_it_be(:vulnerability_external_issue_link, refind: true) { create(:vulnerabilities_external_issue_link, project: project) }
let(:service) { described_class.new(vulnerability_external_issue_link) }
subject(:delete_external_issue_link) { service.execute }
it 'deletes the specified vulnerability-external issue link' do
expect { delete_external_issue_link }.to change { Vulnerabilities::ExternalIssueLink.count }.by(-1)
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