Commit 5d7a9bc4 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Mikołaj Wawrzyniak

Add DAST Saved Scans tab

Adds the "Saved Scans" tab to the DAST profiles library.
parent c08f828f
......@@ -2,10 +2,11 @@
import { GlDropdown, GlDropdownItem, GlTab, GlTabs } from '@gitlab/ui';
import { camelCase, kebabCase } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { getLocationHash } from '~/lib/utils/url_utility';
import * as cacheUtils from '../graphql/cache_utils';
import { getProfileSettings } from '../settings/profiles';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
......@@ -14,6 +15,7 @@ export default {
GlTab,
GlTabs,
},
mixins: [glFeatureFlagsMixin()],
props: {
createNewProfilePaths: {
type: Object,
......@@ -37,6 +39,7 @@ export default {
return getProfileSettings({
createNewProfilePaths,
isDastSavedScansEnabled: this.glFeatures.dastSavedScans,
});
},
tabIndex: {
......@@ -210,8 +213,8 @@ export default {
},
profilesPerPage: 10,
i18n: {
heading: s__('DastProfiles|Manage Profiles'),
newProfileDropdownLabel: s__('DastProfiles|New Profile'),
heading: s__('DastProfiles|Manage DAST scans'),
newProfileDropdownLabel: __('New'),
subHeading: s__(
'DastProfiles|Save commonly used configurations for target sites and scan specifications as profiles. Use these with an on-demand scan.',
),
......
<script>
import ProfilesList from './dast_profiles_list.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue';
export default {
components: {
ProfilesList,
ScanTypeBadge,
},
};
</script>
<template>
<profiles-list v-bind="$attrs" v-on="$listeners">
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastScannerProfile.scanType)="{ value }">
<scan-type-badge :scan-type="value" />
</template>
</profiles-list>
</template>
<script>
import { GlBadge } from '@gitlab/ui';
import {
SCAN_TYPE,
SCAN_TYPE_LABEL,
} from 'ee/security_configuration/dast_scanner_profiles/constants';
const scanTypeToBadgeVariantMap = {
[SCAN_TYPE.ACTIVE]: 'warning',
[SCAN_TYPE.PASSIVE]: 'neutral',
};
export default {
name: 'DastScanTypeBadge',
components: {
GlBadge,
},
props: {
scanType: {
type: String,
required: true,
validator: (value) => Boolean(SCAN_TYPE[value]),
},
},
computed: {
variant() {
return scanTypeToBadgeVariantMap[this.scanType];
},
label() {
return SCAN_TYPE_LABEL[this.scanType].toLowerCase();
},
},
};
</script>
<template>
<gl-badge size="sm" :variant="variant">
{{ label }}
</gl-badge>
</template>
......@@ -10,11 +10,17 @@ export default () => {
}
const {
dataset: { newDastScannerProfilePath, newDastSiteProfilePath, projectFullPath },
dataset: {
newDastSavedScanPath,
newDastScannerProfilePath,
newDastSiteProfilePath,
projectFullPath,
},
} = el;
const props = {
createNewProfilePaths: {
savedScan: newDastSavedScanPath,
scannerProfile: newDastScannerProfilePath,
siteProfile: newDastSiteProfilePath,
},
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query DastSavedScans($fullPath: ID!, $after: String, $before: String, $first: Int, $last: Int) {
project @client {
savedScans(after: $after, before: $before, first: $first, last: $last) {
pageInfo {
...PageInfo
}
edges {
node {
id
name
dastSiteProfile {
id
targetUrl
}
dastScannerProfile {
id
scanType
}
editPath
}
}
}
}
}
mutation dastSavedScansDelete($input: DastSavedScansDeleteInput!) {
savedScansDelete(input: $input) @client {
errors
}
}
/* eslint-disable @gitlab/require-i18n-strings */
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { range } from 'lodash';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
// NOTE: We currently mock some fake DAST scans while the feature is feature-flagged and the
// backend is being worked on.
// This will be cleaned up as part of https://gitlab.com/gitlab-org/gitlab/-/issues/295248.
let id = 0;
const generateFakeDastScan = () => {
id += 1;
return {
node: {
id,
name: `My daily scan #${id}`,
description: 'Tests for SQL injection',
dastSiteProfile: {
id,
targetUrl: 'http://example.com ',
__typename: 'DastSiteProfile',
},
dastScannerProfile: {
id,
scanType: Math.random() < 0.5 ? 'PASSIVE' : 'ACTIVE',
__typename: 'DastScannerProfile',
},
editPath: '/on_demand_scans/1/edit',
__typename: 'DastSavedScan',
},
__typename: 'DastSavedScanEdge',
};
};
const resolvers = {
Query: {
project: () => ({
__typename: 'Project',
savedScans: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'startCursor',
endCursor: 'endCursor',
__typename: 'PageInfo',
},
edges: range(10).map(generateFakeDastScan),
__typename: 'DastSavedScanConnection',
},
}),
},
};
export default new VueApollo({
defaultClient: createDefaultClient(
{},
{
defaultClient: createDefaultClient(resolvers, {
assumeImmutableResults: true,
},
),
}),
});
import dastSavedScansQuery from 'ee/security_configuration/dast_profiles/graphql/dast_saved_scans.query.graphql';
import dastSavedScansDelete from 'ee/security_configuration/dast_profiles/graphql/dast_saved_scans_delete.mutation.graphql';
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles_delete.mutation.graphql';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastScannerProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles_delete.mutation.graphql';
import { dastProfilesDeleteResponse } from 'ee/security_configuration/dast_profiles/graphql/cache_utils';
import DastSavedScansList from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue';
import DastSiteProfileList from 'ee/security_configuration/dast_profiles/components/dast_site_profiles_list.vue';
import DastScannerProfileList from 'ee/security_configuration/dast_profiles/components/dast_scanner_profiles_list.vue';
import { s__ } from '~/locale';
export const getProfileSettings = ({ createNewProfilePaths }) => ({
export const getProfileSettings = ({ createNewProfilePaths, isDastSavedScansEnabled }) => ({
...(isDastSavedScansEnabled
? {
savedScans: {
profileType: 'savedScans',
createNewProfilePath: createNewProfilePaths.savedScan,
graphQL: {
query: dastSavedScansQuery,
deletion: {
mutation: dastSavedScansDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'savedScanDelete',
payloadTypeName: 'DastSavedScanDeletePayload',
}),
},
},
component: DastSavedScansList,
tableFields: [
{
label: s__('DastProfiles|Scan'),
key: 'name',
},
{
label: s__('DastProfiles|Target'),
key: 'dastSiteProfile.targetUrl',
},
{
label: s__('DastProfiles|Scan mode'),
key: 'dastScannerProfile.scanType',
},
],
i18n: {
createNewLinkText: s__('DastProfiles|DAST Scan'),
name: s__('DastProfiles|Saved Scans'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch saved scans. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete saved scan. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete saved scans:'),
},
},
},
}
: {}),
siteProfiles: {
profileType: 'siteProfiles',
createNewProfilePath: createNewProfilePaths.siteProfile,
......
......@@ -6,6 +6,7 @@ module Projects
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project, default_enabled: :yaml)
push_frontend_feature_flag(:dast_saved_scans, @project, default_enabled: :yaml)
end
feature_category :dynamic_application_security_testing
......
- add_to_breadcrumbs _('Security Configuration'), project_security_configuration_path(@project)
- breadcrumb_title s_('DastProfiles|Manage profiles')
- page_title s_('DastProfiles|Manage profiles')
- breadcrumb_title s_('DastProfiles|Manage DAST scans')
- page_title s_('DastProfiles|Manage DAST scans')
.js-dast-profiles{ data: { new_dast_site_profile_path: new_project_security_configuration_dast_profiles_dast_site_profile_path(@project),
.js-dast-profiles{ data: { new_dast_saved_scan_path: new_project_on_demand_scan_path(@project),
new_dast_site_profile_path: new_project_security_configuration_dast_profiles_dast_site_profile_path(@project),
new_dast_scanner_profile_path: new_project_security_configuration_dast_profiles_dast_scanner_profile_path(@project),
project_full_path: @project.path_with_namespace } }
......@@ -5,6 +5,7 @@ import { merge } from 'lodash';
import DastProfiles from 'ee/security_configuration/dast_profiles/components/dast_profiles.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
const TEST_NEW_DAST_SAVED_SCAN_PATH = '/-/on_demand_scans/new';
const TEST_NEW_DAST_SCANNER_PROFILE_PATH = '/-/on_demand_scans/scanner_profiles/new';
const TEST_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
......@@ -15,6 +16,7 @@ describe('EE - DastProfiles', () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = {
createNewProfilePaths: {
savedScan: TEST_NEW_DAST_SAVED_SCAN_PATH,
scannerProfile: TEST_NEW_DAST_SCANNER_PROFILE_PATH,
siteProfile: TEST_NEW_DAST_SITE_PROFILE_PATH,
},
......@@ -24,6 +26,9 @@ describe('EE - DastProfiles', () => {
const defaultMocks = {
$apollo: {
queries: {
savedScans: {
fetchMore: jest.fn().mockResolvedValue(),
},
siteProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
},
......@@ -43,6 +48,11 @@ describe('EE - DastProfiles', () => {
{
propsData: defaultProps,
mocks: defaultMocks,
provide: {
glFeatures: {
dastSavedScans: true,
},
},
},
options,
),
......@@ -72,31 +82,26 @@ describe('EE - DastProfiles', () => {
it('shows a heading that describes the purpose of the page', () => {
createFullComponent();
const heading = withinComponent().getByRole('heading', { name: /manage profiles/i });
const heading = withinComponent().getByRole('heading', { name: /manage dast scans/i });
expect(heading).not.toBe(null);
});
it('has a "New Profile" dropdown menu', () => {
it('has a "New" dropdown menu', () => {
createComponent();
expect(getDropdownComponent().props('text')).toBe('New Profile');
expect(getDropdownComponent().props('text')).toBe('New');
});
it(`shows a "Site Profile" dropdown item that links to ${TEST_NEW_DAST_SITE_PROFILE_PATH}`, () => {
createComponent();
expect(getSiteProfilesDropdownItem('Site Profile').getAttribute('href')).toBe(
TEST_NEW_DAST_SITE_PROFILE_PATH,
);
});
it(`shows a "Scanner Profile" dropdown item that links to ${TEST_NEW_DAST_SCANNER_PROFILE_PATH}`, () => {
it.each`
itemName | href
${'DAST Scan'} | ${TEST_NEW_DAST_SAVED_SCAN_PATH}
${'Site Profile'} | ${TEST_NEW_DAST_SITE_PROFILE_PATH}
${'Scanner Profile'} | ${TEST_NEW_DAST_SCANNER_PROFILE_PATH}
`('shows a "$itemName" dropdown item that links to $href', ({ itemName, href }) => {
createComponent();
expect(getSiteProfilesDropdownItem('Scanner Profile').getAttribute('href')).toBe(
TEST_NEW_DAST_SCANNER_PROFILE_PATH,
);
expect(getSiteProfilesDropdownItem(itemName).getAttribute('href')).toBe(href);
});
});
......@@ -116,7 +121,8 @@ describe('EE - DastProfiles', () => {
it.each`
tabName | shouldBeSelectedByDefault
${'Site Profiles'} | ${true}
${'Saved Scans'} | ${true}
${'Site Profiles'} | ${false}
${'Scanner Profiles'} | ${false}
`(
'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"',
......@@ -133,8 +139,9 @@ describe('EE - DastProfiles', () => {
describe.each`
tabName | index | givenLocationHash
${'Site Profiles'} | ${0} | ${'site-profiles'}
${'Scanner Profiles'} | ${1} | ${'scanner-profiles'}
${'Saved Scans'} | ${0} | ${'saved-scans'}
${'Site Profiles'} | ${1} | ${'site-profiles'}
${'Scanner Profiles'} | ${2} | ${'scanner-profiles'}
`('with location hash set to "$givenLocationHash"', ({ tabName, index, givenLocationHash }) => {
beforeEach(() => {
setWindowLocation(`http://foo.com/index#${givenLocationHash}`);
......@@ -166,6 +173,7 @@ describe('EE - DastProfiles', () => {
describe.each`
description | profileType
${'Saved Scans List'} | ${'savedScans'}
${'Site Profiles List'} | ${'siteProfiles'}
${'Scanner Profiles List'} | ${'scannerProfiles'}
`('$description', ({ profileType }) => {
......@@ -220,4 +228,33 @@ describe('EE - DastProfiles', () => {
expect(mutate).toHaveBeenCalledTimes(1);
});
});
describe('dastSavedScans feature flag disabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: {
dastSavedScans: false,
},
},
});
});
it('does not show a "DAST Scan" item in the dropdown', () => {
expect(getSiteProfilesDropdownItem('DAST Scan')).toBe(null);
});
it('shows only 2 tabs', () => {
expect(withinComponent().getAllByRole('tab')).toHaveLength(2);
});
it('"Site Profile" tab should be selected by default', () => {
const tab = getTab({
tabName: 'Site Profiles',
selected: true,
});
expect(tab).not.toBe(null);
});
});
});
import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import Component from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue';
import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { savedScans } from '../mocks/mock_data';
describe('EE - DastSavedScansList', () => {
let wrapper;
const defaultProps = {
profiles: [],
tableLabel: 'Saved scans',
fields: [
{ key: 'name' },
{ key: 'dastSiteProfile.targetUrl' },
{ key: 'dastScannerProfile.scanType' },
],
profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
fullPath: '/namespace/project',
hasMoreProfilesToLoad: false,
isLoading: false,
};
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(
Component,
merge(
{
propsData: defaultProps,
},
options,
),
);
};
const createFullComponent = wrapperFactory(mount);
const findProfileList = () => wrapper.find(ProfilesList);
afterEach(() => {
wrapper.destroy();
});
it('renders profile list properly', () => {
createFullComponent({
propsData: { profiles: savedScans },
});
expect(findProfileList()).toExist();
});
it('passes down the props properly', () => {
createFullComponent();
expect(findProfileList().props()).toEqual(defaultProps);
});
it('sets listeners on profile list component', () => {
const inputHandler = jest.fn();
createFullComponent({
listeners: {
input: inputHandler,
},
});
findProfileList().vm.$emit('input');
expect(inputHandler).toHaveBeenCalled();
});
});
import { shallowMount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import { SCAN_TYPE } from 'ee/security_configuration/dast_scanner_profiles/constants';
import DastScanTypeBadge from 'ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue';
describe('EE - DastScanTypeBadge', () => {
let wrapper;
const findBadge = () => wrapper.find(GlBadge);
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(DastScanTypeBadge, options);
};
const createComponent = wrapperFactory();
afterEach(() => {
wrapper.destroy();
});
it.each`
scanType | variant
${SCAN_TYPE.ACTIVE} | ${'warning'}
${SCAN_TYPE.PASSIVE} | ${'neutral'}
`('renders a $variant badge for $scanType scans', ({ scanType, variant }) => {
createComponent({
propsData: {
scanType,
},
});
expect(findBadge().props('variant')).toBe(variant);
});
});
......@@ -61,3 +61,13 @@ export const scannerProfiles = [
showDebugMessages: true,
},
];
export const savedScans = [
{
id: 'gid://gitlab/DastScan/1',
name: 'Scan 1',
dastSiteProfile: siteProfiles[0],
dastScannerProfile: scannerProfiles[0],
editPath: '/1/edit',
},
];
......@@ -12,6 +12,10 @@ RSpec.describe "projects/security/dast_profiles/show", type: :view do
expect(rendered).to have_selector('.js-dast-profiles')
end
it 'passes new dast saved scan path' do
expect(rendered).to include '/on_demand_scans/new'
end
it 'passes new dast site profile path' do
expect(rendered).to include '/security/configuration/dast_profiles/dast_site_profiles/new'
end
......
......@@ -8788,6 +8788,12 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr ""
msgid "DastProfiles|Could not delete saved scan. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete saved scans:"
msgstr ""
msgid "DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later."
msgstr ""
......@@ -8800,6 +8806,9 @@ msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
msgstr ""
msgid "DastProfiles|Could not fetch saved scans. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
......@@ -8812,6 +8821,9 @@ msgstr ""
msgid "DastProfiles|Could not update the site profile. Please try again."
msgstr ""
msgid "DastProfiles|DAST Scan"
msgstr ""
msgid "DastProfiles|Debug messages"
msgstr ""
......@@ -8854,7 +8866,7 @@ msgstr ""
msgid "DastProfiles|Include debug messages in the DAST console output."
msgstr ""
msgid "DastProfiles|Manage Profiles"
msgid "DastProfiles|Manage DAST scans"
msgstr ""
msgid "DastProfiles|Manage profiles"
......@@ -8866,9 +8878,6 @@ msgstr ""
msgid "DastProfiles|Minimum = 1 second, Maximum = 3600 seconds"
msgstr ""
msgid "DastProfiles|New Profile"
msgstr ""
msgid "DastProfiles|New scanner profile"
msgstr ""
......@@ -8908,6 +8917,12 @@ msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
msgid "DastProfiles|Saved Scans"
msgstr ""
msgid "DastProfiles|Scan"
msgstr ""
msgid "DastProfiles|Scan mode"
msgstr ""
......@@ -8935,6 +8950,9 @@ msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
msgid "DastProfiles|Target"
msgstr ""
msgid "DastProfiles|Target URL"
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