Commit acd4b984 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch...

Merge branch '293719-jira-integration-fe-on-the-pipeline-security-tab-s-vulnerability-list-create-jira-issue-when' into 'master'

JIRA Integration - (FE) On the pipeline security tab's vulnerability list, create Jira issue

See merge request gitlab-org/gitlab!52990
parents 5caf8fc8 4173d77d
...@@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
end end
before_action :ensure_pipeline, only: [:show] before_action :ensure_pipeline, only: [:show]
before_action :push_experiment_to_gon, only: :index, if: :html_request? before_action :push_experiment_to_gon, only: :index, if: :html_request?
......
...@@ -2,10 +2,21 @@ ...@@ -2,10 +2,21 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlTooltipDirective, GlButton } from '@gitlab/ui'; import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants'; import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import { visitUrl } from '~/lib/utils/url_utility';
import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const i18n = {
moreInfo: s__('SecurityReports|More info'),
createIssue: s__('SecurityReports|Create issue'),
createJiraIssue: s__('SecurityReports|Create Jira issue'),
revertDismissVulnerability: s__('SecurityReports|Undo dismiss'),
dismissVulnerability: s__('SecurityReports|Dismiss vulnerability'),
};
export default { export default {
i18n,
name: 'SecurityDashboardActionButtons', name: 'SecurityDashboardActionButtons',
components: { components: {
GlButton, GlButton,
...@@ -13,6 +24,7 @@ export default { ...@@ -13,6 +24,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
vulnerability: { vulnerability: {
type: Object, type: Object,
...@@ -36,6 +48,18 @@ export default { ...@@ -36,6 +48,18 @@ export default {
}, },
computed: { computed: {
...mapState('vulnerabilities', ['isCreatingIssue', 'isDismissingVulnerability']), ...mapState('vulnerabilities', ['isCreatingIssue', 'isDismissingVulnerability']),
isJiraVulnerabilityIssuesEnabled() {
return (
this.glFeatures?.jiraForVulnerabilities &&
Boolean(this?.vulnerability?.create_jira_issue_url)
);
},
createIssueButtonLabel() {
const { $options } = this;
return this.isJiraVulnerabilityIssuesEnabled
? $options.i18n.createJiraIssue
: $options.i18n.createIssue;
},
}, },
methods: { methods: {
...mapActions('vulnerabilities', [ ...mapActions('vulnerabilities', [
...@@ -46,7 +70,15 @@ export default { ...@@ -46,7 +70,15 @@ export default {
]), ]),
handleCreateIssue() { handleCreateIssue() {
const { vulnerability } = this; const { vulnerability } = this;
this.createIssue({ vulnerability, flashError: true });
if (this.isJiraVulnerabilityIssuesEnabled) {
this.createNewJiraIssue(vulnerability);
} else {
this.createIssue({ vulnerability, flashError: true });
}
},
createNewJiraIssue({ create_jira_issue_url }) {
visitUrl(create_jira_issue_url, true);
}, },
handleDismissVulnerability() { handleDismissVulnerability() {
const { vulnerability } = this; const { vulnerability } = this;
...@@ -61,12 +93,6 @@ export default { ...@@ -61,12 +93,6 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, VULNERABILITY_MODAL_ID); this.$root.$emit(BV_SHOW_MODAL, VULNERABILITY_MODAL_ID);
}, },
}, },
i18n: {
moreInfo: s__('SecurityReports|More info'),
createIssue: s__('SecurityReports|Create issue'),
revertDismissVulnerability: s__('SecurityReports|Undo dismiss'),
dismissVulnerability: s__('SecurityReports|Dismiss vulnerability'),
},
}; };
</script> </script>
...@@ -81,19 +107,21 @@ export default { ...@@ -81,19 +107,21 @@ export default {
variant="info" variant="info"
category="secondary" category="secondary"
icon="information-o" icon="information-o"
data-testid="more-info"
@click="openModal({ vulnerability })" @click="openModal({ vulnerability })"
/> />
<gl-button <gl-button
v-if="canCreateIssue" v-if="canCreateIssue"
key="create-issue" key="create-issue"
v-gl-tooltip v-gl-tooltip
:aria-label="$options.i18n.createIssue" :aria-label="createIssueButtonLabel"
:loading="isCreatingIssue" :loading="isCreatingIssue"
:title="$options.i18n.createIssue" :title="createIssueButtonLabel"
class="js-create-issue" class="js-create-issue"
variant="success" variant="success"
category="secondary" category="secondary"
icon="issue-new" icon="issue-new"
data-testid="create-issue"
@click="handleCreateIssue" @click="handleCreateIssue"
/> />
<template v-if="canDismissVulnerability"> <template v-if="canDismissVulnerability">
...@@ -108,6 +136,7 @@ export default { ...@@ -108,6 +136,7 @@ export default {
variant="warning" variant="warning"
category="secondary" category="secondary"
icon="redo" icon="redo"
data-testid="undo-dismiss"
@click="handleUndoDismiss" @click="handleUndoDismiss"
/> />
<gl-button <gl-button
...@@ -121,6 +150,7 @@ export default { ...@@ -121,6 +150,7 @@ export default {
variant="warning" variant="warning"
category="secondary" category="secondary"
icon="cancel" icon="cancel"
data-testid="dismiss-vulnerability"
@click="handleDismissVulnerability" @click="handleDismissVulnerability"
/> />
</template> </template>
......
import Vue from 'vue'; import { createWrapper, mount, shallowMount } from '@vue/test-utils';
import component from 'ee/security_dashboard/components/vulnerability_action_buttons.vue'; import { GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import VulnerabilityActionButtons, {
i18n,
} from 'ee/security_dashboard/components/vulnerability_action_buttons.vue';
import createStore from 'ee/security_dashboard/store'; import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants'; import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import { visitUrl } from '~/lib/utils/url_utility';
import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities'; import mockDataVulnerabilities from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
describe('Security Dashboard Action Buttons', () => { describe('Security Dashboard Action Buttons', () => {
const Component = Vue.extend(component);
let vm;
let store; let store;
let props; let wrapper;
const wrapperFactory = (mountFn) => ({ ...options }) =>
extendedWrapper(
mountFn(VulnerabilityActionButtons, {
...options,
store,
}),
);
const createShallowComponent = wrapperFactory(shallowMount);
const createFullComponent = wrapperFactory(mount);
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findMoreInfoButton = () => wrapper.findByTestId('more-info');
const findCreateIssueButton = () => wrapper.findByTestId('create-issue');
const findDismissVulnerabilityButton = () => wrapper.findByTestId('dismiss-vulnerability');
const findUndoDismissButton = () => wrapper.findByTestId('undo-dismiss');
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy();
resetStore(store); resetStore(store);
wrapper.destroy();
}); });
describe('with a fresh vulnerability', () => { describe('with a fresh vulnerability', () => {
beforeEach(() => { beforeEach(() => {
props = { wrapper = createFullComponent({
vulnerability: mockDataVulnerabilities[0], propsData: {
canCreateIssue: true, vulnerability: mockDataVulnerabilities[0],
canDismissVulnerability: true, canCreateIssue: true,
}; canDismissVulnerability: true,
vm = mountComponentWithStore(Component, { store, props }); },
jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve()); });
});
afterEach(() => { jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
vm.$destroy();
}); });
it('should render three buttons in a button group', () => { it('should render three buttons in a button group', () => {
expect(vm.$el.querySelectorAll('.btn-group .btn')).toHaveLength(3); expect(findAllButtons()).toHaveLength(3);
}); });
describe('More Info Button', () => { describe('More Info Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-more-info');
});
it('should render the More info button', () => { it('should render the More info button', () => {
expect(button).not.toBeNull(); expect(findMoreInfoButton().exists()).toBe(true);
}); });
it('should emit an `setModalData` event and open the modal when clicked', () => { it('should emit an `setModalData` event and open the modal when clicked', async () => {
jest.spyOn(vm.$root, '$emit'); await findMoreInfoButton().trigger('click');
button.click(); expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/setModalData', {
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/setModalData', {
vulnerability: mockDataVulnerabilities[0], vulnerability: mockDataVulnerabilities[0],
}); });
expect(vm.$root.$emit).toHaveBeenCalledWith(BV_SHOW_MODAL, VULNERABILITY_MODAL_ID); expect(createWrapper(wrapper.vm.$root).emitted(BV_SHOW_MODAL)).toEqual([
[VULNERABILITY_MODAL_ID],
]);
}); });
}); });
describe('Create Issue Button', () => { describe('Create Issue Button', () => {
let button; it('should render the create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(true);
beforeEach(() => {
button = vm.$el.querySelector('.js-create-issue');
}); });
it('should render the create issue button', () => { it('should render the correct tooltip', () => {
expect(button).not.toBeNull(); expect(findCreateIssueButton().attributes('title')).toBe(i18n.createIssue);
}); });
it('should emit an `createIssue` event when clicked', () => { it('should emit an `createIssue` event when clicked', async () => {
button.click(); await findCreateIssueButton().trigger('click');
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', { expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', {
vulnerability: mockDataVulnerabilities[0], vulnerability: mockDataVulnerabilities[0],
flashError: true, flashError: true,
}); });
}); });
});
describe('Dismiss Vulnerability Button', () => { describe('with Jira issues for vulnerabilities enabled', () => {
let button; beforeEach(() => {
wrapper = createFullComponent({
propsData: {
vulnerability: mockDataVulnerabilities[8],
canCreateIssue: true,
},
provide: {
glFeatures: { jiraForVulnerabilities: true },
},
});
});
it('should render the correct tooltip', () => {
expect(findCreateIssueButton().attributes('title')).toBe(i18n.createJiraIssue);
});
it('should open a new window when the create-issue button is clicked', async () => {
expect(visitUrl).not.toHaveBeenCalled();
await findCreateIssueButton().trigger('click');
beforeEach(() => { expect(visitUrl).toHaveBeenCalledWith(
button = vm.$el.querySelector('.js-dismiss-vulnerability'); mockDataVulnerabilities[8].create_jira_issue_url,
true, // external link flag
);
});
}); });
});
describe('Dismiss Vulnerability Button', () => {
it('should render the dismiss vulnerability button', () => { it('should render the dismiss vulnerability button', () => {
expect(button).not.toBeNull(); expect(findDismissVulnerabilityButton().exists()).toBe(true);
}); });
it('should emit an `dismissVulnerability` event when clicked', () => { it('should emit an `dismissVulnerability` event when clicked', async () => {
button.click(); await findDismissVulnerabilityButton().trigger('click');
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/dismissVulnerability', { expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
vulnerability: mockDataVulnerabilities[0], 'vulnerabilities/dismissVulnerability',
flashError: true, {
}); vulnerability: mockDataVulnerabilities[0],
flashError: true,
},
);
}); });
}); });
}); });
describe('with a vulnerbility that has an issue', () => { describe('with a vulnerability that has an issue', () => {
beforeEach(() => { beforeEach(() => {
props = { wrapper = createShallowComponent({
vulnerability: mockDataVulnerabilities[3], propsData: {
}; vulnerability: mockDataVulnerabilities[3],
vm = mountComponentWithStore(Component, { store, props }); },
}); });
afterEach(() => {
vm.$destroy();
}); });
it('should only render one button', () => { it('should only render one button', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(1); expect(findAllButtons()).toHaveLength(1);
}); });
it('should not render the create issue button', () => { it('should not render the create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue')).toBeNull(); expect(findCreateIssueButton().exists()).toBe(false);
}); });
}); });
describe('with a vulnerability that has been dismissed', () => { describe('with a vulnerability that has been dismissed', () => {
beforeEach(() => { beforeEach(() => {
props = { wrapper = createShallowComponent({
vulnerability: mockDataVulnerabilities[2], propsData: {
canDismissVulnerability: true, vulnerability: mockDataVulnerabilities[2],
isDismissed: true, canDismissVulnerability: true,
}; isDismissed: true,
vm = mountComponentWithStore(Component, { store, props }); },
}); });
afterEach(() => {
vm.$destroy();
}); });
it('should render two buttons in a button group', () => { it('should render two buttons in a button group', () => {
expect(vm.$el.querySelectorAll('.btn-group .btn')).toHaveLength(2); expect(findAllButtons()).toHaveLength(2);
}); });
it('should not render the dismiss vulnerability button', () => { it('should not render the dismiss vulnerability button', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).toBeNull(); expect(findDismissVulnerabilityButton().exists()).toBe(false);
}); });
it('should render the undo dismiss button', () => { it('should render the undo dismiss button', () => {
expect(vm.$el.querySelector('.js-undo-dismiss')).not.toBeNull(); expect(findUndoDismissButton().exists()).toBe(true);
}); });
}); });
}); });
...@@ -568,4 +568,61 @@ export default [ ...@@ -568,4 +568,61 @@ export default [
state: 'opened', state: 'opened',
blob_path: '', blob_path: '',
}, },
{
id: 9,
create_jira_issue_url: 'http://jira-project.atlassian.com/report',
report_type: 'container_scanning',
name: 'CVE-2018-1000001 in glibc',
severity: 'high',
confidence: 'unknown',
scanner: {
external_id: 'clair',
name: 'Clair',
vendor: 'GitLab',
},
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2018-1000001',
name: 'CVE-2018-1000001',
url: 'https://security-tracker.debian.org/tracker/CVE-2018-1000001',
},
],
project_fingerprint: 'af08ab5aa899af9e74318ebc23684c9aa728ab7c',
create_vulnerability_feedback_issue_path: '/gitlab-org/sec-reports/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/sec-reports/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/sec-reports/vulnerability_feedback',
project: {
id: 19,
name: 'sec-reports',
full_path: '/gitlab-org/sec-reports',
full_name: 'Gitlab Org / sec-reports',
},
dismissal_feedback: null,
issue_feedback: null,
merge_request_feedback: null,
description:
'In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.',
links: [
{
url: 'https://security-tracker.debian.org/tracker/CVE-2018-1000001',
},
],
location: {
image:
'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
operating_system: 'debian:9',
dependency: {
package: {
name: 'glibc',
},
version: '2.24-11+deb9u3',
},
},
remediations: null,
solution: null,
state: 'opened',
blob_path: '',
},
]; ];
...@@ -25805,6 +25805,9 @@ msgstr "" ...@@ -25805,6 +25805,9 @@ msgstr ""
msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'" msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr "" msgstr ""
msgid "SecurityReports|Create Jira issue"
msgstr ""
msgid "SecurityReports|Create issue" msgid "SecurityReports|Create issue"
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