Commit 6d214afd authored by Imre Farkas's avatar Imre Farkas

Merge branch '276896-prepare-link-to-generate-issue-in-jira-from-vulnerability' into 'master'

Add link to generate issue from vulnerability in Jira

See merge request gitlab-org/gitlab!47048
parents f1afed1a 95d29336
...@@ -11,6 +11,8 @@ module VulnerabilitiesHelper ...@@ -11,6 +11,8 @@ module VulnerabilitiesHelper
result = { result = {
timestamp: Time.now.to_i, timestamp: Time.now.to_i,
create_issue_url: create_issue_url_for(vulnerability), create_issue_url: create_issue_url_for(vulnerability),
create_jira_issue_url: create_jira_issue_url_for(vulnerability),
related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]),
has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid), has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid),
create_mr_url: create_vulnerability_feedback_merge_request_path(vulnerability.finding.project), create_mr_url: create_vulnerability_feedback_merge_request_path(vulnerability.finding.project),
discussions_url: discussions_project_security_vulnerability_path(vulnerability.project, vulnerability), discussions_url: discussions_project_security_vulnerability_path(vulnerability.project, vulnerability),
...@@ -32,6 +34,16 @@ module VulnerabilitiesHelper ...@@ -32,6 +34,16 @@ module VulnerabilitiesHelper
create_issue_project_security_vulnerability_path(vulnerability.project, vulnerability) create_issue_project_security_vulnerability_path(vulnerability.project, vulnerability)
end end
def create_jira_issue_url_for(vulnerability)
return unless vulnerability.project.jira_vulnerabilities_integration_enabled?
summary = _('Investigate vulnerability: %{title}') % { title: vulnerability.title }
description = ApplicationController.render(template: 'vulnerabilities/jira_issue_description.md.erb',
locals: { vulnerability: vulnerability.present })
vulnerability.project.jira_service.new_issue_url_with_predefined_fields(summary, description)
end
def vulnerability_pipeline_data(pipeline) def vulnerability_pipeline_data(pipeline)
return unless pipeline return unless pipeline
......
...@@ -4,6 +4,8 @@ module EE ...@@ -4,6 +4,8 @@ module EE
module JiraService module JiraService
extend ActiveSupport::Concern extend ActiveSupport::Concern
MAX_URL_LENGTH = 4000
prepended do prepended do
validates :project_key, presence: true, if: :project_key_required? validates :project_key, presence: true, if: :project_key_required?
validates :vulnerabilities_issuetype, presence: true, if: :vulnerabilities_enabled validates :vulnerabilities_issuetype, presence: true, if: :vulnerabilities_enabled
...@@ -28,10 +30,22 @@ module EE ...@@ -28,10 +30,22 @@ module EE
def test(_) def test(_)
super.then do |result| super.then do |result|
next result unless result[:success] next result unless result[:success]
next result unless project.try(:jira_vulnerabilities_integration_enabled?) next result unless project.jira_vulnerabilities_integration_enabled?
result.merge(data: { issuetypes: issue_types }) result.merge(data: { issuetypes: issue_types })
end end
end end
def new_issue_url_with_predefined_fields(summary, description)
escaped_summary = CGI.escape(summary)
escaped_description = CGI.escape(description)
"#{url}/secure/CreateIssueDetails!init.jspa?pid=#{jira_project_id}&issuetype=#{vulnerabilities_issuetype}&summary=#{escaped_summary}&description=#{escaped_description}"[0..MAX_URL_LENGTH]
end
def jira_project_id
strong_memoize(:jira_project_id) do
client_url.present? ? jira_request { client.Project.find(project_key).id } : nil
end
end
end end
end end
...@@ -99,7 +99,7 @@ module Jira ...@@ -99,7 +99,7 @@ module Jira
return if vulnerability_ids.blank? return if vulnerability_ids.blank?
vulnerability_ids vulnerability_ids
.map { |vulnerability_id| %Q[description ~ "/-/security/vulnerabilities/#{vulnerability_id}"] } .map { |vulnerability_id| %Q[description ~ "/-/security/vulnerabilities/#{escape_quotes(vulnerability_id.to_s)}"] }
.join(' OR ') .join(' OR ')
.then { |query| "(#{query})" } .then { |query| "(#{query})" }
end end
......
<% if vulnerability.is_a? Vulnerability %>
<%= _("Issue created from vulnerability %{vulnerability_link}".html_safe) % { vulnerability_link: "[#{vulnerability.id}|#{vulnerability_url(vulnerability)}]" } %>
<% end %>
h3. <%= _("Description") %>:
<%= vulnerability.description %>
<% if vulnerability.severity.present? %>
* <%= _("Severity") %>: <%= vulnerability.severity %>
<% end %>
<% if vulnerability.confidence.present? %>
* <%= _("Confidence") %>: <%= vulnerability.confidence %>
<% end %>
<% if vulnerability.try(:file) %>
* <%= _("Location") %>: [<%= vulnerability.location_text %>|<%= vulnerability.location_link %>]
<% end %>
<% if vulnerability.solution.present? %>
### <%= _("Solution") %>:
<%= _("See vulnerability %{vulnerability_link} for any Solution details.".html_safe) % { vulnerability_link: "[#{vulnerability.id}|#{vulnerability_url(vulnerability)}]" } %>
<% end %>
<% if vulnerability.identifiers.present? %>
h3. <%= _("Identifiers") %>:
<% vulnerability.identifiers.each do |identifier| %>
<% if identifier[:url].present? %>
* [<%= identifier[:name] %>|<%= identifier[:url] %>]
<% else %>
* <%= identifier[:name] %>
<% end %>
<% end %>
<% end %>
<% if vulnerability.links.present? %>
h3. <%= _("Links") %>:
<% vulnerability.links.each do |link| %>
<% if link[:name].present? %>
* [<%= link[:name] %>|<%= link[:url] %>]
<% else %>
* <%= link[:url] %>
<% end %>
<% end %>
<% end %>
<% if vulnerability.remediations.present? && vulnerability.remediations.any? %>
### <%= _("Remediations") %>:
<%= _("See vulnerability %{vulnerability_link} for any Remediation details.".html_safe) % { vulnerability_link: "[#{vulnerability.id}|#{vulnerability_url(vulnerability)}]" } %>
<% end %>
<% if vulnerability.try(:scan).present? && vulnerability.try(:scanner).present? %>
h3. <%= _("Scanner") %>:
<% if vulnerability&.scanner[:name].present? %>
* <%= _("Name") %>: <%= vulnerability.scanner[:name] %>
<% end %>
<% if vulnerability&.scan[:type].present? %>
* <%= _("Type") %>: <%= vulnerability.scan[:type] %>
<% end %>
<% if vulnerability&.scan[:status].present? %>
* <%= _("Status") %>: <%= vulnerability.scan[:status] %>
<% end %>
<% if vulnerability&.scan[:start_time].present? %>
* <%= _("Start Time") %>: <%= vulnerability.scan[:start_time] %>
<% end %>
<% if vulnerability&.scan[:end_time].present? %>
* <%= _("End Time") %>: <%= vulnerability.scan[:end_time] %>
<% end %>
<% end %>
---
title: Add link to generate issue from vulnerability in Jira
merge_request: 47048
author:
type: added
...@@ -59,6 +59,8 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -59,6 +59,8 @@ RSpec.describe VulnerabilitiesHelper do
expect(subject).to include( expect(subject).to include(
timestamp: Time.now.to_i, timestamp: Time.now.to_i,
create_issue_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/create_issue", create_issue_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/create_issue",
create_jira_issue_url: nil,
related_jira_issues_path: "/#{project.full_path}/-/integrations/jira/issues?vulnerability_ids%5B%5D=#{vulnerability.id}",
has_mr: anything, has_mr: anything,
create_mr_url: "/#{project.full_path}/-/vulnerability_feedback", create_mr_url: "/#{project.full_path}/-/vulnerability_feedback",
discussions_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/discussions", discussions_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/discussions",
...@@ -142,6 +144,72 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -142,6 +144,72 @@ RSpec.describe VulnerabilitiesHelper do
end end
end end
describe '#create_jira_issue_url_for' do
subject { helper.vulnerability_details(vulnerability, pipeline) }
let(:jira_service) { double('JiraService', new_issue_url_with_predefined_fields: 'https://jira.example.com/new') }
before do
allow(helper).to receive(:can?).and_return(true)
allow(vulnerability.project).to receive(:jira_service).and_return(jira_service)
end
context 'with jira vulnerabilities integration enabled' do
before do
allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(true)
end
let(:expected_jira_issue_description) do
<<-JIRA.strip_heredoc
Issue created from vulnerability [#{vulnerability.id}|http://localhost/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}]
h3. Description:
Description of My vulnerability
* Severity: high
* Confidence: medium
* Location: [maven/src/main/java/com/gitlab/security_products/tests/App.java:29|http://localhost/#{project.full_path}/-/blob/b83d6e391c22777fca1ed3012fce84f633d7fed0/maven/src/main/java/com/gitlab/security_products/tests/App.java#L29]
### Solution:
See vulnerability [#{vulnerability.id}|http://localhost/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}] for any Solution details.
h3. Links:
* [Cipher does not check for integrity first?|https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first]
JIRA
end
it 'renders description using dedicated template' do
expect(ApplicationController).to receive(:render).with(template: 'vulnerabilities/jira_issue_description.md.erb', locals: { vulnerability: an_instance_of(VulnerabilityPresenter) })
subject
end
it 'delegates rendering URL to JiraService' do
expect(jira_service).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.title}", expected_jira_issue_description)
subject
end
it 'generates url to create issue in Jira' do
expect(subject[:create_jira_issue_url]).to eq('https://jira.example.com/new')
end
end
context 'with jira vulnerabilities integration disabled' do
before do
allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(false)
end
it { expect(subject[:create_jira_issue_url]).to be_nil }
end
end
describe '#vulnerability_finding_data' do describe '#vulnerability_finding_data' do
subject { helper.vulnerability_finding_data(vulnerability) } subject { helper.vulnerability_finding_data(vulnerability) }
......
...@@ -3,7 +3,16 @@ ...@@ -3,7 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe JiraService do RSpec.describe JiraService do
let(:jira_service) { build(:jira_service) } let(:jira_service) { build(:jira_service, **options) }
let(:options) do
{
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
project_key: 'GL'
}
end
describe 'validations' do describe 'validations' do
it 'validates presence of project_key if issues_enabled' do it 'validates presence of project_key if issues_enabled' do
...@@ -127,4 +136,30 @@ RSpec.describe JiraService do ...@@ -127,4 +136,30 @@ RSpec.describe JiraService do
end end
end end
end end
describe '#new_issue_url_with_predefined_fields' do
before do
allow(jira_service).to receive(:jira_project_id).and_return('11223')
allow(jira_service).to receive(:vulnerabilities_issuetype).and_return('10001')
end
let(:expected_new_issue_url) { '/secure/CreateIssueDetails!init.jspa?pid=11223&issuetype=10001&summary=Special+Summary%21%3F&description=%2AID%2A%3A+2%0A_Issue_%3A+%21' }
subject(:new_issue_url) { jira_service.new_issue_url_with_predefined_fields("Special Summary!?", "*ID*: 2\n_Issue_: !") }
it { is_expected.to eq(expected_new_issue_url) }
end
describe '#jira_project_id' do
let(:jira_service) { described_class.new(options) }
let(:project_info_result) { { 'id' => '10000' } }
subject(:jira_project_id) { jira_service.jira_project_id }
before do
WebMock.stub_request(:get, /api\/2\/project\/GL/).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: project_info_result.to_json )
end
it { is_expected.to eq('10000') }
end
end end
...@@ -111,7 +111,7 @@ RSpec.describe Jira::JqlBuilderService do ...@@ -111,7 +111,7 @@ RSpec.describe Jira::JqlBuilderService do
end end
context 'with vulnerability_ids params' do context 'with vulnerability_ids params' do
let(:params) { { vulnerability_ids: [1, 25] } } let(:params) { { vulnerability_ids: %w[1 25] } }
it 'builds jql' do it 'builds jql' do
expect(subject).to eq('project = PROJECT_KEY AND (description ~ "/-/security/vulnerabilities/1" OR description ~ "/-/security/vulnerabilities/25") order by created DESC') expect(subject).to eq('project = PROJECT_KEY AND (description ~ "/-/security/vulnerabilities/1" OR description ~ "/-/security/vulnerabilities/25") order by created DESC')
......
...@@ -14822,6 +14822,9 @@ msgstr "" ...@@ -14822,6 +14822,9 @@ msgstr ""
msgid "Invalid yaml" msgid "Invalid yaml"
msgstr "" msgstr ""
msgid "Investigate vulnerability: %{title}"
msgstr ""
msgid "Invitation" msgid "Invitation"
msgstr "" msgstr ""
...@@ -24173,6 +24176,12 @@ msgstr "" ...@@ -24173,6 +24176,12 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel" msgid "See the affected projects in the GitLab admin panel"
msgstr "" msgstr ""
msgid "See vulnerability %{vulnerability_link} for any Remediation details."
msgstr ""
msgid "See vulnerability %{vulnerability_link} for any Solution details."
msgstr ""
msgid "See what's new at GitLab" msgid "See what's new at GitLab"
msgstr "" msgstr ""
......
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