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>
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 { helpPagePath } from '~/helpers/help_page_helper';
import { serializeFormObject } from '~/lib/utils/forms';
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 dastProfileFormMixin from '../../dast_profile_form_mixin';
import tooltipIcon from '../../dast_scanner_profiles/components/tooltip_icon.vue';
......@@ -13,6 +23,7 @@ import {
REDACTED_PASSWORD,
REDACTED_REQUEST_HEADERS,
TARGET_TYPES,
SCAN_METHODS,
} from '../constants';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
......@@ -31,8 +42,10 @@ export default {
GlFormText,
GlFormTextarea,
tooltipIcon,
GlFormSelect,
GlLink,
},
mixins: [dastProfileFormMixin()],
mixins: [dastProfileFormMixin(), glFeatureFlagMixin()],
data() {
const {
name = '',
......@@ -41,6 +54,8 @@ export default {
requestHeaders = '',
auth = {},
targetType = TARGET_TYPES.WEBSITE.value,
scanMethod = null,
scanFilePath = '',
} = this.profile;
const form = {
......@@ -60,6 +75,8 @@ export default {
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 {
tokenId: null,
token: null,
targetTypesOptions: Object.values(TARGET_TYPES),
showAPIFilePath: false,
scanMethodOptions: Object.values(SCAN_METHODS),
SCAN_METHODS,
};
},
computed: {
......@@ -114,8 +134,18 @@ export default {
? s__('DastProfiles|API endpoint 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() {
return this.form.fields.excludedUrls.value
.split(EXCLUDED_URLS_SEPARATOR)
......@@ -132,6 +162,12 @@ export default {
isTargetAPI() {
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() {
return this.authSection.fields.enabled && !this.isTargetAPI;
},
......@@ -145,6 +181,8 @@ export default {
targetType,
requestHeaders,
excludedUrls,
scanMethod,
scanFilePath,
} = serializeFormObject(this.form.fields);
return {
......@@ -159,6 +197,7 @@ export default {
...(requestHeaders !== REDACTED_REQUEST_HEADERS && {
requestHeaders,
}),
...(this.shouldRenderScanMethod && { scanMethod, scanFilePath }),
};
},
},
......@@ -254,6 +293,52 @@ export default {
/>
</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">
<gl-form-group
: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_REQUEST_HEADERS = 2048;
......@@ -10,3 +10,26 @@ export const TARGET_TYPES = {
WEBSITE: { value: 'WEBSITE', text: s__('DastProfiles|Website') },
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
before_action do
authorize_read_on_demand_dast_scan!
push_frontend_feature_flag(:dast_api_scanner, @project, default_enabled: :yaml)
end
feature_category :dynamic_application_security_testing
......
......@@ -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 { TEST_HOST } from 'helpers/test_constants';
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 profilesLibraryPath = `${TEST_HOST}/${projectFullPath}/-/security/configuration/dast_scans`;
......@@ -18,6 +22,7 @@ const profileName = 'My DAST site profile';
const targetUrl = 'http://example.com';
const excludedUrls = 'https://foo.com/logout, https://foo.com/send_mail';
const requestHeaders = 'my-new-header=something';
const scanFilePath = 'test-path';
const defaultProps = {
profilesLibraryPath,
......@@ -38,6 +43,8 @@ describe('DastSiteProfileForm', () => {
const findTargetUrlInput = () => wrapper.findByTestId('target-url-input');
const findExcludedUrlsInput = () => wrapper.findByTestId('excluded-urls-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 findTargetTypeOption = () => wrapper.findByTestId('site-type-option');
......@@ -68,14 +75,15 @@ describe('DastSiteProfileForm', () => {
.filter((r) => r.attributes('value') === type)
.at(0);
radio.element.selected = true;
radio.trigger('change');
return radio.trigger('change');
};
const createComponentFactory = (mountFn = mountExtended) => (options) => {
const mountOpts = merge(
{},
{
propsData: defaultProps,
provide: { projectFullPath },
provide: { projectFullPath, glFeatures: { dastApiScanner: true } },
},
options,
);
......@@ -179,14 +187,41 @@ describe('DastSiteProfileForm', () => {
});
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(() => {
setTargetType('API');
setTargetType(TARGET_TYPES.API.value);
});
it('should hide auth section', () => {
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`
title | profile | mutationVars | mutation | mutationKind
${'New site profile'} | ${{}} | ${{ fullPath: projectFullPath }} | ${dastSiteProfileCreateMutation} | ${'dastSiteProfileCreate'}
......@@ -202,7 +237,9 @@ describe('DastSiteProfileForm', () => {
it('passes correct props to base component', async () => {
await fillForm();
await setTargetType('API');
await setTargetType(TARGET_TYPES.API.value);
await setScanMethodOption(1);
await setFieldValue(scanFilePathInput(), scanFilePath);
const baseDastProfileForm = findBaseDastProfileForm();
expect(baseDastProfileForm.props('mutation')).toBe(mutation);
......@@ -213,6 +250,8 @@ describe('DastSiteProfileForm', () => {
excludedUrls: excludedUrls.split(', '),
requestHeaders,
targetType: 'API',
scanMethod: 'HAR',
scanFilePath,
...mutationVars,
});
});
......@@ -277,4 +316,20 @@ describe('DastSiteProfileForm', () => {
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 ""
msgid "DastProfiles|Branch missing"
msgstr ""
msgid "DastProfiles|Choose a scan method"
msgstr ""
msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr ""
......@@ -11217,6 +11220,9 @@ msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
msgid "DastProfiles|Scan method"
msgstr ""
msgid "DastProfiles|Scan mode"
msgstr ""
......@@ -11295,12 +11301,24 @@ msgstr ""
msgid "DastProfiles|Website"
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}"
msgstr ""
msgid "DastProfiles|You cannot run an active scan against an unvalidated site."
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"
msgstr ""
......@@ -17839,6 +17857,9 @@ msgstr ""
msgid "HAR file path or URL"
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}"
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