Commit e98aebb3 authored by Dheeraj Joshi's avatar Dheeraj Joshi Committed by Natalia Tepluhina

Integrate site validation in profile library

 * Add site validation status column
 * Add validation buttons
 * Integrate validation modal
parent ac4588fe
...@@ -261,6 +261,7 @@ export default { ...@@ -261,6 +261,7 @@ export default {
:profiles-per-page="$options.profilesPerPage" :profiles-per-page="$options.profilesPerPage"
:profiles="profileTypes[profileType].profiles" :profiles="profileTypes[profileType].profiles"
:fields="settings.tableFields" :fields="settings.tableFields"
:full-path="projectFullPath"
@load-more-profiles="fetchMoreProfiles(profileType)" @load-more-profiles="fetchMoreProfiles(profileType)"
@delete-profile="deleteProfile(profileType, $event)" @delete-profile="deleteProfile(profileType, $event)"
/> />
......
...@@ -9,6 +9,14 @@ import { ...@@ -9,6 +9,14 @@ import {
GlTable, GlTable,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import DastSiteValidationModal from 'ee/security_configuration/dast_site_validation/components/dast_site_validation_modal.vue';
import {
DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_STATUS_PROPS,
} from 'ee/security_configuration/dast_site_validation/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const { PENDING, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default { export default {
components: { components: {
...@@ -18,10 +26,12 @@ export default { ...@@ -18,10 +26,12 @@ export default {
GlModal, GlModal,
GlSkeletonLoader, GlSkeletonLoader,
GlTable, GlTable,
DastSiteValidationModal,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
profiles: { profiles: {
type: Array, type: Array,
...@@ -55,12 +65,18 @@ export default { ...@@ -55,12 +65,18 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
fullPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
toBeDeletedProfileId: null, toBeDeletedProfileId: null,
validatingProfile: null,
}; };
}, },
statuses: DAST_SITE_VALIDATION_STATUS_PROPS,
computed: { computed: {
hasError() { hasError() {
return this.errorMessage !== ''; return this.errorMessage !== '';
...@@ -99,6 +115,24 @@ export default { ...@@ -99,6 +115,24 @@ export default {
handleCancel() { handleCancel() {
this.toBeDeletedProfileId = null; this.toBeDeletedProfileId = null;
}, },
shouldShowValidationBtn(status) {
return (
this.glFeatures.securityOnDemandScansSiteValidation &&
(status === PENDING || status === FAILED)
);
},
shouldShowValidationStatus(status) {
return this.glFeatures.securityOnDemandScansSiteValidation && status !== PENDING;
},
showValidationModal() {
this.$refs['dast-site-validation-modal'].show();
},
setValidatingProfile(profile) {
this.validatingProfile = profile;
this.$nextTick(() => {
this.showValidationModal();
});
},
}, },
}; };
</script> </script>
...@@ -133,28 +167,44 @@ export default { ...@@ -133,28 +167,44 @@ export default {
</template> </template>
<template #cell(validationStatus)="{ value }"> <template #cell(validationStatus)="{ value }">
<span> <template v-if="shouldShowValidationStatus(value)">
<span :class="$options.statuses[value].cssClass">
{{ $options.statuses[value].label }}
</span>
<gl-icon <gl-icon
:size="16" v-gl-tooltip
class="gl-vertical-align-text-bottom gl-text-gray-600" name="question-o"
name="information-o" class="gl-vertical-align-text-bottom gl-text-gray-300 gl-ml-2"
:title="$options.statuses[value].tooltipText"
/> />
{{ value }} </template>
</span>
</template> </template>
<template #cell(actions)="{ item }"> <template #cell(actions)="{ item }">
<div class="gl-text-right"> <div class="gl-text-right">
<gl-button
v-if="shouldShowValidationBtn(item.validationStatus)"
variant="info"
category="secondary"
size="small"
@click="setValidatingProfile(item)"
>{{ s__('DastSiteValidation|Validate target site') }}</gl-button
>
<gl-button v-if="item.editPath" :href="item.editPath" class="gl-mx-5" size="small">{{
__('Edit')
}}</gl-button>
<gl-button <gl-button
v-gl-tooltip.hover.focus v-gl-tooltip.hover.focus
icon="remove" icon="remove"
variant="danger" variant="danger"
category="secondary" category="secondary"
size="small"
class="gl-mr-3" class="gl-mr-3"
:title="s__('DastProfiles|Delete profile')" :title="s__('DastProfiles|Delete profile')"
@click="prepareProfileDeletion(item.id)" @click="prepareProfileDeletion(item.id)"
/> />
<gl-button v-if="item.editPath" :href="item.editPath">{{ __('Edit') }}</gl-button>
</div> </div>
</template> </template>
...@@ -197,5 +247,12 @@ export default { ...@@ -197,5 +247,12 @@ export default {
@ok="handleDelete" @ok="handleDelete"
@cancel="handleCancel" @cancel="handleCancel"
/> />
<dast-site-validation-modal
v-if="validatingProfile"
ref="dast-site-validation-modal"
:full-path="fullPath"
:target-url="validatingProfile.targetUrl"
/>
</section> </section>
</template> </template>
...@@ -19,7 +19,7 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({ ...@@ -19,7 +19,7 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
}), }),
}, },
}, },
tableFields: ['profileName', 'targetUrl'], tableFields: ['profileName', 'targetUrl', 'validationStatus'],
i18n: { i18n: {
createNewLinkText: s__('DastProfiles|Site Profile'), createNewLinkText: s__('DastProfiles|Site Profile'),
tabName: s__('DastProfiles|Site Profiles'), tabName: s__('DastProfiles|Site Profiles'),
......
...@@ -139,6 +139,9 @@ export default { ...@@ -139,6 +139,9 @@ export default {
); );
}, },
methods: { methods: {
show() {
this.$refs.modal.show();
},
updateValidationPath() { updateValidationPath() {
this.validationPath = this.isTextFileValidation this.validationPath = this.isTextFileValidation
? this.getTextFileValidationPath() ? this.getTextFileValidationPath()
......
...@@ -27,6 +27,26 @@ export const DAST_SITE_VALIDATION_STATUS = { ...@@ -27,6 +27,26 @@ export const DAST_SITE_VALIDATION_STATUS = {
FAILED: 'FAILED_VALIDATION', FAILED: 'FAILED_VALIDATION',
}; };
export const DAST_SITE_VALIDATION_STATUS_PROPS = {
[DAST_SITE_VALIDATION_STATUS.INPROGRESS]: {
label: s__('DastSiteValidation|Validating...'),
cssClass: 'gl-text-blue-300',
tooltipText: s__('DastSiteValidation|The validation is in progress. Please wait...'),
},
[DAST_SITE_VALIDATION_STATUS.PASSED]: {
label: s__('DastSiteValidation|Validated'),
cssClass: 'gl-text-green-500',
tooltipText: s__(
'DastSiteValidation|Validation succeeded. Both active and passive scans can be run against the target site.',
),
},
[DAST_SITE_VALIDATION_STATUS.FAILED]: {
label: s__('DastSiteValidation|Validation failed'),
cssClass: 'gl-text-red-500',
tooltipText: s__('DastSiteValidation|The validation has failed. Please try again.'),
},
};
export const DAST_SITE_VALIDATION_HTTP_HEADER_KEY = 'Gitlab-On-Demand-DAST'; export const DAST_SITE_VALIDATION_HTTP_HEADER_KEY = 'Gitlab-On-Demand-DAST';
export const DAST_SITE_VALIDATION_MODAL_ID = 'dast-site-validation-modal'; export const DAST_SITE_VALIDATION_MODAL_ID = 'dast-site-validation-modal';
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
module Projects module Projects
module Security module Security
class DastProfilesController < Projects::ApplicationController class DastProfilesController < Projects::ApplicationController
before_action :authorize_read_on_demand_scans! before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project)
push_frontend_feature_flag(:security_on_demand_scans_http_header_validation, @project)
end
feature_category :dynamic_application_security_testing feature_category :dynamic_application_security_testing
......
...@@ -3,11 +3,7 @@ ...@@ -3,11 +3,7 @@
module Projects module Projects
module Security module Security
class DastSiteProfilesController < Projects::ApplicationController class DastSiteProfilesController < Projects::ApplicationController
before_action do before_action :authorize_read_on_demand_scans!
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project)
push_frontend_feature_flag(:security_on_demand_scans_http_header_validation, @project)
end
feature_category :dynamic_application_security_testing feature_category :dynamic_application_security_testing
......
---
title: Swap edit and delete button for DAST Profile library
merge_request: 48124
author:
type: changed
...@@ -4,6 +4,7 @@ import { mount, shallowMount, createWrapper } from '@vue/test-utils'; ...@@ -4,6 +4,7 @@ import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import DastProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue'; import DastProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { siteProfiles as profiles } from './mock_data';
const TEST_ERROR_MESSAGE = 'something went wrong'; const TEST_ERROR_MESSAGE = 'something went wrong';
...@@ -18,6 +19,7 @@ describe('EE - DastProfilesList', () => { ...@@ -18,6 +19,7 @@ describe('EE - DastProfilesList', () => {
profilesPerPage: 10, profilesPerPage: 10,
errorMessage: '', errorMessage: '',
errorDetails: [], errorDetails: [],
fullPath: '/namespace/project',
}; };
wrapper = mountFn( wrapper = mountFn(
...@@ -25,6 +27,9 @@ describe('EE - DastProfilesList', () => { ...@@ -25,6 +27,9 @@ describe('EE - DastProfilesList', () => {
merge( merge(
{}, {},
{ {
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
propsData: defaultProps, propsData: defaultProps,
}, },
options, options,
...@@ -108,30 +113,6 @@ describe('EE - DastProfilesList', () => { ...@@ -108,30 +113,6 @@ describe('EE - DastProfilesList', () => {
}); });
describe('with existing profiles', () => { describe('with existing profiles', () => {
const profiles = [
{
id: 1,
profileName: 'Profile 1',
targetUrl: 'http://example-1.com',
editPath: '/1/edit',
validationStatus: 'Pending',
},
{
id: 2,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/2/edit',
validationStatus: 'Pending',
},
{
id: 3,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/3/edit',
validationStatus: 'Pending',
},
];
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)]; const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
describe('profiles list', () => { describe('profiles list', () => {
...@@ -149,22 +130,64 @@ describe('EE - DastProfilesList', () => { ...@@ -149,22 +130,64 @@ describe('EE - DastProfilesList', () => {
}); });
it.each(profiles)('renders list item %# correctly', profile => { it.each(profiles)('renders list item %# correctly', profile => {
const [ const [profileCell, targetUrlCell, , actionsCell] = getTableRowForProfile(profile).cells;
profileCell,
targetUrlCell,
validationStatusCell,
actionsCell,
] = getTableRowForProfile(profile).cells;
expect(profileCell.innerText).toContain(profile.profileName); expect(profileCell.innerText).toContain(profile.profileName);
expect(targetUrlCell.innerText).toContain(profile.targetUrl); expect(targetUrlCell.innerText).toContain(profile.targetUrl);
expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null); expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null);
const editLink = within(actionsCell).getByRole('link', { name: /edit/i }); const editLink = within(actionsCell).getByRole('link', { name: /edit/i });
expect(editLink).not.toBe(null); expect(editLink).not.toBe(null);
expect(editLink.getAttribute('href')).toBe(profile.editPath); expect(editLink.getAttribute('href')).toBe(profile.editPath);
}); });
describe('with site validation enabled', () => {
describe.each`
status | statusEnum | label | hasValidateButton
${'pending'} | ${'PENDING_VALIDATION'} | ${''} | ${true}
${'in-progress'} | ${'INPROGRESS_VALIDATION'} | ${'Validating...'} | ${false}
${'passed'} | ${'PASSED_VALIDATION'} | ${'Validated'} | ${false}
${'failed'} | ${'FAILED_VALIDATION'} | ${'Validation failed'} | ${true}
`('profile with validation $status', ({ statusEnum, label, hasValidateButton }) => {
const profile = profiles.find(({ validationStatus }) => validationStatus === statusEnum);
it(`should show correct label`, () => {
const validationStatusCell = getTableRowForProfile(profile).cells[2];
expect(validationStatusCell.innerText).toContain(label);
});
it(`should ${hasValidateButton ? '' : 'not '}render validate button`, () => {
const actionsCell = getTableRowForProfile(profile).cells[3];
const validateButton = within(actionsCell).queryByRole('button', {
name: /validate/i,
});
if (hasValidateButton) {
expect(validateButton).not.toBeNull();
} else {
expect(validateButton).toBeNull();
}
});
});
});
describe('without site validation enabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: { profiles },
});
});
it.each(profiles)('profile %# should not have validate button and status', profile => {
const [, , validationStatusCell, actionsCell] = getTableRowForProfile(profile).cells;
expect(within(actionsCell).queryByRole('button', { name: /validate/i })).toBe(null);
expect(validationStatusCell.innerText).toBe('');
});
});
}); });
describe('load more profiles', () => { describe('load more profiles', () => {
......
...@@ -180,6 +180,7 @@ describe('EE - DastProfiles', () => { ...@@ -180,6 +180,7 @@ describe('EE - DastProfiles', () => {
profilesPerPage: expect.any(Number), profilesPerPage: expect.any(Number),
profiles: [], profiles: [],
fields: expect.any(Array), fields: expect.any(Array),
fullPath: '/namespace/project',
}); });
}); });
......
export const siteProfiles = [
{
id: 1,
profileName: 'Profile 1',
targetUrl: 'http://example-1.com',
editPath: '/1/edit',
validationStatus: 'PENDING_VALIDATION',
},
{
id: 2,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/2/edit',
validationStatus: 'INPROGRESS_VALIDATION',
},
{
id: 3,
profileName: 'Profile 3',
targetUrl: 'http://example-2.com',
editPath: '/3/edit',
validationStatus: 'PASSED_VALIDATION',
},
{
id: 4,
profileName: 'Profile 4',
targetUrl: 'http://example-3.com',
editPath: '/3/edit',
validationStatus: 'FAILED_VALIDATION',
},
];
...@@ -90,6 +90,25 @@ describe('DastSiteValidationModal', () => { ...@@ -90,6 +90,25 @@ describe('DastSiteValidationModal', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it("calls GlModal's show method when own show method is called", () => {
const showMock = jest.fn();
createComponent({
mountOptions: {
stubs: {
GlModal: {
render: () => {},
methods: {
show: showMock,
},
},
},
},
});
wrapper.vm.show();
expect(showMock).toHaveBeenCalled();
});
describe('rendering', () => { describe('rendering', () => {
describe('loading', () => { describe('loading', () => {
beforeEach(() => { beforeEach(() => {
......
...@@ -8674,12 +8674,30 @@ msgstr "" ...@@ -8674,12 +8674,30 @@ msgstr ""
msgid "DastSiteValidation|Text file validation" msgid "DastSiteValidation|Text file validation"
msgstr "" msgstr ""
msgid "DastSiteValidation|The validation has failed. Please try again."
msgstr ""
msgid "DastSiteValidation|The validation is in progress. Please wait..."
msgstr ""
msgid "DastSiteValidation|Validate" msgid "DastSiteValidation|Validate"
msgstr "" msgstr ""
msgid "DastSiteValidation|Validate target site" msgid "DastSiteValidation|Validate target site"
msgstr "" msgstr ""
msgid "DastSiteValidation|Validated"
msgstr ""
msgid "DastSiteValidation|Validating..."
msgstr ""
msgid "DastSiteValidation|Validation failed"
msgstr ""
msgid "DastSiteValidation|Validation succeeded. Both active and passive scans can be run against the target site."
msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
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