Commit 7942afee authored by Dheeraj Joshi's avatar Dheeraj Joshi Committed by Alex Kalderimis

Add option to specify target type for DAST Profiles

parent 69bd1adf
......@@ -18,7 +18,10 @@ import {
SCAN_TYPE_LABEL,
SCAN_TYPE,
} from 'ee/security_configuration/dast_scanner_profiles/constants';
import { EXCLUDED_URLS_SEPARATOR } from 'ee/security_configuration/dast_site_profiles_form/constants';
import {
EXCLUDED_URLS_SEPARATOR,
TARGET_TYPES,
} from 'ee/security_configuration/dast_site_profiles_form/constants';
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_validation/constants';
import { initFormField } from 'ee/security_configuration/utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
......@@ -234,6 +237,9 @@ export default {
hasExcludedUrls() {
return this.selectedSiteProfile.excludedUrls?.length > 0;
},
targetTypeValue() {
return TARGET_TYPES[this.selectedSiteProfile.targetType].text;
},
storageKey() {
return `${this.projectPath}/${ON_DEMAND_SCANS_STORAGE_KEY}`;
},
......@@ -501,6 +507,11 @@ export default {
:label="s__('DastProfiles|Target URL')"
:value="selectedSiteProfile.targetUrl"
/>
<profile-selector-summary-cell
v-if="glFeatures.securityDastSiteProfilesApiOption"
:label="s__('DastProfiles|Site type')"
:value="targetTypeValue"
/>
</div>
<template v-if="glFeatures.securityDastSiteProfilesAdditionalFields">
<template v-if="selectedSiteProfile.auth.enabled">
......
......@@ -14,6 +14,7 @@ query DastSiteProfiles($fullPath: ID!, $after: String, $before: String, $first:
profileName
normalizedTargetUrl
targetUrl
targetType
editPath
validationStatus
referencedInSecurityPolicies
......
......@@ -8,6 +8,7 @@ import {
GlModal,
GlFormTextarea,
GlFormText,
GlFormRadioGroup,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { isEqual } from 'lodash';
......@@ -23,6 +24,7 @@ import {
EXCLUDED_URLS_SEPARATOR,
REDACTED_PASSWORD,
REDACTED_REQUEST_HEADERS,
TARGET_TYPES,
} from '../constants';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
......@@ -41,6 +43,7 @@ export default {
DastSiteAuthSection,
GlFormText,
tooltipIcon,
GlFormRadioGroup,
},
directives: {
validation: validation(),
......@@ -63,8 +66,14 @@ export default {
},
},
data() {
const { name = '', targetUrl = '', excludedUrls = [], requestHeaders = '', auth = {} } =
this.siteProfile || {};
const {
name = '',
targetUrl = '',
excludedUrls = [],
requestHeaders = '',
auth = {},
targetType = TARGET_TYPES.WEBSITE.value,
} = this.siteProfile || {};
const form = {
state: false,
......@@ -82,6 +91,7 @@ export default {
required: false,
skipValidation: true,
}),
targetType: initFormField({ value: targetType, skipValidation: true }),
},
};
......@@ -95,6 +105,7 @@ export default {
token: null,
errorMessage: '',
errors: [],
targetTypesOptions: Object.values(TARGET_TYPES),
};
},
computed: {
......@@ -156,6 +167,12 @@ export default {
}
return authFields;
},
isTargetAPI() {
return (
this.glFeatures.securityDastSiteProfilesApiOption &&
this.form.fields.targetType.value === TARGET_TYPES.API.value
);
},
},
methods: {
onSubmit() {
......@@ -173,9 +190,13 @@ export default {
this.hideErrors();
const { errorMessage } = this.i18n;
const { profileName, targetUrl, requestHeaders, excludedUrls } = serializeFormObject(
this.form.fields,
);
const {
profileName,
targetUrl,
targetType,
requestHeaders,
excludedUrls,
} = serializeFormObject(this.form.fields);
const variables = {
input: {
......@@ -183,8 +204,11 @@ export default {
...(this.isEdit ? { id: this.siteProfile.id } : {}),
profileName,
targetUrl,
...(this.glFeatures.securityDastSiteProfilesApiOption && {
targetType,
}),
...(this.glFeatures.securityDastSiteProfilesAdditionalFields && {
auth: this.serializedAuthFields,
...(!this.isTargetAPI && { auth: this.serializedAuthFields }),
...(excludedUrls && {
excludedUrls: this.parsedExcludedUrls,
}),
......@@ -314,6 +338,17 @@ export default {
<hr class="gl-border-gray-100" />
<gl-form-group
v-if="glFeatures.securityDastSiteProfilesApiOption"
:label="s__('DastProfiles|Site type')"
>
<gl-form-radio-group
v-model="form.fields.targetType.value"
:options="targetTypesOptions"
data-testid="site-type-option"
/>
</gl-form-group>
<gl-form-group
data-testid="target-url-input-group"
:invalid-feedback="form.fields.targetUrl.feedback"
......@@ -381,7 +416,7 @@ export default {
</gl-form-group>
<dast-site-auth-section
v-if="glFeatures.securityDastSiteProfilesAdditionalFields"
v-if="glFeatures.securityDastSiteProfilesAdditionalFields && !isTargetAPI"
v-model="authSection"
:disabled="isPolicyProfile"
:show-validation="form.showValidation"
......
import { s__ } from '~/locale';
export const MAX_CHAR_LIMIT_EXCLUDED_URLS = 2048;
export const MAX_CHAR_LIMIT_REQUEST_HEADERS = 2048;
export const EXCLUDED_URLS_SEPARATOR = ',';
export const REDACTED_PASSWORD = '••••••••';
export const REDACTED_REQUEST_HEADERS = '••••••••';
export const TARGET_TYPES = {
WEBSITE: { value: 'WEBSITE', text: s__('DastProfiles|Website') },
API: { value: 'API', text: s__('DastProfiles|Rest API') },
};
......@@ -6,6 +6,7 @@ module Projects
before_action do
push_frontend_feature_flag(:security_dast_site_profiles_additional_fields, @project, default_enabled: :yaml)
push_frontend_feature_flag(:security_dast_site_profiles_api_option, @project, default_enabled: :yaml)
end
before_action :authorize_read_on_demand_scans!, only: :index
......
......@@ -9,6 +9,7 @@ module Projects
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_dast_site_profiles_additional_fields, @project, default_enabled: :yaml)
push_frontend_feature_flag(:security_dast_site_profiles_api_option, @project, default_enabled: :yaml)
end
feature_category :dynamic_application_security_testing
......@@ -26,6 +27,7 @@ module Projects
id
name: profileName
targetUrl
targetType
excludedUrls
requestHeaders
auth { enabled url username usernameField password passwordField }
......
......@@ -559,6 +559,7 @@ describe('OnDemandScansForm', () => {
provide: {
glFeatures: {
securityDastSiteProfilesAdditionalFields: true,
securityDastSiteProfilesApiOption: true,
},
},
});
......@@ -569,6 +570,7 @@ describe('OnDemandScansForm', () => {
const summary = wrapper.find(SiteProfileSelector).text();
const defaultPassword = '••••••••';
const defaultRequestHeaders = '[Redacted]';
const defaultSiteType = 'Website';
expect(summary).toMatch(authEnabledProfile.targetUrl);
expect(summary).toMatch(authEnabledProfile.excludedUrls.join(','));
......@@ -578,6 +580,7 @@ describe('OnDemandScansForm', () => {
expect(summary).toMatch(authEnabledProfile.auth.passwordField);
expect(summary).toMatch(defaultPassword);
expect(summary).toMatch(defaultRequestHeaders);
expect(summary).toMatch(defaultSiteType);
});
it('does not render the summary provided an invalid profile ID', async () => {
......
......@@ -34,6 +34,7 @@ describe('OnDemandScansSiteProfileSelector', () => {
newSiteProfilePath: TEST_NEW_PATH,
glFeatures: {
securityDastSiteProfilesAdditionalFields: true,
securityDastSiteProfilesApiOption: true,
},
},
slots: {
......
......@@ -40,6 +40,7 @@ export const siteProfiles = [
id: 'gid://gitlab/DastSiteProfile/1',
profileName: 'Site profile #1',
targetUrl: 'https://foo.com',
targetType: 'WEBSITE',
normalizedTargetUrl: 'https://foo.com:443',
editPath: '/site_profiles/edit/1',
validationStatus: 'PENDING_VALIDATION',
......@@ -59,6 +60,7 @@ export const siteProfiles = [
id: 'gid://gitlab/DastSiteProfile/2',
profileName: 'Site profile #2',
targetUrl: 'https://bar.com',
targetType: 'API',
normalizedTargetUrl: 'https://bar.com:443',
editPath: '/site_profiles/edit/2',
validationStatus: 'PASSED_VALIDATION',
......@@ -87,4 +89,5 @@ export const policySiteProfile = {
},
excludedUrls: ['https://bar.com/logout'],
referencedInSecurityPolicies: ['some_policy'],
targetType: 'WEBSITE',
};
......@@ -54,6 +54,7 @@ describe('DastSiteProfileForm', () => {
const findExcludedUrlsInput = () => wrapper.findByTestId('excluded-urls-input');
const findRequestHeadersInput = () => wrapper.findByTestId('request-headers-input');
const findAuthCheckbox = () => wrapper.findByTestId('auth-enable-checkbox');
const findTargetTypeOption = () => wrapper.findByTestId('site-type-option');
const findSubmitButton = () => wrapper.findByTestId('dast-site-profile-form-submit-button');
const findCancelButton = () => wrapper.findByTestId('dast-site-profile-form-cancel-button');
const findAlert = () => wrapper.findByTestId('dast-site-profile-form-alert');
......@@ -73,6 +74,28 @@ describe('DastSiteProfileForm', () => {
});
};
const fillForm = async () => {
await setFieldValue(findProfileNameInput(), profileName);
await setFieldValue(findTargetUrlInput(), targetUrl);
await setFieldValue(findExcludedUrlsInput(), excludedUrls);
await setFieldValue(findRequestHeadersInput(), requestHeaders);
await setAuthFieldsValues(siteProfileOne.auth);
};
const fillAndSubmitForm = async () => {
await fillForm();
submitForm();
};
const setTargetType = async (type) => {
const radio = wrapper
.findAll('input[type="radio"]')
.filter((r) => r.attributes('value') === type)
.at(0);
radio.element.selected = true;
radio.trigger('change');
};
const mockClientFactory = (handlers) => {
const mockClient = createMockClient();
......@@ -110,6 +133,7 @@ describe('DastSiteProfileForm', () => {
provide: {
glFeatures: {
securityDastSiteProfilesAdditionalFields: true,
securityDastSiteProfilesApiOption: true,
},
},
},
......@@ -175,10 +199,11 @@ describe('DastSiteProfileForm', () => {
createFullComponent();
});
it('should render correctly', () => {
it('should render correctly with default values', () => {
expect(findAuthSection().exists()).toBe(true);
expect(findExcludedUrlsInput().exists()).toBe(true);
expect(findRequestHeadersInput().exists()).toBe(true);
expect(findTargetTypeOption().vm.$attrs.checked).toBe('WEBSITE');
});
it('should have maxlength constraint', () => {
......@@ -216,6 +241,48 @@ describe('DastSiteProfileForm', () => {
expect(findByNameAttribute('password').element.value).toBe('');
});
});
describe('when target type is API', () => {
beforeEach(() => {
setTargetType('API');
});
it('should hide auth section', () => {
expect(findAuthSection().exists()).toBe(false);
});
describe.each`
title | siteProfile | mutationVars | mutationKind
${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'}
${'Edit site profile'} | ${siteProfileOne} | ${{ id: siteProfileOne.id }} | ${'dastSiteProfileUpdate'}
`('$title', ({ siteProfile, mutationVars, mutationKind }) => {
beforeEach(() => {
createFullComponent({
propsData: {
siteProfile,
},
});
});
it('form submission triggers correct GraphQL mutation', async () => {
await fillForm();
await setTargetType('API');
await submitForm();
expect(requestHandlers[mutationKind]).toHaveBeenCalledWith({
input: {
profileName,
targetUrl,
fullPath,
excludedUrls: siteProfileOne.excludedUrls,
requestHeaders,
targetType: 'API',
...mutationVars,
},
});
});
});
});
});
describe.each`
......@@ -240,15 +307,6 @@ describe('DastSiteProfileForm', () => {
});
describe('submission', () => {
const fillAndSubmitForm = async () => {
await setFieldValue(findProfileNameInput(), profileName);
await setFieldValue(findTargetUrlInput(), targetUrl);
await setFieldValue(findExcludedUrlsInput(), excludedUrls);
await setFieldValue(findRequestHeadersInput(), requestHeaders);
await setAuthFieldsValues(siteProfileOne.auth);
submitForm();
};
describe('on success', () => {
beforeEach(async () => {
await fillAndSubmitForm();
......@@ -267,6 +325,7 @@ describe('DastSiteProfileForm', () => {
fullPath,
auth: siteProfileOne.auth,
excludedUrls: siteProfileOne.excludedUrls,
targetType: siteProfileOne.targetType,
...mutationVars,
},
});
......@@ -359,16 +418,17 @@ describe('DastSiteProfileForm', () => {
});
});
describe('when feature flag is off', () => {
describe('when all feature flags are off', () => {
const mountOpts = {
provide: {
glFeatures: {
securityDastSiteProfilesAdditionalFields: false,
securityDastSiteProfilesApiOption: false,
},
},
};
const fillAndSubmitForm = async () => {
const fillRequiredFieldsAndSubmitForm = async () => {
await setFieldValue(findProfileNameInput(), profileName);
await setFieldValue(findTargetUrlInput(), targetUrl);
submitForm();
......@@ -380,6 +440,7 @@ describe('DastSiteProfileForm', () => {
expect(findAuthSection().exists()).toBe(false);
expect(findExcludedUrlsInput().exists()).toBe(false);
expect(findRequestHeadersInput().exists()).toBe(false);
expect(findTargetTypeOption().exists()).toBe(false);
});
describe.each`
......@@ -394,11 +455,11 @@ describe('DastSiteProfileForm', () => {
},
...mountOpts,
});
fillAndSubmitForm();
fillRequiredFieldsAndSubmitForm();
});
it('form submission triggers correct GraphQL mutation', async () => {
await fillAndSubmitForm();
await fillRequiredFieldsAndSubmitForm();
expect(requestHandlers[mutationKind]).toHaveBeenCalledWith({
input: {
profileName,
......
......@@ -108,6 +108,7 @@ RSpec.describe Projects::Security::DastSiteProfilesController, type: :request do
id: global_id_of(dast_site_profile),
name: dast_site_profile.name,
targetUrl: dast_site_profile.dast_site.url,
targetType: dast_site_profile.target_type.upcase,
excludedUrls: dast_site_profile.excluded_urls,
requestHeaders: Dast::SiteProfilePresenter::REDACTED_REQUEST_HEADERS,
auth: {
......
......@@ -10094,6 +10094,9 @@ msgstr ""
msgid "DastProfiles|Request headers"
msgstr ""
msgid "DastProfiles|Rest API"
msgstr ""
msgid "DastProfiles|Run scan"
msgstr ""
......@@ -10139,6 +10142,9 @@ msgstr ""
msgid "DastProfiles|Site name"
msgstr ""
msgid "DastProfiles|Site type"
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
......@@ -10187,6 +10193,9 @@ msgstr ""
msgid "DastProfiles|Validation status"
msgstr ""
msgid "DastProfiles|Website"
msgstr ""
msgid "DastSiteValidation|Copy HTTP header to clipboard"
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