Commit 9041c27c authored by Sam Beckham's avatar Sam Beckham Committed by Phil Hughes

Add modals and actions for the vulnerabilities in the Group security dashboard

parent c3a5223b
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import Tabs from '~/vue_shared/components/tabs/tabs'; import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue'; import Tab from '~/vue_shared/components/tabs/tab.vue';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import SecurityDashboardTable from './security_dashboard_table.vue'; import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue'; import VulnerabilityCountList from './vulnerability_count_list.vue';
import SvgBlankState from '~/pipelines/components/blank_state.vue'; import SvgBlankState from '~/pipelines/components/blank_state.vue';
...@@ -16,6 +18,7 @@ export default { ...@@ -16,6 +18,7 @@ export default {
}, },
components: { components: {
Icon, Icon,
IssueModal,
SecurityDashboardTable, SecurityDashboardTable,
SvgBlankState, SvgBlankState,
Tab, Tab,
...@@ -46,7 +49,7 @@ export default { ...@@ -46,7 +49,7 @@ export default {
}, },
computed: { computed: {
...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']), ...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']),
...mapState('vulnerabilities', ['hasError']), ...mapState('vulnerabilities', ['hasError', 'modal']),
sastCount() { sastCount() {
return this.vulnerabilitiesCountByReportType('sast'); return this.vulnerabilitiesCountByReportType('sast');
}, },
...@@ -66,7 +69,7 @@ export default { ...@@ -66,7 +69,7 @@ export default {
<span class="vertical-align-middle">${s__( <span class="vertical-align-middle">${s__(
'Security Reports|Security Dashboard Documentation', 'Security Reports|Security Dashboard Documentation',
)}</span> )}</span>
${gl.utils.spriteIcon('external-link', 's16 vertical-align-middle')} ${spriteIcon('external-link', 's16 vertical-align-middle')}
</a> </a>
`, `,
html: true, html: true,
...@@ -83,6 +86,9 @@ export default { ...@@ -83,6 +86,9 @@ export default {
'setVulnerabilitiesCountEndpoint', 'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesEndpoint', 'setVulnerabilitiesEndpoint',
'fetchVulnerabilitiesCount', 'fetchVulnerabilitiesCount',
'createIssue',
'dismissVulnerability',
'undoDismissal',
]), ]),
}, },
}; };
...@@ -90,6 +96,7 @@ export default { ...@@ -90,6 +96,7 @@ export default {
<template> <template>
<div> <div>
<div class="flash-container"></div>
<svg-blank-state <svg-blank-state
v-if="hasError" v-if="hasError"
:svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
...@@ -110,7 +117,8 @@ export default { ...@@ -110,7 +117,8 @@ export default {
</span> </span>
<span <span
v-popover="popoverOptions" v-popover="popoverOptions"
class="text-muted ml-1" class="text-muted prepend-left-4"
:aria-label="__('help')"
> >
<icon <icon
name="question" name="question"
...@@ -124,6 +132,14 @@ export default { ...@@ -124,6 +132,14 @@ export default {
/> />
</tab> </tab>
</tabs> </tabs>
<issue-modal
:modal="modal"
:can-create-issue-permission="true"
:can-create-feedback-permission="true"
@createNewIssue="createIssue({ vulnerability: modal.vulnerability })"
@dismissIssue="dismissVulnerability({ vulnerability: modal.vulnerability })"
@revertDismissIssue="undoDismissal({ vulnerability: modal.vulnerability })"
/>
</div> </div>
</div> </div>
</template> </template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'SecurityDashboardActionButtons',
components: {
Icon,
},
props: {
vulnerability: {
type: Object,
required: true,
},
},
methods: {
openModal() {
// TODO: Open the modal
},
newIssue() {
this.$store.dispatch('newIssue', this.vulnerability);
},
dismissVulnerability() {
this.$store.dispatch('dismissVulnerability', this.vulnerability);
},
},
};
</script>
<template>
<div>
<button
:aria-label="s__('Reports|More info')"
class="btn btn-secondary js-more-info"
type="button"
@click="openModal()"
>
<icon
name="external-link"
/>
</button>
<button
:aria-label="s__('Reports|New Issue')"
class="btn btn-inverted btn-info js-new-issue"
type="button"
@click="newIssue()"
>
<icon
name="issue-new"
/>
</button>
<button
:aria-label="s__('Reports|Dismiss Vulnerability')"
class="btn btn-inverted btn-remove js-dismiss-vulnerability"
type="button"
@click="dismissVulnerability()"
>
<icon
name="cancel"
/>
</button>
</div>
</template>
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
this.fetchVulnerabilities(); this.fetchVulnerabilities();
}, },
methods: { methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilities']), ...mapActions('vulnerabilities', ['fetchVulnerabilities', 'openModal']),
}, },
}; };
</script> </script>
...@@ -63,6 +63,7 @@ export default { ...@@ -63,6 +63,7 @@ export default {
v-for="vulnerability in vulnerabilities" v-for="vulnerability in vulnerabilities"
:key="vulnerability.id" :key="vulnerability.id"
:vulnerability="vulnerability" :vulnerability="vulnerability"
@openModal="openModal({ vulnerability })"
/> />
<pagination <pagination
......
<script> <script>
import { mapActions } from 'vuex';
import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SecurityDashboardActionButtons from './security_dashboard_action_buttons.vue'; import VulnerabilityActionButtons from './vulnerability_action_buttons.vue';
import VulnerabilityIssueLink from './vulnerability_issue_link.vue'; import VulnerabilityIssueLink from './vulnerability_issue_link.vue';
export default { export default {
name: 'SecurityDashboardTableRow', name: 'SecurityDashboardTableRow',
components: { components: {
SeverityBadge, SeverityBadge,
SecurityDashboardActionButtons,
GlSkeletonLoading, GlSkeletonLoading,
VulnerabilityActionButtons,
VulnerabilityIssueLink, VulnerabilityIssueLink,
}, },
props: { props: {
...@@ -31,17 +32,26 @@ export default { ...@@ -31,17 +32,26 @@ export default {
severity() { severity() {
return this.vulnerability.severity || ' '; return this.vulnerability.severity || ' ';
}, },
projectNamespace() { projectFullName() {
const { project } = this.vulnerability; const { project } = this.vulnerability;
return project && project.full_name ? project.full_name : null; return project && project.full_name;
}, },
isDismissed() { isDismissed() {
return this.vulnerability.dismissal_feedback; return Boolean(this.vulnerability.dismissal_feedback);
}, },
hasIssue() { hasIssue() {
return this.vulnerability.issue_feedback; return Boolean(this.vulnerability.issue_feedback);
},
canDismissVulnerability() {
return Boolean(this.vulnerability.vulnerability_feedback_url);
},
canCreateIssue() {
return this.canDismissVulnerability && !this.hasIssue;
}, },
}, },
methods: {
...mapActions('vulnerabilities', ['openModal']),
},
}; };
</script> </script>
...@@ -73,8 +83,11 @@ export default { ...@@ -73,8 +83,11 @@ export default {
:lines="2" :lines="2"
/> />
<div v-else> <div v-else>
<strike v-if="isDismissed">{{ vulnerability.name }}</strike> <span
<span v-else>{{ vulnerability.name }}</span> class="js-vulnerability-info"
:class="{ strikethrough: isDismissed }"
@click="openModal({ vulnerability })"
>{{ vulnerability.name }}</span>
<vulnerability-issue-link <vulnerability-issue-link
v-if="hasIssue" v-if="hasIssue"
:issue="vulnerability.issue_feedback" :issue="vulnerability.issue_feedback"
...@@ -82,9 +95,9 @@ export default { ...@@ -82,9 +95,9 @@ export default {
/> />
<br /> <br />
<span <span
v-if="projectNamespace" v-if="projectFullName"
class="vulnerability-namespace"> class="vulnerability-namespace">
{{ projectNamespace }} {{ projectFullName }}
</span> </span>
</div> </div>
</div> </div>
...@@ -98,12 +111,10 @@ export default { ...@@ -98,12 +111,10 @@ export default {
{{ s__('Reports|Confidence') }} {{ s__('Reports|Confidence') }}
</div> </div>
<div class="table-mobile-content text-capitalize"> <div class="table-mobile-content text-capitalize">
<strike v-if="isDismissed">{{ confidence }}</strike> <span :class="{ strikethrough: isDismissed }">{{ confidence }}</span>
<span v-else>{{ confidence }}</span>
</div> </div>
</div> </div>
<!-- This is hidden till we can hook up the actions
<div class="table-section section-20"> <div class="table-section section-20">
<div <div
class="table-mobile-header" class="table-mobile-header"
...@@ -112,12 +123,14 @@ export default { ...@@ -112,12 +123,14 @@ export default {
{{ s__('Reports|Actions') }} {{ s__('Reports|Actions') }}
</div> </div>
<div class="table-mobile-content vulnerabilities-action-buttons"> <div class="table-mobile-content vulnerabilities-action-buttons">
<security-dashboard-action-buttons <vulnerability-action-buttons
:vulnerability="vulnerability" :vulnerability="vulnerability"
:can-create-issue="canCreateIssue"
:can-dismiss-vulnerability="canDismissVulnerability"
:is-dismissed="isDismissed"
/> />
</div> </div>
</div> </div>
-->
</div> </div>
</template> </template>
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'SecurityDashboardActionButtons',
components: {
Icon,
LoadingButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
vulnerability: {
type: Object,
required: true,
},
canCreateIssue: {
type: Boolean,
required: false,
default: false,
},
canDismissVulnerability: {
type: Boolean,
required: false,
default: false,
},
isDismissed: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('vulnerabilities', ['isCreatingIssue', 'isDismissingVulnerability']),
},
methods: {
...mapActions('vulnerabilities', [
'openModal',
'createIssue',
'dismissVulnerability',
'undoDismissal',
]),
handleCreateIssue() {
const { vulnerability } = this;
this.createIssue({ vulnerability, flashError: true });
},
handleDismissVulnerability() {
const { vulnerability } = this;
this.dismissVulnerability({ vulnerability, flashError: true });
},
handleUndoDismissal() {
const { vulnerability } = this;
this.undoDismissal({ vulnerability, flashError: true });
},
},
};
</script>
<template>
<div>
<button
key="more-info"
v-gl-tooltip
:aria-label="s__('Security Reports|More info')"
:title="s__('Security Reports|More info')"
class="btn btn-secondary js-more-info"
type="button"
@click="openModal({ vulnerability })"
>
<icon name="information" />
</button>
<loading-button
v-if="canCreateIssue"
key="create-issue"
v-gl-tooltip
:aria-label="s__('Security Reports|New Issue')"
:loading="isCreatingIssue"
:title="s__('Security Reports|New Issue')"
container-class="btn btn-inverted btn-success js-create-issue"
type="button"
@click="handleCreateIssue"
>
<icon name="issue-new" />
</loading-button>
<template v-if="canDismissVulnerability">
<loading-button
v-if="isDismissed"
key="undo-dismissal"
:label="s__('Security Reports|Undo Dismissal')"
:loading="isDismissingVulnerability"
container-class="btn btn-inverted btn-warning js-undo-dismissal"
type="button"
@click="handleUndoDismissal"
/>
<loading-button
v-else
key="dismiss-vulnerability"
v-gl-tooltip
:aria-label="s__('Security Reports|Dismiss Vulnerability')"
:loading="isDismissingVulnerability"
:title="s__('Security Reports|Dismiss Vulnerability')"
container-class="btn btn-inverted btn-warning js-dismiss-vulnerability"
type="button"
@click="handleDismissVulnerability"
>
<icon name="cancel" />
</loading-button>
</template>
</div>
</template>
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'VulnerabilityIssueLink', name: 'VulnerabilityIssueLink',
...@@ -8,7 +8,7 @@ export default { ...@@ -8,7 +8,7 @@ export default {
Icon, Icon,
}, },
directives: { directives: {
Tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
issue: { issue: {
...@@ -31,7 +31,7 @@ export default { ...@@ -31,7 +31,7 @@ export default {
<template> <template>
<div class="d-inline"> <div class="d-inline">
<icon <icon
v-tooltip v-gl-tooltip
name="issues" name="issues"
css-classes="text-success vertical-align-middle" css-classes="text-success vertical-align-middle"
:title="s__('Security Dashboard|Issue Created')" :title="s__('Security Dashboard|Issue Created')"
......
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import createFlash from '~/flash';
export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => { export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint); commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint);
...@@ -30,17 +33,19 @@ export const requestVulnerabilitiesCount = ({ commit }) => { ...@@ -30,17 +33,19 @@ export const requestVulnerabilitiesCount = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES_COUNT); commit(types.REQUEST_VULNERABILITIES_COUNT);
}; };
export const receiveVulnerabilitiesCountSuccess = ({ commit }, response) => { export const receiveVulnerabilitiesCountSuccess = ({ commit }, { data }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, response.data); commit(types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, data);
}; };
export const receiveVulnerabilitiesCountError = ({ commit }) => { export const receiveVulnerabilitiesCountError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR); commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR);
}; };
export const fetchVulnerabilities = ({ state, dispatch }, page = 1) => { export const fetchVulnerabilities = ({ state, dispatch }, pageNumber) => {
dispatch('requestVulnerabilities'); dispatch('requestVulnerabilities');
const page = pageNumber || (state.pageInfo && state.pageInfo.page) || 1;
axios({ axios({
method: 'GET', method: 'GET',
url: state.vulnerabilitiesEndpoint, url: state.vulnerabilitiesEndpoint,
...@@ -59,10 +64,10 @@ export const requestVulnerabilities = ({ commit }) => { ...@@ -59,10 +64,10 @@ export const requestVulnerabilities = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES); commit(types.REQUEST_VULNERABILITIES);
}; };
export const receiveVulnerabilitiesSuccess = ({ commit }, response = {}) => { export const receiveVulnerabilitiesSuccess = ({ commit }, { headers, data }) => {
const normalizedHeaders = normalizeHeaders(response.headers); const normalizedHeaders = normalizeHeaders(headers);
const pageInfo = parseIntPagination(normalizedHeaders); const pageInfo = parseIntPagination(normalizedHeaders);
const vulnerabilities = response.data; const vulnerabilities = data;
commit(types.RECEIVE_VULNERABILITIES_SUCCESS, { pageInfo, vulnerabilities }); commit(types.RECEIVE_VULNERABILITIES_SUCCESS, { pageInfo, vulnerabilities });
}; };
...@@ -71,4 +76,117 @@ export const receiveVulnerabilitiesError = ({ commit }) => { ...@@ -71,4 +76,117 @@ export const receiveVulnerabilitiesError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_ERROR); commit(types.RECEIVE_VULNERABILITIES_ERROR);
}; };
export const openModal = ({ commit }, payload = {}) => {
$('#modal-mrwidget-security-issue').modal('show');
commit(types.SET_MODAL_DATA, payload);
};
export const createIssue = ({ dispatch }, { vulnerability, flashError }) => {
dispatch('requestCreateIssue');
axios
.post(vulnerability.vulnerability_feedback_url, {
vulnerability_feedback: {
feedback_type: 'issue',
category: vulnerability.report_type,
project_fingerprint: vulnerability.project_fingerprint,
vulnerability_data: {
...vulnerability,
category: vulnerability.report_type,
},
},
})
.then(({ data }) => {
dispatch('receiveCreateIssueSuccess', data);
})
.catch(() => {
dispatch('receiveCreateIssueError', { flashError });
});
};
export const requestCreateIssue = ({ commit }) => {
commit(types.REQUEST_CREATE_ISSUE);
};
export const receiveCreateIssueSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_CREATE_ISSUE_SUCCESS, payload);
};
export const receiveCreateIssueError = ({ commit }) => {
commit(types.RECEIVE_CREATE_ISSUE_ERROR);
createFlash(s__('Security Reports|There was an error creating the issue.'));
};
export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError }) => {
dispatch('requestDismissVulnerability');
axios
.post(vulnerability.vulnerability_feedback_url, {
vulnerability_feedback: {
feedback_type: 'dismissal',
category: vulnerability.report_type,
project_fingerprint: vulnerability.project_fingerprint,
vulnerability_data: {
...vulnerability,
category: vulnerability.report_type,
},
},
})
.then(({ data }) => {
const { id } = vulnerability;
dispatch('receiveDismissVulnerabilitySuccess', { id, data });
})
.catch(() => {
dispatch('receiveDismissVulnerabilityError', { flashError });
});
};
export const requestDismissVulnerability = ({ commit }) => {
commit(types.REQUEST_DISMISS_VULNERABILITY);
};
export const receiveDismissVulnerabilitySuccess = ({ commit }, payload) => {
commit(types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS, payload);
};
export const receiveDismissVulnerabilityError = ({ commit }, { flashError }) => {
commit(types.RECEIVE_DISMISS_VULNERABILITY_ERROR);
if (flashError) {
createFlash(s__('Security Reports|There was an error dismissing the issue.'));
}
};
export const undoDismissal = ({ dispatch }, { vulnerability, flashError }) => {
const { vulnerability_feedback_url, dismissal_feedback } = vulnerability;
// eslint-disable-next-line camelcase
const url = `${vulnerability_feedback_url}/${dismissal_feedback.id}`;
dispatch('requestUndoDismissal');
axios
.delete(url)
.then(() => {
const { id } = vulnerability;
dispatch('receiveUndoDismissalSuccess', { id });
})
.catch(() => {
dispatch('receiveUndoDismissalError', { flashError });
});
};
export const requestUndoDismissal = ({ commit }) => {
commit(types.REQUEST_UNDO_DISMISSAL);
};
export const receiveUndoDismissalSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_UNDO_DISMISSAL_SUCCESS, payload);
};
export const receiveUndoDismissalError = ({ commit }, { flashError }) => {
commit(types.RECEIVE_UNDO_DISMISSAL_ERROR);
if (flashError) {
createFlash(s__('Security Reports|There was an error undoing this dismissal.'));
}
};
export default () => {}; export default () => {};
...@@ -7,3 +7,17 @@ export const SET_VULNERABILITIES_COUNT_ENDPOINT = 'SET_VULNERABILITIES_COUNT_END ...@@ -7,3 +7,17 @@ export const SET_VULNERABILITIES_COUNT_ENDPOINT = 'SET_VULNERABILITIES_COUNT_END
export const REQUEST_VULNERABILITIES_COUNT = 'REQUEST_VULNERABILITIES_COUNT'; export const REQUEST_VULNERABILITIES_COUNT = 'REQUEST_VULNERABILITIES_COUNT';
export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_COUNT_SUCCESS'; export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_COUNT_SUCCESS';
export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR'; export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR';
export const SET_MODAL_DATA = 'SET_MODAL_DATA';
export const REQUEST_CREATE_ISSUE = 'REQUEST_CREATE_ISSUE';
export const RECEIVE_CREATE_ISSUE_SUCCESS = 'RECEIVE_CREATE_ISSUE_SUCCESS';
export const RECEIVE_CREATE_ISSUE_ERROR = 'RECEIVE_CREATE_ISSUE_ERROR';
export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
export const RECEIVE_DISMISS_VULNERABILITY_SUCCESS = 'RECEIVE_DISMISS_VULNERABILITY_SUCCESS';
export const RECEIVE_DISMISS_VULNERABILITY_ERROR = 'RECEIVE_DISMISS_VULNERABILITY_ERROR';
export const REQUEST_UNDO_DISMISSAL = 'REQUEST_UNDO_DISMISSAL';
export const RECEIVE_UNDO_DISMISSAL_SUCCESS = 'RECEIVE_UNDO_DISMISSAL_SUCCESS';
export const RECEIVE_UNDO_DISMISSAL_ERROR = 'RECEIVE_UNDO_DISMISSAL_ERROR';
import Vue from 'vue';
import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -32,4 +35,90 @@ export default { ...@@ -32,4 +35,90 @@ export default {
state.isLoadingVulnerabilitiesCount = false; state.isLoadingVulnerabilitiesCount = false;
state.hasError = true; state.hasError = true;
}, },
[types.SET_MODAL_DATA](state, payload) {
const { vulnerability } = payload;
Vue.set(state.modal, 'title', vulnerability.name);
Vue.set(state.modal.data.description, 'value', vulnerability.description);
Vue.set(
state.modal.data.project,
'value',
vulnerability.project && vulnerability.project.full_name,
);
Vue.set(
state.modal.data.project,
'url',
vulnerability.project && vulnerability.project.full_path,
);
Vue.set(state.modal.data.file, 'value', vulnerability.location && vulnerability.location.file);
Vue.set(
state.modal.data.identifiers,
'value',
vulnerability.identifiers.length && vulnerability.identifiers,
);
Vue.set(state.modal.data.severity, 'value', vulnerability.severity);
Vue.set(state.modal.data.confidence, 'value', vulnerability.confidence);
Vue.set(state.modal.data.solution, 'value', vulnerability.solution);
Vue.set(state.modal.data.links, 'value', vulnerability.links);
Vue.set(state.modal.data.instances, 'value', vulnerability.instances);
Vue.set(state.modal, 'vulnerability', vulnerability);
Vue.set(state.modal.vulnerability, 'hasIssue', Boolean(vulnerability.issue_feedback));
Vue.set(state.modal, 'error', null);
},
[types.REQUEST_CREATE_ISSUE](state) {
state.isCreatingIssue = true;
Vue.set(state.modal, 'isCreatingNewIssue', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_CREATE_ISSUE_SUCCESS](state, payload) {
// We don't cancel the loading state here because we're navigating away from the page
visitUrl(payload.issue_url);
},
[types.RECEIVE_CREATE_ISSUE_ERROR](state) {
state.isCreatingIssue = false;
Vue.set(state.modal, 'isCreatingNewIssue', false);
Vue.set(state.modal, 'error', 'There was an error creating the issue');
},
[types.REQUEST_DISMISS_VULNERABILITY](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id);
vulnerability.dismissal_feedback = payload.data;
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', true);
},
[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](state) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(
state.modal,
'error',
s__('Security Reports|There was an error dismissing the vulnerability.'),
);
},
[types.REQUEST_UNDO_DISMISSAL](state) {
state.isDismissingVulnerability = true;
Vue.set(state.modal, 'isDismissingVulnerability', true);
Vue.set(state.modal, 'error', null);
},
[types.RECEIVE_UNDO_DISMISSAL_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id);
vulnerability.dismissal_feedback = null;
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(state.modal.vulnerability, 'isDismissed', false);
},
[types.RECEIVE_UNDO_DISMISSAL_ERROR](state) {
state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false);
Vue.set(
state.modal,
'error',
s__('Security Reports|There was an error undoing the dismissal.'),
);
},
}; };
import { s__ } from '~/locale';
export default () => ({ export default () => ({
hasError: false, hasError: false,
isLoadingVulnerabilities: true, isLoadingVulnerabilities: true,
...@@ -7,4 +9,26 @@ export default () => ({ ...@@ -7,4 +9,26 @@ export default () => ({
vulnerabilitiesCount: {}, vulnerabilitiesCount: {},
vulnerabilitiesCountEndpoint: null, vulnerabilitiesCountEndpoint: null,
vulnerabilitiesEndpoint: null, vulnerabilitiesEndpoint: null,
activeVulnerability: null,
modal: {
data: {
description: { text: s__('Vulnerability|Description') },
project: {
text: s__('Vulnerability|Project'),
isLink: true,
},
file: { text: s__('Vulnerability|File') },
identifiers: { text: s__('Vulnerability|Identifiers') },
severity: { text: s__('Vulnerability|Severity') },
confidence: { text: s__('Vulnerability|Confidence') },
solution: { text: s__('Vulnerability|Solution') },
links: { text: s__('Vulnerability|Links') },
instances: { text: s__('Vulnerability|Instances') },
},
vulnerability: {},
isCreatingNewIssue: false,
isDismissingVulnerability: false,
},
isCreatingIssue: false,
isDismissingVulnerability: false,
}); });
---
title: Add modals and actions to the vulnerabilities in the Group security dashboard
merge_request: 7910
author:
type: added
import Vue from 'vue';
import Vuex from 'vuex';
import component from 'ee/security_dashboard/components/security_dashboard_action_buttons.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Security Dashboard Action Buttons', () => {
let vm;
let props;
let actions;
beforeEach(() => {
props = { vulnerability: { id: 123 } };
actions = {
newIssue: jasmine.createSpy('newIssue'),
dismissVulnerability: jasmine.createSpy('dismissVulnerability'),
};
const Component = Vue.extend(component);
const store = new Vuex.Store({ actions });
vm = mountComponentWithStore(Component, { props, store });
});
afterEach(() => {
vm.$destroy();
});
it('should render three buttons', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(3);
});
describe('More Info Button', () => {
it('should render the More info button', () => {
expect(vm.$el.querySelector('.js-more-info')).not.toBeNull();
});
});
describe('New Issue Button', () => {
it('should render the New Issue button', () => {
expect(vm.$el.querySelector('.js-new-issue')).not.toBeNull();
});
it('should trigger the `newIssue` action when clicked', () => {
vm.$el.querySelector('.js-new-issue').click();
expect(actions.newIssue).toHaveBeenCalledTimes(1);
});
});
describe('Dismiss Vulnerability Button', () => {
it('should render the Dismiss Vulnerability button', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).not.toBeNull();
});
it('should trigger the `dismissVulnerability` action when clicked', () => {
vm.$el.querySelector('.js-dismiss-vulnerability').click();
expect(actions.dismissVulnerability).toHaveBeenCalledTimes(1);
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import component from 'ee/security_dashboard/components/security_dashboard_table_row.vue'; import component from 'ee/security_dashboard/components/security_dashboard_table_row.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Table Row', () => { describe('Security Dashboard Table Row', () => {
let vm; let vm;
let props; let props;
const store = createStore();
const Component = Vue.extend(component); const Component = Vue.extend(component);
afterEach(() => {
vm.$destroy();
});
describe('when loading', () => { describe('when loading', () => {
beforeEach(() => { beforeEach(() => {
props = { isLoading: true }; props = { isLoading: true };
vm = mountComponent(Component, props); vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
}); });
it('should display the skeleton loader', () => { it('should display the skeleton loader', () => {
...@@ -33,16 +36,15 @@ describe('Security Dashboard Table Row', () => { ...@@ -33,16 +36,15 @@ describe('Security Dashboard Table Row', () => {
}); });
describe('when loaded', () => { describe('when loaded', () => {
beforeEach(() => { const vulnerability = mockDataVulnerabilities[0];
const vulnerability = {
severity: 'high',
name: 'Test vulnerability',
confidence: 'medium',
project: { full_name: 'project name' },
};
beforeEach(() => {
props = { vulnerability }; props = { vulnerability };
vm = mountComponent(Component, props); vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
}); });
it('should not display the skeleton loader', () => { it('should not display the skeleton loader', () => {
...@@ -55,22 +57,34 @@ describe('Security Dashboard Table Row', () => { ...@@ -55,22 +57,34 @@ describe('Security Dashboard Table Row', () => {
); );
}); });
it('should render the name', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.name,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.full_name,
);
});
it('should render the confidence', () => { it('should render the confidence', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent).toContain( expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent).toContain(
props.vulnerability.confidence, props.vulnerability.confidence,
); );
}); });
describe('the project name', () => {
it('should render the name', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.name,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.full_name,
);
});
it('should fire the openModal action when clicked', () => {
spyOn(vm.$store, 'dispatch');
vm.$el.querySelector('.js-vulnerability-info').click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/openModal', {
vulnerability,
});
});
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter'; import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import component from 'ee/security_dashboard/components/security_dashboard_table.vue'; import component from 'ee/security_dashboard/components/security_dashboard_table.vue';
import createStore from 'ee/security_dashboard/store'; import createStore from 'ee/security_dashboard/store';
import mockDataVulnerabilities from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities.json';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import waitForPromises from 'spec/helpers/wait_for_promises'; import waitForPromises from 'spec/helpers/wait_for_promises';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Table', () => { describe('Security Dashboard Table', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_action_buttons.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import mockDataVulnerabilities from '../store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Security Dashboard Action Buttons', () => {
const Component = Vue.extend(component);
let vm;
let store;
let props;
beforeEach(() => {
store = createStore();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
describe('with a fresh vulnerability', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[0],
canCreateIssue: true,
canDismissVulnerability: true,
};
vm = mountComponentWithStore(Component, { store, props });
spyOn(vm.$store, 'dispatch').and.returnValue(Promise.resolve());
});
afterEach(() => {
vm.$destroy();
});
it('should render three buttons', () => {
expect(vm.$el.querySelectorAll('.btn')).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();
});
it('should emit an `openModal` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/openModal', {
vulnerability: mockDataVulnerabilities[0],
});
});
});
describe('Create Issue Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-create-issue');
});
it('should render the create issue button', () => {
expect(button).not.toBeNull();
});
it('should emit an `createIssue` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/createIssue', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
describe('Dismiss Vulnerability Button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-dismiss-vulnerability');
});
it('should render the dismiss vulnerability button', () => {
expect(button).not.toBeNull();
});
it('should emit an `dismissVulnerability` event when clicked', () => {
button.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('vulnerabilities/dismissVulnerability', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
});
describe('with a vulnerbility that has an issue', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[3],
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should only render one button', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(1);
});
it('should not render the create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue')).toBeNull();
});
});
describe('with a vulnerbility that has been dismissed', () => {
beforeEach(() => {
props = {
vulnerability: mockDataVulnerabilities[2],
};
vm = mountComponentWithStore(Component, { store, props });
});
afterEach(() => {
vm.$destroy();
});
it('should only render one button', () => {
expect(vm.$el.querySelectorAll('.btn')).toHaveLength(1);
});
it('should not render the dismiss vulnerability button', () => {
expect(vm.$el.querySelector('.js-dismiss-vulnerability')).toBeNull();
});
});
});
...@@ -3,15 +3,37 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,15 +3,37 @@ import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import mockDataVulnerabilities from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities.json';
import mockDataVulnerabilitiesCount from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities_count.json';
import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state'; import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types'; import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import * as actions from 'ee/security_dashboard/store/modules/vulnerabilities/actions'; import * as actions from 'ee/security_dashboard/store/modules/vulnerabilities/actions';
import mockDataVulnerabilities from './data/mock_data_vulnerabilities.json';
import mockDataVulnerabilitiesCount from './data/mock_data_vulnerabilities_count.json';
describe('vulnerabiliites count actions', () => { describe('vulnerabiliites count actions', () => {
const data = mockDataVulnerabilitiesCount; const data = mockDataVulnerabilitiesCount;
describe('setVulnerabilitiesCountEndpoint', () => {
it('should commit the correct mutuation', done => {
const state = initialState;
const endpoint = 'fakepath.json';
testAction(
actions.setVulnerabilitiesCountEndpoint,
endpoint,
state,
[
{
type: types.SET_VULNERABILITIES_COUNT_ENDPOINT,
payload: endpoint,
},
],
[],
done,
);
});
});
describe('fetchVulnerabilitesCount', () => { describe('fetchVulnerabilitesCount', () => {
let mock; let mock;
const state = initialState; const state = initialState;
...@@ -255,20 +277,102 @@ describe('vulnerabilities actions', () => { ...@@ -255,20 +277,102 @@ describe('vulnerabilities actions', () => {
); );
}); });
}); });
});
describe('setVulnerabilitiesCountEndpoint', () => { describe('openModal', () => {
it('should commit the correct mutuation', done => { it('should commit the SET_MODAL_DATA mutation', done => {
const state = initialState;
const vulnerability = mockDataVulnerabilities[0];
testAction(
actions.openModal,
{ vulnerability },
state,
[
{
type: types.SET_MODAL_DATA,
payload: { vulnerability },
},
],
[],
done,
);
});
});
describe('issue creation', () => {
describe('createIssue', () => {
const vulnerability = mockDataVulnerabilities[0];
const data = { issue_url: 'fakepath.html' };
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onPost(vulnerability.vulnerability_feedback_url).replyOnce(200, { data });
});
it('should dispatch the request and success actions', done => {
testAction(
actions.createIssue,
{ vulnerability },
{},
[],
[
{ type: 'requestCreateIssue' },
{
type: 'receiveCreateIssueSuccess',
payload: { data },
},
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onPost(vulnerability.vulnerability_feedback_url).replyOnce(404, {});
});
it('should dispatch the request and error actions', done => {
const flashError = false;
testAction(
actions.createIssue,
{ vulnerability, flashError },
{},
[],
[
{ type: 'requestCreateIssue' },
{ type: 'receiveCreateIssueError', payload: { flashError } },
],
done,
);
});
});
});
describe('receiveCreateIssueSuccess', () => {
it('should commit the success mutation', done => {
const state = initialState; const state = initialState;
const endpoint = 'fakepath.json'; const data = mockDataVulnerabilities[0];
testAction( testAction(
actions.setVulnerabilitiesCountEndpoint, actions.receiveCreateIssueSuccess,
endpoint, { data },
state, state,
[ [
{ {
type: types.SET_VULNERABILITIES_COUNT_ENDPOINT, type: types.RECEIVE_CREATE_ISSUE_SUCCESS,
payload: endpoint, payload: { data },
}, },
], ],
[], [],
...@@ -276,4 +380,257 @@ describe('vulnerabilities actions', () => { ...@@ -276,4 +380,257 @@ describe('vulnerabilities actions', () => {
); );
}); });
}); });
describe('receiveCreateIssueError', () => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveCreateIssueError,
{},
state,
[{ type: types.RECEIVE_CREATE_ISSUE_ERROR }],
[],
done,
);
});
});
describe('requestCreateIssue', () => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestCreateIssue,
{},
state,
[{ type: types.REQUEST_CREATE_ISSUE }],
[],
done,
);
});
});
});
describe('vulnerability dismissal', () => {
describe('dismissVulnerability', () => {
const vulnerability = mockDataVulnerabilities[0];
const data = { vulnerability };
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onPost(vulnerability.vulnerability_feedback_url).replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
testAction(
actions.dismissVulnerability,
{ vulnerability },
{},
[],
[
{ type: 'requestDismissVulnerability' },
{
type: 'receiveDismissVulnerabilitySuccess',
payload: { data, id: vulnerability.id },
},
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onPost(vulnerability.vulnerability_feedback_url).replyOnce(404, {});
});
it('should dispatch the request and error actions', done => {
const flashError = false;
testAction(
actions.dismissVulnerability,
{ vulnerability, flashError },
{},
[],
[
{ type: 'requestDismissVulnerability' },
{ type: 'receiveDismissVulnerabilityError', payload: { flashError: false } },
],
done,
);
});
});
});
describe('receiveDismissVulnerabilitySuccess', () => {
it('should commit the success mutation', done => {
const state = initialState;
const data = mockDataVulnerabilities[0];
testAction(
actions.receiveDismissVulnerabilitySuccess,
{ data },
state,
[
{
type: types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS,
payload: { data },
},
],
[],
done,
);
});
});
describe('receiveDismissVulnerabilityError', () => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveDismissVulnerabilityError,
{},
state,
[{ type: types.RECEIVE_DISMISS_VULNERABILITY_ERROR }],
[],
done,
);
});
});
describe('requestDismissVulnerability', () => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestDismissVulnerability,
{},
state,
[{ type: types.REQUEST_DISMISS_VULNERABILITY }],
[],
done,
);
});
});
});
describe('undo vulnerability dismissal', () => {
describe('undoDismissal', () => {
const vulnerability = mockDataVulnerabilities[2];
const url = `${vulnerability.vulnerability_feedback_url}/${
vulnerability.dismissal_feedback.id
}`;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onDelete(url).replyOnce(200, {});
});
it('should dispatch the request and success actions', done => {
testAction(
actions.undoDismissal,
{ vulnerability },
{},
[],
[
{ type: 'requestUndoDismissal' },
{ type: 'receiveUndoDismissalSuccess', payload: { id: vulnerability.id } },
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onDelete(url).replyOnce(404, {});
});
it('should dispatch the request and error actions', done => {
const flashError = false;
testAction(
actions.undoDismissal,
{ vulnerability, flashError },
{},
[],
[
{ type: 'requestUndoDismissal' },
{ type: 'receiveUndoDismissalError', payload: { flashError: false } },
],
done,
);
});
});
});
describe('receiveUndoDismissalSuccess', () => {
it('should commit the success mutation', done => {
const state = initialState;
const data = mockDataVulnerabilities[0];
testAction(
actions.receiveUndoDismissalSuccess,
{ data },
state,
[
{
type: types.RECEIVE_UNDO_DISMISSAL_SUCCESS,
payload: { data },
},
],
[],
done,
);
});
});
describe('receiveUndoDismissalError', () => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveUndoDismissalError,
{},
state,
[{ type: types.RECEIVE_UNDO_DISMISSAL_ERROR }],
[],
done,
);
});
});
describe('requestUndoDismissal', () => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestUndoDismissal,
{},
state,
[{ type: types.REQUEST_UNDO_DISMISSAL }],
[],
done,
);
});
});
}); });
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
}, },
"dismissal_feedback": null, "dismissal_feedback": null,
"issue_feedback": null, "issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
...@@ -81,6 +82,7 @@ ...@@ -81,6 +82,7 @@
}, },
"dismissal_feedback": null, "dismissal_feedback": null,
"issue_feedback": null, "issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
...@@ -152,6 +154,7 @@ ...@@ -152,6 +154,7 @@
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4" "project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
}, },
"issue_feedback": null, "issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
...@@ -224,6 +227,7 @@ ...@@ -224,6 +227,7 @@
"branch": "master", "branch": "master",
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4" "project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
}, },
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
...@@ -318,6 +322,7 @@ ...@@ -318,6 +322,7 @@
"branch": "master", "branch": "master",
"project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4" "project_fingerprint": "4e5b6966dd100170b4b1ad599c7058cce91b57b4"
}, },
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
...@@ -367,6 +372,7 @@ ...@@ -367,6 +372,7 @@
}, },
"dismissal_feedback": null, "dismissal_feedback": null,
"issue_feedback": null, "issue_feedback": null,
"vulnerability_feedback_url": "https://example.com/vulnerability_feedback",
"description": "The cipher does not provide data integrity update 1", "description": "The cipher does not provide data integrity update 1",
"solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", "solution": "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
"location": { "location": {
......
import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state'; import createState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types'; import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import mutations from 'ee/security_dashboard/store/modules/vulnerabilities/mutations'; import mutations from 'ee/security_dashboard/store/modules/vulnerabilities/mutations';
import mockData from './data/mock_data_vulnerabilities.json';
describe('vulnerabilities module mutations', () => { describe('vulnerabilities module mutations', () => {
describe('SET_VULNERABILITIES_ENDPOINT', () => { describe('SET_VULNERABILITIES_ENDPOINT', () => {
it('should set `vulnerabilitiesEndpoint` to `fakepath.json`', () => { it('should set `vulnerabilitiesEndpoint` to `fakepath.json`', () => {
const state = initialState; const state = createState();
const endpoint = 'fakepath.json'; const endpoint = 'fakepath.json';
mutations[types.SET_VULNERABILITIES_ENDPOINT](state, endpoint); mutations[types.SET_VULNERABILITIES_ENDPOINT](state, endpoint);
...@@ -19,7 +20,7 @@ describe('vulnerabilities module mutations', () => { ...@@ -19,7 +20,7 @@ describe('vulnerabilities module mutations', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
...initialState, ...createState(),
hasError: true, hasError: true,
}; };
mutations[types.REQUEST_VULNERABILITIES](state); mutations[types.REQUEST_VULNERABILITIES](state);
...@@ -40,10 +41,10 @@ describe('vulnerabilities module mutations', () => { ...@@ -40,10 +41,10 @@ describe('vulnerabilities module mutations', () => {
beforeEach(() => { beforeEach(() => {
payload = { payload = {
vulnerabilities: [1, 2, 3, 4, 5], vulnerabilities: mockData,
pageInfo: { a: 1, b: 2, c: 3 }, pageInfo: { a: 1, b: 2, c: 3 },
}; };
state = initialState; state = createState();
mutations[types.RECEIVE_VULNERABILITIES_SUCCESS](state, payload); mutations[types.RECEIVE_VULNERABILITIES_SUCCESS](state, payload);
}); });
...@@ -62,7 +63,7 @@ describe('vulnerabilities module mutations', () => { ...@@ -62,7 +63,7 @@ describe('vulnerabilities module mutations', () => {
describe('RECEIVE_VULNERABILITIES_ERROR', () => { describe('RECEIVE_VULNERABILITIES_ERROR', () => {
it('should set `isLoadingVulnerabilities` to `false`', () => { it('should set `isLoadingVulnerabilities` to `false`', () => {
const state = initialState; const state = createState();
mutations[types.RECEIVE_VULNERABILITIES_ERROR](state); mutations[types.RECEIVE_VULNERABILITIES_ERROR](state);
...@@ -72,7 +73,7 @@ describe('vulnerabilities module mutations', () => { ...@@ -72,7 +73,7 @@ describe('vulnerabilities module mutations', () => {
describe('SET_VULNERABILITIES_COUNT_ENDPOINT', () => { describe('SET_VULNERABILITIES_COUNT_ENDPOINT', () => {
it('should set `vulnerabilitiesCountEndpoint` to `fakepath.json`', () => { it('should set `vulnerabilitiesCountEndpoint` to `fakepath.json`', () => {
const state = initialState; const state = createState();
const endpoint = 'fakepath.json'; const endpoint = 'fakepath.json';
mutations[types.SET_VULNERABILITIES_COUNT_ENDPOINT](state, endpoint); mutations[types.SET_VULNERABILITIES_COUNT_ENDPOINT](state, endpoint);
...@@ -86,7 +87,7 @@ describe('vulnerabilities module mutations', () => { ...@@ -86,7 +87,7 @@ describe('vulnerabilities module mutations', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
...initialState, ...createState(),
hasError: true, hasError: true,
}; };
mutations[types.REQUEST_VULNERABILITIES_COUNT](state); mutations[types.REQUEST_VULNERABILITIES_COUNT](state);
...@@ -106,8 +107,8 @@ describe('vulnerabilities module mutations', () => { ...@@ -106,8 +107,8 @@ describe('vulnerabilities module mutations', () => {
let state; let state;
beforeEach(() => { beforeEach(() => {
payload = { a: 1, b: 2, c: 3 }; payload = mockData;
state = initialState; state = createState();
mutations[types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS](state, payload); mutations[types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS](state, payload);
}); });
...@@ -122,11 +123,267 @@ describe('vulnerabilities module mutations', () => { ...@@ -122,11 +123,267 @@ describe('vulnerabilities module mutations', () => {
describe('RECEIVE_VULNERABILITIES_COUNT_ERROR', () => { describe('RECEIVE_VULNERABILITIES_COUNT_ERROR', () => {
it('should set `isLoadingVulnerabilitiesCount` to `false`', () => { it('should set `isLoadingVulnerabilitiesCount` to `false`', () => {
const state = initialState; const state = createState();
mutations[types.RECEIVE_VULNERABILITIES_COUNT_ERROR](state); mutations[types.RECEIVE_VULNERABILITIES_COUNT_ERROR](state);
expect(state.isLoadingVulnerabilitiesCount).toBeFalsy(); expect(state.isLoadingVulnerabilitiesCount).toBeFalsy();
}); });
}); });
describe('SET_MODAL_DATA', () => {
const vulnerability = mockData[0];
let payload;
let state;
beforeEach(() => {
state = createState();
payload = { vulnerability };
mutations[types.SET_MODAL_DATA](state, payload);
});
it('should set the modal title', () => {
expect(state.modal.title).toEqual(vulnerability.name);
});
it('should set the modal description', () => {
expect(state.modal.data.description.value).toEqual(vulnerability.description);
});
it('should set the modal project', () => {
expect(state.modal.data.project.value).toEqual(vulnerability.project.full_name);
expect(state.modal.data.project.url).toEqual(vulnerability.project.full_path);
});
it('should set the modal file', () => {
expect(state.modal.data.file.value).toEqual(vulnerability.location.file);
});
it('should set the modal identifiers', () => {
expect(state.modal.data.identifiers.value).toEqual(vulnerability.identifiers);
});
it('should set the modal severity', () => {
expect(state.modal.data.severity.value).toEqual(vulnerability.severity);
});
it('should set the modal confidence', () => {
expect(state.modal.data.confidence.value).toEqual(vulnerability.confidence);
});
it('should set the modal solution', () => {
expect(state.modal.data.solution.value).toEqual(vulnerability.solution);
});
it('should set the modal links', () => {
expect(state.modal.data.links.value).toEqual(vulnerability.links);
});
it('should set the modal instances', () => {
expect(state.modal.data.instances.value).toEqual(vulnerability.instances);
});
it('should set the modal vulnerability', () => {
expect(state.modal.vulnerability).toEqual(vulnerability);
});
});
describe('REQUEST_CREATE_ISSUE', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.REQUEST_CREATE_ISSUE](state);
});
it('should set isCreatingIssue to true', () => {
expect(state.isCreatingIssue).toBe(true);
});
it('should set isCreatingNewIssue in the modal data to true', () => {
expect(state.modal.isCreatingNewIssue).toBe(true);
});
it('should nullify the error state on the modal', () => {
expect(state.modal.error).toBeNull();
});
});
describe('RECEIVE_CREATE_ISSUE_SUCCESS', () => {
it('should fire the visitUrl function on the issue URL', () => {
const state = createState();
const payload = { issue_url: 'fakepath.html' };
const visitUrl = spyOnDependency(mutations, 'visitUrl');
mutations[types.RECEIVE_CREATE_ISSUE_SUCCESS](state, payload);
expect(visitUrl).toHaveBeenCalledWith(payload.issue_url);
});
});
describe('RECEIVE_CREATE_ISSUE_ERROR', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.RECEIVE_CREATE_ISSUE_ERROR](state);
});
it('should set isCreatingIssue to false', () => {
expect(state.isCreatingIssue).toBe(false);
});
it('should set isCreatingNewIssue in the modal data to false', () => {
expect(state.modal.isCreatingNewIssue).toBe(false);
});
it('should set the error state on the modal', () => {
expect(state.modal.error).toEqual('There was an error creating the issue');
});
});
describe('REQUEST_DISMISS_VULNERABILITY', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.REQUEST_DISMISS_VULNERABILITY](state);
});
it('should set isDismissingVulnerability to true', () => {
expect(state.isDismissingVulnerability).toBe(true);
});
it('should set isDismissingVulnerability in the modal data to true', () => {
expect(state.modal.isDismissingVulnerability).toBe(true);
});
it('should nullify the error state on the modal', () => {
expect(state.modal.error).toBeNull();
});
});
describe('RECEIVE_DISMISS_VULNERABILITY_SUCCESS', () => {
let state;
let payload;
let vulnerability;
let data;
beforeEach(() => {
state = createState();
state.vulnerabilities = mockData;
[vulnerability] = mockData;
data = { name: 'dismissal feedback' };
payload = { id: vulnerability.id, data };
mutations[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state, payload);
});
it('should set the dismissal feedback on the passed vulnerability', () => {
expect(vulnerability.dismissal_feedback).toEqual(data);
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability on the modal to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('shoulfd set isDissmissed on the modal vulnerability to be true', () => {
expect(state.modal.vulnerability.isDismissed).toBe(true);
});
});
describe('RECEIVE_DISMISS_VULNERABILITY_ERROR', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.RECEIVE_DISMISS_VULNERABILITY_ERROR](state);
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability in the modal data to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('should set the error state on the modal', () => {
expect(state.modal.error).toEqual('There was an error dismissing the vulnerability.');
});
});
describe('REQUEST_UNDO_DISMISSAL', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.REQUEST_UNDO_DISMISSAL](state);
});
it('should set isDismissingVulnerability to true', () => {
expect(state.isDismissingVulnerability).toBe(true);
});
it('should set isDismissingVulnerability in the modal data to true', () => {
expect(state.modal.isDismissingVulnerability).toBe(true);
});
it('should nullify the error state on the modal', () => {
expect(state.modal.error).toBeNull();
});
});
describe('RECEIVE_UNDO_DISMISSAL_SUCCESS', () => {
let state;
let payload;
let vulnerability;
beforeEach(() => {
state = createState();
state.vulnerabilities = mockData;
[vulnerability] = mockData;
payload = { id: vulnerability.id };
mutations[types.RECEIVE_UNDO_DISMISSAL_SUCCESS](state, payload);
});
it('should set the dismissal feedback on the passed vulnerability', () => {
expect(vulnerability.dismissal_feedback).toBeNull();
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability on the modal to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('should set isDissmissed on the modal vulnerability to be false', () => {
expect(state.modal.vulnerability.isDismissed).toBe(false);
});
});
describe('RECEIVE_UNDO_DISMISSAL_ERROR', () => {
let state;
beforeEach(() => {
state = createState();
mutations[types.RECEIVE_UNDO_DISMISSAL_ERROR](state);
});
it('should set isDismissingVulnerability to false', () => {
expect(state.isDismissingVulnerability).toBe(false);
});
it('should set isDismissingVulnerability in the modal data to false', () => {
expect(state.modal.isDismissingVulnerability).toBe(false);
});
it('should set the error state on the modal', () => {
expect(state.modal.error).toEqual('There was an error undoing the dismissal.');
});
});
}); });
...@@ -6661,13 +6661,13 @@ msgstr "" ...@@ -6661,13 +6661,13 @@ msgstr ""
msgid "Reports|%{failedString} and %{resolvedString}" msgid "Reports|%{failedString} and %{resolvedString}"
msgstr "" msgstr ""
msgid "Reports|Class" msgid "Reports|Actions"
msgstr "" msgstr ""
msgid "Reports|Confidence" msgid "Reports|Class"
msgstr "" msgstr ""
msgid "Reports|Dismiss Vulnerability" msgid "Reports|Confidence"
msgstr "" msgstr ""
msgid "Reports|Execution time" msgid "Reports|Execution time"
...@@ -6676,12 +6676,6 @@ msgstr "" ...@@ -6676,12 +6676,6 @@ msgstr ""
msgid "Reports|Failure" msgid "Reports|Failure"
msgstr "" msgstr ""
msgid "Reports|More info"
msgstr ""
msgid "Reports|New Issue"
msgstr ""
msgid "Reports|Severity" msgid "Reports|Severity"
msgstr "" msgstr ""
...@@ -7017,12 +7011,39 @@ msgstr "" ...@@ -7017,12 +7011,39 @@ msgstr ""
msgid "Security Reports|At this time, the security dashboard only supports SAST." msgid "Security Reports|At this time, the security dashboard only supports SAST."
msgstr "" msgstr ""
msgid "Security Reports|Dismiss Vulnerability"
msgstr ""
msgid "Security Reports|More info"
msgstr ""
msgid "Security Reports|New Issue"
msgstr ""
msgid "Security Reports|Security Dashboard Documentation" msgid "Security Reports|Security Dashboard Documentation"
msgstr "" msgstr ""
msgid "Security Reports|There was an error creating the issue."
msgstr ""
msgid "Security Reports|There was an error dismissing the issue."
msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerability."
msgstr ""
msgid "Security Reports|There was an error fetching the dashboard. Please try again in a few moments or contact your support team." msgid "Security Reports|There was an error fetching the dashboard. Please try again in a few moments or contact your support team."
msgstr "" msgstr ""
msgid "Security Reports|There was an error undoing the dismissal."
msgstr ""
msgid "Security Reports|There was an error undoing this dismissal."
msgstr ""
msgid "Security Reports|Undo Dismissal"
msgstr ""
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities." msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr "" msgstr ""
...@@ -8696,6 +8717,33 @@ msgstr "" ...@@ -8696,6 +8717,33 @@ msgstr ""
msgid "VisibilityLevel|Unknown" msgid "VisibilityLevel|Unknown"
msgstr "" msgstr ""
msgid "Vulnerability|Confidence"
msgstr ""
msgid "Vulnerability|Description"
msgstr ""
msgid "Vulnerability|File"
msgstr ""
msgid "Vulnerability|Identifiers"
msgstr ""
msgid "Vulnerability|Instances"
msgstr ""
msgid "Vulnerability|Links"
msgstr ""
msgid "Vulnerability|Project"
msgstr ""
msgid "Vulnerability|Severity"
msgstr ""
msgid "Vulnerability|Solution"
msgstr ""
msgid "Want to see the data? Please ask an administrator for access." msgid "Want to see the data? Please ask an administrator for access."
msgstr "" msgstr ""
...@@ -9353,6 +9401,9 @@ msgstr "" ...@@ -9353,6 +9401,9 @@ msgstr ""
msgid "from" msgid "from"
msgstr "" msgstr ""
msgid "help"
msgstr ""
msgid "here" msgid "here"
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