Commit e0ad65ab authored by Mark Florian's avatar Mark Florian Committed by Kushal Pandya

Use Security Dashboard in pipelines view

This [replaces][1] the Split Security Reports app with the Security
Dashboard in the pipelines view. This is part of a larger effort to
[move security reports logic to the backend][2].

This is behind the `pipeline_report_api` feature flag, which is
currently disabled by default.

A few other related changes were made:

- Old references to "timeline" were replaced with "history" in specs
- DRY in some specs regarding state initialisation
- Make the history and count endpoint props optional on the dashboard
  component, which, if missing/empty, disable the display of the counts
  and charts components
- Fixes a rendering discrepancy in the vulnerability modal, which was
  displaying a `File: undefined` row

[1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/13496
[2]: https://gitlab.com/groups/gitlab-org/-/epics/1425
parent 563d3d00
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import createDashboardStore from 'ee/security_dashboard/store';
import SecurityDashboardApp from 'ee/security_dashboard/components/app.vue';
import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue'; import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue';
import createStore from 'ee/vue_shared/security_reports/store'; import createStore from 'ee/vue_shared/security_reports/store';
import { updateBadgeCount } from './utils'; import { updateBadgeCount } from './utils';
Vue.use(Translate); Vue.use(Translate);
export default () => { const initSecurityDashboardApp = el => {
const securityTab = document.getElementById('js-security-report-app'); const {
dashboardDocumentation,
emptyStateSvgPath,
pipelineId,
projectId,
vulnerabilitiesEndpoint,
vulnerabilityFeedbackHelpPath,
} = el.dataset;
// They are being rendered under the same condition return new Vue({
if (securityTab) { el,
const datasetOptions = securityTab.dataset; store: createDashboardStore(),
const { render(createElement) {
headBlobPath, return createElement(SecurityDashboardApp, {
sourceBranch, props: {
sastHeadPath, dashboardDocumentation,
sastHelpPath, emptyStateSvgPath,
dependencyScanningHeadPath, lockToProject: {
dependencyScanningHelpPath, id: parseInt(projectId, 10),
vulnerabilityFeedbackPath, },
vulnerabilityFeedbackHelpPath, pipelineId: parseInt(pipelineId, 10),
createVulnerabilityFeedbackIssuePath, vulnerabilitiesEndpoint,
createVulnerabilityFeedbackMergeRequestPath, vulnerabilityFeedbackHelpPath,
createVulnerabilityFeedbackDismissalPath, },
dastHeadPath, on: {
sastContainerHeadPath, vulnerabilitiesCountChanged(count) {
dastHelpPath, updateBadgeCount('.js-security-counter', count);
sastContainerHelpPath, },
} = datasetOptions; },
const pipelineId = parseInt(datasetOptions.pipelineId, 10); });
},
});
};
const store = createStore(); const initSplitSecurityReportsApp = el => {
const datasetOptions = el.dataset;
const {
headBlobPath,
sourceBranch,
sastHeadPath,
sastHelpPath,
dependencyScanningHeadPath,
dependencyScanningHelpPath,
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
createVulnerabilityFeedbackIssuePath,
createVulnerabilityFeedbackMergeRequestPath,
createVulnerabilityFeedbackDismissalPath,
dastHeadPath,
sastContainerHeadPath,
dastHelpPath,
sastContainerHelpPath,
} = datasetOptions;
const pipelineId = parseInt(datasetOptions.pipelineId, 10);
// Tab content const store = createStore();
// eslint-disable-next-line no-new
new Vue({ return new Vue({
el: securityTab, el,
store, store,
components: { components: {
SecurityReportApp, SecurityReportApp,
}, },
render(createElement) { render(createElement) {
return createElement('security-report-app', { return createElement('security-report-app', {
props: { props: {
headBlobPath, headBlobPath,
sourceBranch, sourceBranch,
sastHeadPath, sastHeadPath,
sastHelpPath, sastHelpPath,
dependencyScanningHeadPath, dependencyScanningHeadPath,
dependencyScanningHelpPath, dependencyScanningHelpPath,
vulnerabilityFeedbackPath, vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
createVulnerabilityFeedbackIssuePath, createVulnerabilityFeedbackIssuePath,
createVulnerabilityFeedbackMergeRequestPath, createVulnerabilityFeedbackMergeRequestPath,
createVulnerabilityFeedbackDismissalPath, createVulnerabilityFeedbackDismissalPath,
pipelineId, pipelineId,
dastHeadPath, dastHeadPath,
sastContainerHeadPath, sastContainerHeadPath,
dastHelpPath, dastHelpPath,
sastContainerHelpPath, sastContainerHelpPath,
canCreateIssue: Boolean(createVulnerabilityFeedbackIssuePath), canCreateIssue: Boolean(createVulnerabilityFeedbackIssuePath),
canCreateMergeRequest: Boolean(createVulnerabilityFeedbackMergeRequestPath), canCreateMergeRequest: Boolean(createVulnerabilityFeedbackMergeRequestPath),
canDismissVulnerability: Boolean(createVulnerabilityFeedbackDismissalPath), canDismissVulnerability: Boolean(createVulnerabilityFeedbackDismissalPath),
}, },
on: { on: {
updateBadgeCount: count => { updateBadgeCount: count => {
updateBadgeCount('.js-sast-counter', count); updateBadgeCount('.js-security-counter', count);
},
}, },
}); },
}, });
}); },
});
};
export default () => {
const securityTab = document.getElementById('js-security-report-app');
if (securityTab) {
if (gon.features && gon.features.pipelineReportApi) {
initSecurityDashboardApp(securityTab);
} else {
initSplitSecurityReportsApp(securityTab);
}
} }
}; };
...@@ -23,7 +23,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -23,7 +23,6 @@ document.addEventListener('DOMContentLoaded', () => {
dashboardDocumentation, dashboardDocumentation,
emptyStateSvgPath, emptyStateSvgPath,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilitiesSummaryEndpoint, vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
securityDashboardHelpPath, securityDashboardHelpPath,
...@@ -40,7 +39,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -40,7 +39,6 @@ document.addEventListener('DOMContentLoaded', () => {
dashboardDocumentation, dashboardDocumentation,
emptyStateSvgPath, emptyStateSvgPath,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesHistoryEndpoint,
vulnerabilitiesSummaryEndpoint, vulnerabilitiesSummaryEndpoint,
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
securityDashboardHelpPath, securityDashboardHelpPath,
...@@ -80,7 +78,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -80,7 +78,6 @@ document.addEventListener('DOMContentLoaded', () => {
components: { components: {
SecurityReportApp, SecurityReportApp,
}, },
methods: {},
render(createElement) { render(createElement) {
return createElement('security-report-app', { return createElement('security-report-app', {
props, props,
......
...@@ -36,11 +36,13 @@ export default { ...@@ -36,11 +36,13 @@ export default {
}, },
vulnerabilitiesCountEndpoint: { vulnerabilitiesCountEndpoint: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
vulnerabilitiesHistoryEndpoint: { vulnerabilitiesHistoryEndpoint: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
vulnerabilityFeedbackHelpPath: { vulnerabilityFeedbackHelpPath: {
type: String, type: String,
...@@ -50,7 +52,12 @@ export default { ...@@ -50,7 +52,12 @@ export default {
type: Object, type: Object,
required: false, required: false,
default: null, default: null,
validator: project => !isUndefined(project.id) && !isUndefined(project.name), validator: project => !isUndefined(project.id),
},
pipelineId: {
type: Number,
required: false,
default: null,
}, },
}, },
computed: { computed: {
...@@ -75,6 +82,15 @@ export default { ...@@ -75,6 +82,15 @@ export default {
isLockedToProject() { isLockedToProject() {
return this.lockToProject !== null; return this.lockToProject !== null;
}, },
shouldShowChart() {
return Boolean(this.vulnerabilitiesHistoryEndpoint);
},
shouldShowCountList() {
return this.isLockedToProject && Boolean(this.vulnerabilitiesCountEndpoint);
},
},
watch: {
'pageInfo.total': 'emitVulnerabilitiesCountChanged',
}, },
created() { created() {
if (this.isLockedToProject) { if (this.isLockedToProject) {
...@@ -83,6 +99,7 @@ export default { ...@@ -83,6 +99,7 @@ export default {
optionId: this.lockToProject.id, optionId: this.lockToProject.id,
}); });
} }
this.setPipelineId(this.pipelineId);
this.setProjectsEndpoint(this.projectsEndpoint); this.setProjectsEndpoint(this.projectsEndpoint);
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint); this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint); this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
...@@ -90,9 +107,7 @@ export default { ...@@ -90,9 +107,7 @@ export default {
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page }); this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesCount(this.activeFilters); this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters); this.fetchVulnerabilitiesHistory(this.activeFilters);
if (!this.isLockedToProject) { this.fetchProjects();
this.fetchProjects();
}
}, },
methods: { methods: {
...mapActions('vulnerabilities', [ ...mapActions('vulnerabilities', [
...@@ -106,6 +121,7 @@ export default { ...@@ -106,6 +121,7 @@ export default {
'fetchVulnerabilitiesCount', 'fetchVulnerabilitiesCount',
'fetchVulnerabilitiesHistory', 'fetchVulnerabilitiesHistory',
'openDismissalCommentBox', 'openDismissalCommentBox',
'setPipelineId',
'setVulnerabilitiesCountEndpoint', 'setVulnerabilitiesCountEndpoint',
'setVulnerabilitiesEndpoint', 'setVulnerabilitiesEndpoint',
'setVulnerabilitiesHistoryEndpoint', 'setVulnerabilitiesHistoryEndpoint',
...@@ -116,6 +132,9 @@ export default { ...@@ -116,6 +132,9 @@ export default {
]), ]),
...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']), ...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
...mapActions('filters', ['lockFilter']), ...mapActions('filters', ['lockFilter']),
emitVulnerabilitiesCountChanged(count) {
this.$emit('vulnerabilitiesCountChanged', count);
},
}, },
}; };
</script> </script>
...@@ -126,7 +145,7 @@ export default { ...@@ -126,7 +145,7 @@ export default {
<filters /> <filters />
</header> </header>
<vulnerability-count-list v-if="isLockedToProject" class="mb-0" /> <vulnerability-count-list v-if="shouldShowCountList" class="mb-0" />
<div class="row mt-4"> <div class="row mt-4">
<main role="main" class="col" :class="{ 'col-xl-7': !isLockedToProject }"> <main role="main" class="col" :class="{ 'col-xl-7': !isLockedToProject }">
...@@ -136,7 +155,7 @@ export default { ...@@ -136,7 +155,7 @@ export default {
/> />
</main> </main>
<aside v-if="!isLockedToProject" class="col-xl-5"> <aside v-if="shouldShowChart" class="col-xl-5">
<vulnerability-chart /> <vulnerability-chart />
</aside> </aside>
</div> </div>
......
...@@ -27,6 +27,10 @@ export const setProjectsEndpoint = ({ commit }, endpoint) => { ...@@ -27,6 +27,10 @@ export const setProjectsEndpoint = ({ commit }, endpoint) => {
}; };
export const fetchProjects = ({ state, dispatch }) => { export const fetchProjects = ({ state, dispatch }) => {
if (!state.projectsEndpoint) {
return;
}
dispatch('requestProjects'); dispatch('requestProjects');
getAllProjects(state.projectsEndpoint) getAllProjects(state.projectsEndpoint)
......
...@@ -17,6 +17,8 @@ import createFlash from '~/flash'; ...@@ -17,6 +17,8 @@ import createFlash from '~/flash';
const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide'); const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide');
export const setPipelineId = ({ commit }, id) => commit(types.SET_PIPELINE_ID, id);
export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => { export const setVulnerabilitiesEndpoint = ({ commit }, endpoint) => {
commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint); commit(types.SET_VULNERABILITIES_ENDPOINT, endpoint);
}; };
...@@ -145,7 +147,10 @@ export const receiveCreateIssueError = ({ commit }, { flashError }) => { ...@@ -145,7 +147,10 @@ export const receiveCreateIssueError = ({ commit }, { flashError }) => {
} }
}; };
export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError, comment }) => { export const dismissVulnerability = (
{ dispatch, state },
{ vulnerability, flashError, comment },
) => {
dispatch('requestDismissVulnerability'); dispatch('requestDismissVulnerability');
axios axios
...@@ -154,6 +159,7 @@ export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError, ...@@ -154,6 +159,7 @@ export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError,
category: vulnerability.report_type, category: vulnerability.report_type,
comment, comment,
feedback_type: 'dismissal', feedback_type: 'dismissal',
pipeline_id: state.pipelineId,
project_fingerprint: vulnerability.project_fingerprint, project_fingerprint: vulnerability.project_fingerprint,
vulnerability_data: { vulnerability_data: {
...vulnerability, ...vulnerability,
...@@ -162,9 +168,8 @@ export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError, ...@@ -162,9 +168,8 @@ export const dismissVulnerability = ({ dispatch }, { vulnerability, flashError,
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
const { id } = vulnerability;
dispatch('closeDismissalCommentBox'); dispatch('closeDismissalCommentBox');
dispatch('receiveDismissVulnerabilitySuccess', { id, data }); dispatch('receiveDismissVulnerabilitySuccess', { vulnerability, data });
}) })
.catch(() => { .catch(() => {
dispatch('receiveDismissVulnerabilityError', { flashError }); dispatch('receiveDismissVulnerabilityError', { flashError });
...@@ -204,9 +209,8 @@ export const addDismissalComment = ({ dispatch }, { vulnerability, comment }) => ...@@ -204,9 +209,8 @@ export const addDismissalComment = ({ dispatch }, { vulnerability, comment }) =>
comment, comment,
}) })
.then(({ data }) => { .then(({ data }) => {
const { id } = vulnerability;
dispatch('closeDismissalCommentBox'); dispatch('closeDismissalCommentBox');
dispatch('receiveAddDismissalCommentSuccess', { id, data }); dispatch('receiveAddDismissalCommentSuccess', { vulnerability, data });
}) })
.catch(() => { .catch(() => {
dispatch('receiveAddDismissalCommentError'); dispatch('receiveAddDismissalCommentError');
...@@ -276,8 +280,7 @@ export const undoDismiss = ({ dispatch }, { vulnerability, flashError }) => { ...@@ -276,8 +280,7 @@ export const undoDismiss = ({ dispatch }, { vulnerability, flashError }) => {
axios axios
.delete(destroy_vulnerability_feedback_dismissal_path) .delete(destroy_vulnerability_feedback_dismissal_path)
.then(() => { .then(() => {
const { id } = vulnerability; dispatch('receiveUndoDismissSuccess', { vulnerability });
dispatch('receiveUndoDismissSuccess', { id });
}) })
.catch(() => { .catch(() => {
dispatch('receiveUndoDismissError', { flashError }); dispatch('receiveUndoDismissError', { flashError });
......
export const SET_PIPELINE_ID = 'SET_PIPELINE_ID';
export const SET_VULNERABILITIES_ENDPOINT = 'SET_VULNERABILITIES_ENDPOINT'; export const SET_VULNERABILITIES_ENDPOINT = 'SET_VULNERABILITIES_ENDPOINT';
export const SET_VULNERABILITIES_PAGE = 'SET_VULNERABILITIES_PAGE'; export const SET_VULNERABILITIES_PAGE = 'SET_VULNERABILITIES_PAGE';
export const REQUEST_VULNERABILITIES = 'REQUEST_VULNERABILITIES'; export const REQUEST_VULNERABILITIES = 'REQUEST_VULNERABILITIES';
......
...@@ -3,8 +3,12 @@ import { s__, __ } from '~/locale'; ...@@ -3,8 +3,12 @@ import { s__, __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { DAYS } from './constants'; import { DAYS } from './constants';
import { isSameVulnerability } from './utils';
export default { export default {
[types.SET_PIPELINE_ID](state, payload) {
state.pipelineId = payload;
},
[types.SET_VULNERABILITIES_ENDPOINT](state, payload) { [types.SET_VULNERABILITIES_ENDPOINT](state, payload) {
state.vulnerabilitiesEndpoint = payload; state.vulnerabilitiesEndpoint = payload;
}, },
...@@ -106,7 +110,7 @@ export default { ...@@ -106,7 +110,7 @@ export default {
} }
Vue.set(state.modal.data.className, 'value', className); Vue.set(state.modal.data.className, 'value', className);
Vue.set(state.modal.data.file, 'value', `${file}${lineSuffix}`); Vue.set(state.modal.data.file, 'value', file ? `${file}${lineSuffix}` : null);
Vue.set(state.modal.data.image, 'value', image); Vue.set(state.modal.data.image, 'value', image);
Vue.set(state.modal.data.namespace, 'value', namespace); Vue.set(state.modal.data.namespace, 'value', namespace);
} }
...@@ -164,7 +168,9 @@ export default { ...@@ -164,7 +168,9 @@ export default {
Vue.set(state.modal, 'error', null); Vue.set(state.modal, 'error', null);
}, },
[types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state, payload) { [types.RECEIVE_DISMISS_VULNERABILITY_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id); const vulnerability = state.vulnerabilities.find(vuln =>
isSameVulnerability(vuln, payload.vulnerability),
);
vulnerability.dismissal_feedback = payload.data; vulnerability.dismissal_feedback = payload.data;
state.isDismissingVulnerability = false; state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false); Vue.set(state.modal, 'isDismissingVulnerability', false);
...@@ -185,7 +191,9 @@ export default { ...@@ -185,7 +191,9 @@ export default {
Vue.set(state.modal, 'error', null); Vue.set(state.modal, 'error', null);
}, },
[types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](state, payload) { [types.RECEIVE_ADD_DISMISSAL_COMMENT_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id); const vulnerability = state.vulnerabilities.find(vuln =>
isSameVulnerability(vuln, payload.vulnerability),
);
if (vulnerability) { if (vulnerability) {
vulnerability.dismissal_feedback = payload.data; vulnerability.dismissal_feedback = payload.data;
state.isDismissingVulnerability = false; state.isDismissingVulnerability = false;
...@@ -223,7 +231,9 @@ export default { ...@@ -223,7 +231,9 @@ export default {
Vue.set(state.modal, 'error', null); Vue.set(state.modal, 'error', null);
}, },
[types.RECEIVE_REVERT_DISMISSAL_SUCCESS](state, payload) { [types.RECEIVE_REVERT_DISMISSAL_SUCCESS](state, payload) {
const vulnerability = state.vulnerabilities.find(vuln => vuln.id === payload.id); const vulnerability = state.vulnerabilities.find(vuln =>
isSameVulnerability(vuln, payload.vulnerability),
);
vulnerability.dismissal_feedback = null; vulnerability.dismissal_feedback = null;
state.isDismissingVulnerability = false; state.isDismissingVulnerability = false;
Vue.set(state.modal, 'isDismissingVulnerability', false); Vue.set(state.modal, 'isDismissingVulnerability', false);
......
...@@ -13,6 +13,7 @@ export default () => ({ ...@@ -13,6 +13,7 @@ export default () => ({
vulnerabilitiesHistoryDayRange: 90, vulnerabilitiesHistoryDayRange: 90,
vulnerabilitiesHistoryMaxDayInterval: 7, vulnerabilitiesHistoryMaxDayInterval: 7,
pageInfo: {}, pageInfo: {},
pipelineId: null,
vulnerabilitiesCountEndpoint: null, vulnerabilitiesCountEndpoint: null,
vulnerabilitiesHistoryEndpoint: null, vulnerabilitiesHistoryEndpoint: null,
vulnerabilitiesEndpoint: null, vulnerabilitiesEndpoint: null,
......
/* eslint-disable import/prefer-default-export */
import { isEqual } from 'underscore';
const isVulnerabilityLike = object =>
Boolean(object && object.location && object.identifiers && object.identifiers[0]);
/**
* Determines whether the provided objects represent the same vulnerability.
* @param {Object} vulnerability A vulnerability occurrence
* @param {Object} other Another vulnerability occurrence
* @returns {boolean}
*/
export const isSameVulnerability = (vulnerability, other) => {
if (!isVulnerabilityLike(vulnerability) || !isVulnerabilityLike(other)) {
return false;
}
// The `[location_fingerprint, identifiers[0]]` tuple is currently the most
// correct/robust set of data to compare to see if two objects represent the
// same vulnerability[1]. Unfortunately, `location_fingerprint` isn't exposed
// by the API yet, so we fall back to a slower deep equality comparison on
// `location` (which is a superset of `location_fingerprint`) if the former
// isn't present.
//
// [1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/7586
let isLocationEqual = false;
if (vulnerability.location_fingerprint && other.location_fingerprint) {
isLocationEqual = vulnerability.location_fingerprint === other.location_fingerprint;
} else {
isLocationEqual = isEqual(vulnerability.location, other.location);
}
return isLocationEqual && isEqual(vulnerability.identifiers[0], other.identifiers[0]);
};
...@@ -81,11 +81,6 @@ export default { ...@@ -81,11 +81,6 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
vulnerabilitiesHistoryEndpoint: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
headline() { headline() {
...@@ -141,7 +136,6 @@ export default { ...@@ -141,7 +136,6 @@ export default {
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint" :vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:vulnerabilities-count-endpoint="vulnerabilitiesSummaryEndpoint" :vulnerabilities-count-endpoint="vulnerabilitiesSummaryEndpoint"
:vulnerabilities-history-endpoint="vulnerabilitiesHistoryEndpoint"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath" :vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/> />
</template> </template>
......
...@@ -11,22 +11,30 @@ ...@@ -11,22 +11,30 @@
- if pipeline.expose_security_dashboard? - if pipeline.expose_security_dashboard?
#js-tab-security.build-security.tab-pane #js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { head_blob_path: blob_path, - if Feature.enabled?(:pipeline_report_api)
sast_head_path: sast_endpoint, #js-security-report-app{ data: { dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
dependency_scanning_head_path: dependency_scanning_endpoint, empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dast_head_path: dast_endpoint, pipeline_id: pipeline.id,
sast_container_head_path: sast_container_endpoint, project_id: project.id,
pipeline_id: pipeline.id, vulnerabilities_endpoint: expose_path(api_v4_projects_vulnerabilities_path(id: project.id, params: { pipeline_id: pipeline.id, scope: 'all' })),
source_branch: pipeline.ref, vulnerability_feedback_help_path: help_page_path('user/application_security/index') } }
vulnerability_feedback_path: project_vulnerability_feedback_index_path(project), - else
vulnerability_feedback_help_path: help_page_path('user/application_security/index'), #js-security-report-app{ data: { head_blob_path: blob_path,
sast_help_path: help_page_path('user/application_security/sast/index'), sast_head_path: sast_endpoint,
dependency_scanning_help_path: help_page_path('user/application_security/dependency_scanning/index'), dependency_scanning_head_path: dependency_scanning_endpoint,
dast_help_path: help_page_path('user/application_security/dast/index'), dast_head_path: dast_endpoint,
sast_container_help_path: help_page_path('user/application_security/container_scanning/index'), sast_container_head_path: sast_container_endpoint,
create_vulnerability_feedback_issue_path: create_vulnerability_feedback_issue_path(project), pipeline_id: pipeline.id,
create_vulnerability_feedback_merge_request_path: create_vulnerability_feedback_merge_request_path(project), source_branch: pipeline.ref,
create_vulnerability_feedback_dismissal_path: create_vulnerability_feedback_dismissal_path(project) } } vulnerability_feedback_path: project_vulnerability_feedback_index_path(project),
vulnerability_feedback_help_path: help_page_path('user/application_security/index'),
sast_help_path: help_page_path('user/application_security/sast/index'),
dependency_scanning_help_path: help_page_path('user/application_security/dependency_scanning/index'),
dast_help_path: help_page_path('user/application_security/dast/index'),
sast_container_help_path: help_page_path('user/application_security/container_scanning/index'),
create_vulnerability_feedback_issue_path: create_vulnerability_feedback_issue_path(project),
create_vulnerability_feedback_merge_request_path: create_vulnerability_feedback_merge_request_path(project),
create_vulnerability_feedback_dismissal_path: create_vulnerability_feedback_dismissal_path(project) } }
- if pipeline.expose_license_management_data? - if pipeline.expose_license_management_data?
#js-tab-licenses.tab-pane #js-tab-licenses.tab-pane
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%li.js-security-tab-link %li.js-security-tab-link
= link_to security_project_pipeline_path(project, pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab qa-security-tab' do = link_to security_project_pipeline_path(project, pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab qa-security-tab' do
= _("Security") = _("Security")
%span.badge.badge-pill.js-sast-counter.hidden %span.badge.badge-pill.js-security-counter.hidden
- if pipeline.expose_license_management_data? - if pipeline.expose_license_management_data?
%li.js-licenses-tab-link %li.js-licenses-tab-link
......
...@@ -99,17 +99,37 @@ describe 'Pipeline', :js do ...@@ -99,17 +99,37 @@ describe 'Pipeline', :js do
context 'with a sast artifact' do context 'with a sast artifact' do
before do before do
create(:ee_ci_build, :sast, pipeline: pipeline) create(:ee_ci_build, :sast, pipeline: pipeline)
visit security_project_pipeline_path(project, pipeline)
end end
it 'shows jobs tab pane as active' do context 'when feature flag is enabled' do
expect(page).to have_content('Security') before do
expect(page).to have_css('#js-tab-security') visit security_project_pipeline_path(project, pipeline)
end
it 'shows jobs tab pane as active' do
expect(page).to have_content('Security')
expect(page).to have_css('#js-tab-security')
end
it 'shows security dashboard' do
expect(page).to have_css('.js-security-dashboard-table')
end
end end
it 'shows security report section' do context 'when feature flag is disabled' do
expect(page).to have_content('SAST is loading') before do
stub_feature_flags(pipeline_report_api: false)
visit security_project_pipeline_path(project, pipeline)
end
it 'shows jobs tab pane as active' do
expect(page).to have_content('Security')
expect(page).to have_css('#js-tab-security')
end
it 'shows security report section' do
expect(page).to have_content('SAST is loading')
end
end end
end end
......
...@@ -13,31 +13,37 @@ import createStore from 'ee/security_dashboard/store'; ...@@ -13,31 +13,37 @@ import createStore from 'ee/security_dashboard/store';
const localVue = createLocalVue(); const localVue = createLocalVue();
const pipelineId = 123;
const projectsEndpoint = `${TEST_HOST}/projects`; const projectsEndpoint = `${TEST_HOST}/projects`;
const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`; const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_summary`; const vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`; const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
describe('Card security reports app', () => { describe('Security Dashboard app', () => {
let wrapper; let wrapper;
let mock; let mock;
let fetchProjectsSpy; let fetchProjectsSpy;
let lockFilterSpy; let lockFilterSpy;
let setPipelineIdSpy;
let store;
const setup = () => { const setup = () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
fetchProjectsSpy = jest.fn(); fetchProjectsSpy = jest.fn();
lockFilterSpy = jest.fn(); lockFilterSpy = jest.fn();
setPipelineIdSpy = jest.fn();
}; };
const createComponent = props => { const createComponent = props => {
store = createStore();
wrapper = shallowMount(SecurityDashboardApp, { wrapper = shallowMount(SecurityDashboardApp, {
localVue, localVue,
store: createStore(), store,
sync: false, sync: false,
methods: { methods: {
lockFilter: lockFilterSpy, lockFilter: lockFilterSpy,
fetchProjects: fetchProjectsSpy, fetchProjects: fetchProjectsSpy,
setPipelineId: setPipelineIdSpy,
}, },
propsData: { propsData: {
dashboardDocumentation: '', dashboardDocumentation: '',
...@@ -46,6 +52,7 @@ describe('Card security reports app', () => { ...@@ -46,6 +52,7 @@ describe('Card security reports app', () => {
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint, vulnerabilitiesCountEndpoint,
vulnerabilitiesHistoryEndpoint, vulnerabilitiesHistoryEndpoint,
pipelineId,
vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerabilities_feedback_help`, vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerabilities_feedback_help`,
...props, ...props,
}, },
...@@ -90,12 +97,27 @@ describe('Card security reports app', () => { ...@@ -90,12 +97,27 @@ describe('Card security reports app', () => {
it('does not lock project filters', () => { it('does not lock project filters', () => {
expect(lockFilterSpy).not.toHaveBeenCalled(); expect(lockFilterSpy).not.toHaveBeenCalled();
}); });
it('sets the pipeline id', () => {
expect(setPipelineIdSpy).toHaveBeenCalledWith(pipelineId);
});
describe('when the total number of vulnerabilities change', () => {
const newCount = 3;
beforeEach(() => {
localVue.set(store.state.vulnerabilities.pageInfo, 'total', newCount);
});
it('emits a vulnerabilitiesCountChanged event', () => {
expect(wrapper.emitted('vulnerabilitiesCountChanged')).toEqual([[newCount]]);
});
});
}); });
describe('with project lock', () => { describe('with project lock', () => {
const project = { const project = {
id: 123, id: 123,
name: 'my-project',
}; };
beforeEach(() => { beforeEach(() => {
setup(); setup();
...@@ -112,8 +134,8 @@ describe('Card security reports app', () => { ...@@ -112,8 +134,8 @@ describe('Card security reports app', () => {
expect(wrapper.vm.isLockedToProject).toBe(true); expect(wrapper.vm.isLockedToProject).toBe(true);
}); });
it('does not fetch projects', () => { it('fetches projects', () => {
expect(fetchProjectsSpy).not.toHaveBeenCalled(); expect(fetchProjectsSpy).toHaveBeenCalled();
}); });
it('locks the filters to a given project', () => { it('locks the filters to a given project', () => {
...@@ -123,4 +145,21 @@ describe('Card security reports app', () => { ...@@ -123,4 +145,21 @@ describe('Card security reports app', () => {
}); });
}); });
}); });
describe.each`
endpointProp | Component
${'vulnerabilitiesCountEndpoint'} | ${VulnerabilityCountList}
${'vulnerabilitiesHistoryEndpoint'} | ${VulnerabilityChart}
`('with an empty $endpointProp', ({ endpointProp, Component }) => {
beforeEach(() => {
setup();
createComponent({
[endpointProp]: '',
});
});
it(`does not show the ${Component.name}`, () => {
expect(wrapper.find(Component).exists()).toBe(false);
});
});
}); });
import { isSameVulnerability } from 'ee/security_dashboard/store/modules/vulnerabilities/utils';
import mockData from '../../../../javascripts/security_dashboard/store/vulnerabilities/data/mock_data_vulnerabilities.json';
describe('Vulnerabilities utils', () => {
const clone = serializable => JSON.parse(JSON.stringify(serializable));
const vuln = clone(mockData[0]);
const vulnWithNewLocation = { ...clone(vuln), location: { foo: 1 } };
const vulnWithNewIdentifier = { ...clone(vuln), identifiers: [{ foo: 1 }] };
describe('isSameVulnerability', () => {
describe.each`
description | vulnerability | other | result
${'identical vulnerabilities'} | ${vuln} | ${vuln} | ${true}
${'cloned vulnerabilities'} | ${vuln} | ${clone(vuln)} | ${true}
${'different locations'} | ${vuln} | ${vulnWithNewLocation} | ${false}
${'different primary identifiers'} | ${vuln} | ${vulnWithNewIdentifier} | ${false}
${'cloned non-vulnerabilities'} | ${{ foo: 1 }} | ${{ foo: 1 }} | ${false}
${'null values'} | ${null} | ${null} | ${false}
${'undefined values'} | ${undefined} | ${undefined} | ${false}
`('given $description', ({ vulnerability, other, result }) => {
it(`returns ${result}`, () => {
expect(isSameVulnerability(vulnerability, other)).toBe(result);
});
});
});
});
...@@ -12,7 +12,6 @@ const localVue = createLocalVue(); ...@@ -12,7 +12,6 @@ const localVue = createLocalVue();
const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`; const vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities`;
const vulnerabilitiesSummaryEndpoint = `${TEST_HOST}/vulnerabilities_summary`; const vulnerabilitiesSummaryEndpoint = `${TEST_HOST}/vulnerabilities_summary`;
const vulnerabilitiesHistoryEndpoint = `${TEST_HOST}/vulnerabilities_history`;
describe('Card security reports app', () => { describe('Card security reports app', () => {
let wrapper; let wrapper;
...@@ -58,7 +57,6 @@ describe('Card security reports app', () => { ...@@ -58,7 +57,6 @@ describe('Card security reports app', () => {
vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerability_feedback_help`, vulnerabilityFeedbackHelpPath: `${TEST_HOST}/vulnerability_feedback_help`,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
vulnerabilitiesSummaryEndpoint, vulnerabilitiesSummaryEndpoint,
vulnerabilitiesHistoryEndpoint,
...props, ...props,
}, },
}); });
......
...@@ -92,6 +92,16 @@ describe('projects actions', () => { ...@@ -92,6 +92,16 @@ describe('projects actions', () => {
); );
}); });
}); });
describe('with an empty endpoint', () => {
beforeEach(() => {
state.projectsEndpoint = '';
});
it('should not do anything', done => {
testAction(actions.fetchProjects, {}, state, [], [], done);
});
});
}); });
describe('receiveProjectsSuccess', () => { describe('receiveProjectsSuccess', () => {
......
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