Commit 12007969 authored by Simon Knox's avatar Simon Knox

Merge branch 'djadmin-api-security-frontend-2' into 'master'

Add scan method option to DAST Site Profile

See merge request gitlab-org/gitlab!80295
parents 50d2a85b 3d80c1f7
<script> <script>
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormText, GlFormTextarea } from '@gitlab/ui'; import {
GlFormGroup,
GlFormInput,
GlFormRadioGroup,
GlFormText,
GlFormTextarea,
GlFormSelect,
GlLink,
} from '@gitlab/ui';
import { initFormField } from 'ee/security_configuration/utils'; import { initFormField } from 'ee/security_configuration/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { serializeFormObject } from '~/lib/utils/forms'; import { serializeFormObject } from '~/lib/utils/forms';
import { __, s__, n__, sprintf } from '~/locale'; import { __, s__, n__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BaseDastProfileForm from '../../components/base_dast_profile_form.vue'; import BaseDastProfileForm from '../../components/base_dast_profile_form.vue';
import dastProfileFormMixin from '../../dast_profile_form_mixin'; import dastProfileFormMixin from '../../dast_profile_form_mixin';
import tooltipIcon from '../../dast_scanner_profiles/components/tooltip_icon.vue'; import tooltipIcon from '../../dast_scanner_profiles/components/tooltip_icon.vue';
...@@ -13,6 +23,7 @@ import { ...@@ -13,6 +23,7 @@ import {
REDACTED_PASSWORD, REDACTED_PASSWORD,
REDACTED_REQUEST_HEADERS, REDACTED_REQUEST_HEADERS,
TARGET_TYPES, TARGET_TYPES,
SCAN_METHODS,
} from '../constants'; } from '../constants';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql'; import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql'; import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
...@@ -31,8 +42,10 @@ export default { ...@@ -31,8 +42,10 @@ export default {
GlFormText, GlFormText,
GlFormTextarea, GlFormTextarea,
tooltipIcon, tooltipIcon,
GlFormSelect,
GlLink,
}, },
mixins: [dastProfileFormMixin()], mixins: [dastProfileFormMixin(), glFeatureFlagMixin()],
data() { data() {
const { const {
name = '', name = '',
...@@ -41,6 +54,8 @@ export default { ...@@ -41,6 +54,8 @@ export default {
requestHeaders = '', requestHeaders = '',
auth = {}, auth = {},
targetType = TARGET_TYPES.WEBSITE.value, targetType = TARGET_TYPES.WEBSITE.value,
scanMethod = null,
scanFilePath = '',
} = this.profile; } = this.profile;
const form = { const form = {
...@@ -60,6 +75,8 @@ export default { ...@@ -60,6 +75,8 @@ export default {
skipValidation: true, skipValidation: true,
}), }),
targetType: initFormField({ value: targetType, skipValidation: true }), targetType: initFormField({ value: targetType, skipValidation: true }),
scanMethod: initFormField({ value: scanMethod, skipValidation: true }),
scanFilePath: initFormField({ value: scanFilePath, skipValidation: true }),
}, },
}; };
...@@ -70,6 +87,9 @@ export default { ...@@ -70,6 +87,9 @@ export default {
tokenId: null, tokenId: null,
token: null, token: null,
targetTypesOptions: Object.values(TARGET_TYPES), targetTypesOptions: Object.values(TARGET_TYPES),
showAPIFilePath: false,
scanMethodOptions: Object.values(SCAN_METHODS),
SCAN_METHODS,
}; };
}, },
computed: { computed: {
...@@ -114,8 +134,18 @@ export default { ...@@ -114,8 +134,18 @@ export default {
? s__('DastProfiles|API endpoint URL') ? s__('DastProfiles|API endpoint URL')
: s__('DastProfiles|Target URL'), : s__('DastProfiles|Target URL'),
}, },
scanMethod: {
label: s__('DastProfiles|Scan method'),
helpText: s__('DastProfiles|What does each method do?'),
defaultOption: s__('DastProfiles|Choose a scan method'),
},
}; };
}, },
dastApiDocsPath() {
return helpPagePath('user/application_security/dast_api/', {
anchor: 'enable-dast-api-scanning',
});
},
parsedExcludedUrls() { parsedExcludedUrls() {
return this.form.fields.excludedUrls.value return this.form.fields.excludedUrls.value
.split(EXCLUDED_URLS_SEPARATOR) .split(EXCLUDED_URLS_SEPARATOR)
...@@ -132,6 +162,12 @@ export default { ...@@ -132,6 +162,12 @@ export default {
isTargetAPI() { isTargetAPI() {
return this.form.fields.targetType.value === TARGET_TYPES.API.value; return this.form.fields.targetType.value === TARGET_TYPES.API.value;
}, },
shouldRenderScanMethod() {
return this.glFeatures.dastApiScanner && this.isTargetAPI;
},
selectedScanMethod() {
return SCAN_METHODS[this.form.fields.scanMethod.value];
},
isAuthEnabled() { isAuthEnabled() {
return this.authSection.fields.enabled && !this.isTargetAPI; return this.authSection.fields.enabled && !this.isTargetAPI;
}, },
...@@ -145,6 +181,8 @@ export default { ...@@ -145,6 +181,8 @@ export default {
targetType, targetType,
requestHeaders, requestHeaders,
excludedUrls, excludedUrls,
scanMethod,
scanFilePath,
} = serializeFormObject(this.form.fields); } = serializeFormObject(this.form.fields);
return { return {
...@@ -159,6 +197,7 @@ export default { ...@@ -159,6 +197,7 @@ export default {
...(requestHeaders !== REDACTED_REQUEST_HEADERS && { ...(requestHeaders !== REDACTED_REQUEST_HEADERS && {
requestHeaders, requestHeaders,
}), }),
...(this.shouldRenderScanMethod && { scanMethod, scanFilePath }),
}; };
}, },
}, },
...@@ -254,6 +293,52 @@ export default { ...@@ -254,6 +293,52 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="shouldRenderScanMethod"
id="scan-method-popover-container"
:label="i18n.scanMethod.label"
>
<gl-form-select
v-model="form.fields.scanMethod.value"
v-validation:[form.showValidation]
:options="scanMethodOptions"
name="scanMethod"
class="mw-460"
data-testid="scan-method-select-input"
:state="form.fields.scanMethod.state"
required
>
<template #first>
<option :value="null" disabled>{{ i18n.scanMethod.defaultOption }}</option>
</template>
</gl-form-select>
<gl-form-text
><gl-link :href="dastApiDocsPath" target="_blank">{{
i18n.scanMethod.helpText
}}</gl-link></gl-form-text
>
<gl-form-group
v-if="selectedScanMethod"
class="gl-mt-5"
:label="selectedScanMethod.inputLabel"
:invalid-feedback="form.fields.scanFilePath.feedback"
>
<gl-form-input
v-model="form.fields.scanFilePath.value"
v-validation:[form.showValidation]
name="scanFilePath"
class="mw-460"
data-testid="scan-file-path-input"
type="text"
:placeholder="selectedScanMethod.placeholder"
required
:state="form.fields.scanFilePath.state"
/>
</gl-form-group>
</gl-form-group>
<div class="row"> <div class="row">
<gl-form-group <gl-form-group
:label="i18n.excludedUrls.label" :label="i18n.excludedUrls.label"
......
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
export const MAX_CHAR_LIMIT_EXCLUDED_URLS = 2048; export const MAX_CHAR_LIMIT_EXCLUDED_URLS = 2048;
export const MAX_CHAR_LIMIT_REQUEST_HEADERS = 2048; export const MAX_CHAR_LIMIT_REQUEST_HEADERS = 2048;
...@@ -10,3 +10,26 @@ export const TARGET_TYPES = { ...@@ -10,3 +10,26 @@ export const TARGET_TYPES = {
WEBSITE: { value: 'WEBSITE', text: s__('DastProfiles|Website') }, WEBSITE: { value: 'WEBSITE', text: s__('DastProfiles|Website') },
API: { value: 'API', text: s__('DastProfiles|API') }, API: { value: 'API', text: s__('DastProfiles|API') },
}; };
export const SCAN_METHODS = {
HAR: {
text: __('HTTP Archive (HAR)'),
value: 'HAR',
inputLabel: __('HAR file path or URL'),
placeholder: s__(
'DastProfiles|folder/dast_example.har or https://example.com/dast_example.har',
),
},
OPENAPI: {
text: __('OpenAPI'),
value: 'OPENAPI',
inputLabel: __('OpenAPI Specification file path or URL'),
placeholder: s__('DastProfiles|folder/openapi.json or https://example.com/openapi.json'),
},
POSTMAN_COLLECTION: {
text: __('Postman collection'),
value: 'POSTMAN_COLLECTION',
inputLabel: __('Postman collection file path or URL'),
placeholder: s__('DastProfiles|folder/example.postman_collection.json or https://example.com/'),
},
};
...@@ -8,6 +8,7 @@ module Projects ...@@ -8,6 +8,7 @@ module Projects
before_action do before_action do
authorize_read_on_demand_dast_scan! authorize_read_on_demand_dast_scan!
push_frontend_feature_flag(:dast_api_scanner, @project, default_enabled: :yaml)
end end
feature_category :dynamic_application_security_testing feature_category :dynamic_application_security_testing
......
...@@ -10,6 +10,10 @@ import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_profil ...@@ -10,6 +10,10 @@ import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_profil
import { policySiteProfiles } from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data'; import { policySiteProfiles } from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
SCAN_METHODS,
TARGET_TYPES,
} from 'ee/security_configuration/dast_profiles/dast_site_profiles/constants';
const projectFullPath = 'group/project'; const projectFullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${projectFullPath}/-/security/configuration/dast_scans`; const profilesLibraryPath = `${TEST_HOST}/${projectFullPath}/-/security/configuration/dast_scans`;
...@@ -18,6 +22,7 @@ const profileName = 'My DAST site profile'; ...@@ -18,6 +22,7 @@ const profileName = 'My DAST site profile';
const targetUrl = 'http://example.com'; const targetUrl = 'http://example.com';
const excludedUrls = 'https://foo.com/logout, https://foo.com/send_mail'; const excludedUrls = 'https://foo.com/logout, https://foo.com/send_mail';
const requestHeaders = 'my-new-header=something'; const requestHeaders = 'my-new-header=something';
const scanFilePath = 'test-path';
const defaultProps = { const defaultProps = {
profilesLibraryPath, profilesLibraryPath,
...@@ -38,6 +43,8 @@ describe('DastSiteProfileForm', () => { ...@@ -38,6 +43,8 @@ describe('DastSiteProfileForm', () => {
const findTargetUrlInput = () => wrapper.findByTestId('target-url-input'); const findTargetUrlInput = () => wrapper.findByTestId('target-url-input');
const findExcludedUrlsInput = () => wrapper.findByTestId('excluded-urls-input'); const findExcludedUrlsInput = () => wrapper.findByTestId('excluded-urls-input');
const findRequestHeadersInput = () => wrapper.findByTestId('request-headers-input'); const findRequestHeadersInput = () => wrapper.findByTestId('request-headers-input');
const findScanMethodInput = () => wrapper.findByTestId('scan-method-select-input');
const scanFilePathInput = () => wrapper.findByTestId('scan-file-path-input');
const findAuthCheckbox = () => wrapper.findByTestId('auth-enable-checkbox'); const findAuthCheckbox = () => wrapper.findByTestId('auth-enable-checkbox');
const findTargetTypeOption = () => wrapper.findByTestId('site-type-option'); const findTargetTypeOption = () => wrapper.findByTestId('site-type-option');
...@@ -68,14 +75,15 @@ describe('DastSiteProfileForm', () => { ...@@ -68,14 +75,15 @@ describe('DastSiteProfileForm', () => {
.filter((r) => r.attributes('value') === type) .filter((r) => r.attributes('value') === type)
.at(0); .at(0);
radio.element.selected = true; radio.element.selected = true;
radio.trigger('change'); return radio.trigger('change');
}; };
const createComponentFactory = (mountFn = mountExtended) => (options) => { const createComponentFactory = (mountFn = mountExtended) => (options) => {
const mountOpts = merge( const mountOpts = merge(
{}, {},
{ {
propsData: defaultProps, propsData: defaultProps,
provide: { projectFullPath }, provide: { projectFullPath, glFeatures: { dastApiScanner: true } },
}, },
options, options,
); );
...@@ -179,14 +187,41 @@ describe('DastSiteProfileForm', () => { ...@@ -179,14 +187,41 @@ describe('DastSiteProfileForm', () => {
}); });
describe('when target type is API', () => { describe('when target type is API', () => {
const getScanMethodOption = (index) => {
return findScanMethodInput().findAll('option').at(index);
};
const setScanMethodOption = (index) => {
getScanMethodOption(index).setSelected();
findScanMethodInput().trigger('change');
};
beforeEach(() => { beforeEach(() => {
setTargetType('API'); setTargetType(TARGET_TYPES.API.value);
}); });
it('should hide auth section', () => { it('should hide auth section', () => {
expect(findAuthSection().exists()).toBe(false); expect(findAuthSection().exists()).toBe(false);
}); });
describe('scan method option', () => {
it('should render all scan method options', () => {
expect(findScanMethodInput().exists()).toBe(true);
expect(getScanMethodOption(0).attributes('disabled')).toBe('disabled');
Object.values(SCAN_METHODS).forEach((method, index) => {
expect(getScanMethodOption(index + 1).text()).toBe(method.text);
});
});
it('should not show scan file-path input by default', () => {
expect(scanFilePathInput().exists()).toBe(false);
});
it('should show scan file-path input upon selection', async () => {
await setScanMethodOption(1);
expect(scanFilePathInput().exists()).toBe(true);
});
});
describe.each` describe.each`
title | profile | mutationVars | mutation | mutationKind title | profile | mutationVars | mutation | mutationKind
${'New site profile'} | ${{}} | ${{ fullPath: projectFullPath }} | ${dastSiteProfileCreateMutation} | ${'dastSiteProfileCreate'} ${'New site profile'} | ${{}} | ${{ fullPath: projectFullPath }} | ${dastSiteProfileCreateMutation} | ${'dastSiteProfileCreate'}
...@@ -202,7 +237,9 @@ describe('DastSiteProfileForm', () => { ...@@ -202,7 +237,9 @@ describe('DastSiteProfileForm', () => {
it('passes correct props to base component', async () => { it('passes correct props to base component', async () => {
await fillForm(); await fillForm();
await setTargetType('API'); await setTargetType(TARGET_TYPES.API.value);
await setScanMethodOption(1);
await setFieldValue(scanFilePathInput(), scanFilePath);
const baseDastProfileForm = findBaseDastProfileForm(); const baseDastProfileForm = findBaseDastProfileForm();
expect(baseDastProfileForm.props('mutation')).toBe(mutation); expect(baseDastProfileForm.props('mutation')).toBe(mutation);
...@@ -213,6 +250,8 @@ describe('DastSiteProfileForm', () => { ...@@ -213,6 +250,8 @@ describe('DastSiteProfileForm', () => {
excludedUrls: excludedUrls.split(', '), excludedUrls: excludedUrls.split(', '),
requestHeaders, requestHeaders,
targetType: 'API', targetType: 'API',
scanMethod: 'HAR',
scanFilePath,
...mutationVars, ...mutationVars,
}); });
}); });
...@@ -277,4 +316,20 @@ describe('DastSiteProfileForm', () => { ...@@ -277,4 +316,20 @@ describe('DastSiteProfileForm', () => {
expect(findParentFormGroup().attributes('disabled')).toBe('true'); expect(findParentFormGroup().attributes('disabled')).toBe('true');
}); });
}); });
describe('when dastApiScanner FF is disabled', () => {
beforeEach(() => {
createShallowComponent({
propsData: {
profile: policySiteProfiles[0],
},
provide: { glFeatures: { dastApiScanner: false } },
});
});
it('should not show scan method options', () => {
expect(findScanMethodInput().exists()).toBe(false);
expect(scanFilePathInput().exists()).toBe(false);
});
});
}); });
...@@ -11076,6 +11076,9 @@ msgstr "" ...@@ -11076,6 +11076,9 @@ msgstr ""
msgid "DastProfiles|Branch missing" msgid "DastProfiles|Branch missing"
msgstr "" msgstr ""
msgid "DastProfiles|Choose a scan method"
msgstr ""
msgid "DastProfiles|Could not create the scanner profile. Please try again." msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr "" msgstr ""
...@@ -11217,6 +11220,9 @@ msgstr "" ...@@ -11217,6 +11220,9 @@ msgstr ""
msgid "DastProfiles|Save profile" msgid "DastProfiles|Save profile"
msgstr "" msgstr ""
msgid "DastProfiles|Scan method"
msgstr ""
msgid "DastProfiles|Scan mode" msgid "DastProfiles|Scan mode"
msgstr "" msgstr ""
...@@ -11295,12 +11301,24 @@ msgstr "" ...@@ -11295,12 +11301,24 @@ msgstr ""
msgid "DastProfiles|Website" msgid "DastProfiles|Website"
msgstr "" msgstr ""
msgid "DastProfiles|What does each method do?"
msgstr ""
msgid "DastProfiles|You can either choose a passive scan or validate the target site from the site profile management page. %{docsLinkStart}Learn more about site validation.%{docsLinkEnd}" msgid "DastProfiles|You can either choose a passive scan or validate the target site from the site profile management page. %{docsLinkStart}Learn more about site validation.%{docsLinkEnd}"
msgstr "" msgstr ""
msgid "DastProfiles|You cannot run an active scan against an unvalidated site." msgid "DastProfiles|You cannot run an active scan against an unvalidated site."
msgstr "" msgstr ""
msgid "DastProfiles|folder/dast_example.har or https://example.com/dast_example.har"
msgstr ""
msgid "DastProfiles|folder/example.postman_collection.json or https://example.com/"
msgstr ""
msgid "DastProfiles|folder/openapi.json or https://example.com/openapi.json"
msgstr ""
msgid "DastSiteValidation|Copy HTTP header to clipboard" msgid "DastSiteValidation|Copy HTTP header to clipboard"
msgstr "" msgstr ""
...@@ -17839,6 +17857,9 @@ msgstr "" ...@@ -17839,6 +17857,9 @@ msgstr ""
msgid "HAR file path or URL" msgid "HAR file path or URL"
msgstr "" msgstr ""
msgid "HTTP Archive (HAR)"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}" msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
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