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
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(: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
before_action :ensure_pipeline, only: [:show]
before_action :push_experiment_to_gon, only: :index, if: :html_request?
......
......@@ -2,10 +2,21 @@
import { mapActions, mapState } from 'vuex';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
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 glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 {
i18n,
name: 'SecurityDashboardActionButtons',
components: {
GlButton,
......@@ -13,6 +24,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
vulnerability: {
type: Object,
......@@ -36,6 +48,18 @@ export default {
},
computed: {
...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: {
...mapActions('vulnerabilities', [
......@@ -46,7 +70,15 @@ export default {
]),
handleCreateIssue() {
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() {
const { vulnerability } = this;
......@@ -61,12 +93,6 @@ export default {
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>
......@@ -81,19 +107,21 @@ export default {
variant="info"
category="secondary"
icon="information-o"
data-testid="more-info"
@click="openModal({ vulnerability })"
/>
<gl-button
v-if="canCreateIssue"
key="create-issue"
v-gl-tooltip
:aria-label="$options.i18n.createIssue"
:aria-label="createIssueButtonLabel"
:loading="isCreatingIssue"
:title="$options.i18n.createIssue"
:title="createIssueButtonLabel"
class="js-create-issue"
variant="success"
category="secondary"
icon="issue-new"
data-testid="create-issue"
@click="handleCreateIssue"
/>
<template v-if="canDismissVulnerability">
......@@ -108,6 +136,7 @@ export default {
variant="warning"
category="secondary"
icon="redo"
data-testid="undo-dismiss"
@click="handleUndoDismiss"
/>
<gl-button
......@@ -121,6 +150,7 @@ export default {
variant="warning"
category="secondary"
icon="cancel"
data-testid="dismiss-vulnerability"
@click="handleDismissVulnerability"
/>
</template>
......
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_action_buttons.vue';
import { createWrapper, mount, shallowMount } from '@vue/test-utils';
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 { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
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 { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/modules/vulnerabilities/data/mock_data_vulnerabilities';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
describe('Security Dashboard Action Buttons', () => {
const Component = Vue.extend(component);
let vm;
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(() => {
store = createStore();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
wrapper.destroy();
});
describe('with a fresh vulnerability', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[0],
canCreateIssue: true,
canDismissVulnerability: true,
};
vm = mountComponentWithStore(Component, { store, props });
jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
});
wrapper = createFullComponent({
propsData: {
vulnerability: mockDataVulnerabilities[0],
canCreateIssue: true,
canDismissVulnerability: true,
},
});
afterEach(() => {
vm.$destroy();
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
});
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', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-more-info');
});
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', () => {
jest.spyOn(vm.$root, '$emit');
it('should emit an `setModalData` event and open the modal when clicked', async () => {
await findMoreInfoButton().trigger('click');
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/setModalData', {
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/setModalData', {
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', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-create-issue');
it('should render the create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(true);
});
it('should render the create issue button', () => {
expect(button).not.toBeNull();
it('should render the correct tooltip', () => {
expect(findCreateIssueButton().attributes('title')).toBe(i18n.createIssue);
});
it('should emit an `createIssue` event when clicked', () => {
button.click();
it('should emit an `createIssue` event when clicked', async () => {
await findCreateIssueButton().trigger('click');
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', {
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
describe('Dismiss Vulnerability Button', () => {
let button;
describe('with Jira issues for vulnerabilities enabled', () => {
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(() => {
button = vm.$el.querySelector('.js-dismiss-vulnerability');
expect(visitUrl).toHaveBeenCalledWith(
mockDataVulnerabilities[8].create_jira_issue_url,
true, // external link flag
);
});
});
});
describe('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', () => {
button.click();
it('should emit an `dismissVulnerability` event when clicked', async () => {
await findDismissVulnerabilityButton().trigger('click');
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/dismissVulnerability', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/dismissVulnerability',
{
vulnerability: mockDataVulnerabilities[0],
flashError: true,
},
);
});
});
});
describe('with a vulnerbility that has an issue', () => {
describe('with a vulnerability that has an issue', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[3],
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
wrapper = createShallowComponent({
propsData: {
vulnerability: mockDataVulnerabilities[3],
},
});
});
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', () => {
expect(vm.$el.querySelector('.js-create-issue')).toBeNull();
expect(findCreateIssueButton().exists()).toBe(false);
});
});
describe('with a vulnerability that has been dismissed', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[2],
canDismissVulnerability: true,
isDismissed: true,
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
wrapper = createShallowComponent({
propsData: {
vulnerability: mockDataVulnerabilities[2],
canDismissVulnerability: true,
isDismissed: true,
},
});
});
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', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).toBeNull();
expect(findDismissVulnerabilityButton().exists()).toBe(false);
});
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 [
state: 'opened',
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 ""
msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
msgid "SecurityReports|Create Jira issue"
msgstr ""
msgid "SecurityReports|Create issue"
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