Commit f4e16423 authored by David Pisek's avatar David Pisek Committed by Nathan Friend

DAST site profiles: Add header validation

This commit adds the http header validation method to the form
that let's a user create or edit a DAST on-demand site profile.
parent d97b63cd
---
name: security_on_demand_scans_http_header_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42812
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276403
milestone: '13.6'
type: development
group: group::dynamic analysis
default_enabled: false
...@@ -11,11 +11,16 @@ import { ...@@ -11,11 +11,16 @@ import {
GlInputGroupText, GlInputGroupText,
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { omit } from 'lodash';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { cleanLeadingSeparator, joinPaths, stripPathTail } from '~/lib/utils/url_utility'; import { cleanLeadingSeparator, joinPaths, stripPathTail } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { import {
DAST_SITE_VALIDATION_HTTP_HEADER_KEY,
DAST_SITE_VALIDATION_METHOD_HTTP_HEADER,
DAST_SITE_VALIDATION_METHOD_TEXT_FILE, DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
DAST_SITE_VALIDATION_METHODS, DAST_SITE_VALIDATION_METHODS,
DAST_SITE_VALIDATION_STATUS, DAST_SITE_VALIDATION_STATUS,
...@@ -27,6 +32,7 @@ import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graph ...@@ -27,6 +32,7 @@ import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graph
export default { export default {
name: 'DastSiteValidation', name: 'DastSiteValidation',
components: { components: {
ClipboardButton,
GlAlert, GlAlert,
GlButton, GlButton,
GlCard, GlCard,
...@@ -38,6 +44,7 @@ export default { ...@@ -38,6 +44,7 @@ export default {
GlInputGroupText, GlInputGroupText,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [glFeatureFlagsMixin()],
apollo: { apollo: {
dastSiteValidation: { dastSiteValidation: {
query: dastSiteValidationQuery, query: dastSiteValidationQuery,
...@@ -103,6 +110,16 @@ export default { ...@@ -103,6 +110,16 @@ export default {
}; };
}, },
computed: { computed: {
validationMethodOptions() {
const isHttpHeaderValidationEnabled = this.glFeatures
.securityOnDemandScansHttpHeaderValidation;
const enabledValidationMethods = omit(DAST_SITE_VALIDATION_METHODS, [
!isHttpHeaderValidationEnabled ? DAST_SITE_VALIDATION_METHOD_HTTP_HEADER : '',
]);
return Object.values(enabledValidationMethods);
},
urlObject() { urlObject() {
try { try {
return new URL(this.targetUrl); return new URL(this.targetUrl);
...@@ -119,12 +136,18 @@ export default { ...@@ -119,12 +136,18 @@ export default {
isTextFileValidation() { isTextFileValidation() {
return this.validationMethod === DAST_SITE_VALIDATION_METHOD_TEXT_FILE; return this.validationMethod === DAST_SITE_VALIDATION_METHOD_TEXT_FILE;
}, },
isHttpHeaderValidation() {
return this.validationMethod === DAST_SITE_VALIDATION_METHOD_HTTP_HEADER;
},
textFileName() { textFileName() {
return `GitLab-DAST-Site-Validation-${this.token}.txt`; return `GitLab-DAST-Site-Validation-${this.token}.txt`;
}, },
locationStepLabel() { locationStepLabel() {
return DAST_SITE_VALIDATION_METHODS[this.validationMethod].i18n.locationStepLabel; return DAST_SITE_VALIDATION_METHODS[this.validationMethod].i18n.locationStepLabel;
}, },
httpHeader() {
return `${DAST_SITE_VALIDATION_HTTP_HEADER_KEY}: uuid-code-${this.token}`;
},
}, },
watch: { watch: {
targetUrl() { targetUrl() {
...@@ -132,13 +155,22 @@ export default { ...@@ -132,13 +155,22 @@ export default {
}, },
}, },
created() { created() {
this.unsubscribe = this.$watch(() => this.token, this.updateValidationPath, { this.unsubscribe = this.$watch(
immediate: true, () => [this.token, this.validationMethod],
}); this.updateValidationPath,
{
immediate: true,
},
);
}, },
methods: { methods: {
updateValidationPath() { updateValidationPath() {
this.validationPath = joinPaths(stripPathTail(this.path), this.textFileName); this.validationPath = this.isTextFileValidation
? this.getTextFileValidationPath()
: this.path;
},
getTextFileValidationPath() {
return joinPaths(stripPathTail(this.path), this.textFileName);
}, },
onValidationPathInput() { onValidationPathInput() {
this.unsubscribe(); this.unsubscribe();
...@@ -189,7 +221,6 @@ export default { ...@@ -189,7 +221,6 @@ export default {
this.hasValidationError = true; this.hasValidationError = true;
}, },
}, },
validationMethodOptions: Object.values(DAST_SITE_VALIDATION_METHODS),
}; };
</script> </script>
...@@ -199,7 +230,7 @@ export default { ...@@ -199,7 +230,7 @@ export default {
{{ s__('DastProfiles|Site is not validated yet, please follow the steps.') }} {{ s__('DastProfiles|Site is not validated yet, please follow the steps.') }}
</gl-alert> </gl-alert>
<gl-form-group :label="s__('DastProfiles|Step 1 - Choose site validation method')"> <gl-form-group :label="s__('DastProfiles|Step 1 - Choose site validation method')">
<gl-form-radio-group v-model="validationMethod" :options="$options.validationMethodOptions" /> <gl-form-radio-group v-model="validationMethod" :options="validationMethodOptions" />
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
v-if="isTextFileValidation" v-if="isTextFileValidation"
...@@ -217,6 +248,16 @@ export default { ...@@ -217,6 +248,16 @@ export default {
{{ textFileName }} {{ textFileName }}
</gl-button> </gl-button>
</gl-form-group> </gl-form-group>
<gl-form-group
v-else-if="isHttpHeaderValidation"
:label="s__('DastProfiles|Step 2 - Add following HTTP header to your site')"
>
<code class="gl-p-3 gl-bg-black gl-text-white">{{ httpHeader }}</code>
<clipboard-button
:text="httpHeader"
:title="s__('DastProfiles|Copy HTTP header to clipboard')"
/>
</gl-form-group>
<gl-form-group :label="locationStepLabel" class="mw-460"> <gl-form-group :label="locationStepLabel" class="mw-460">
<gl-form-input-group> <gl-form-input-group>
<template #prepend> <template #prepend>
...@@ -255,7 +296,7 @@ export default { ...@@ -255,7 +296,7 @@ export default {
<gl-icon name="status_failed" /> <gl-icon name="status_failed" />
{{ {{
s__( s__(
'DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method.', 'DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method.',
) )
}} }}
</template> </template>
......
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const DAST_SITE_VALIDATION_METHOD_TEXT_FILE = 'TEXT_FILE'; export const DAST_SITE_VALIDATION_METHOD_TEXT_FILE = 'TEXT_FILE';
export const DAST_SITE_VALIDATION_METHOD_HTTP_HEADER = 'HTTP_HEADER';
export const DAST_SITE_VALIDATION_METHODS = { export const DAST_SITE_VALIDATION_METHODS = {
[DAST_SITE_VALIDATION_METHOD_TEXT_FILE]: { [DAST_SITE_VALIDATION_METHOD_TEXT_FILE]: {
value: DAST_SITE_VALIDATION_METHOD_TEXT_FILE, value: DAST_SITE_VALIDATION_METHOD_TEXT_FILE,
...@@ -9,6 +11,13 @@ export const DAST_SITE_VALIDATION_METHODS = { ...@@ -9,6 +11,13 @@ export const DAST_SITE_VALIDATION_METHODS = {
locationStepLabel: s__('DastProfiles|Step 3 - Confirm text file location and validate'), locationStepLabel: s__('DastProfiles|Step 3 - Confirm text file location and validate'),
}, },
}, },
[DAST_SITE_VALIDATION_METHOD_HTTP_HEADER]: {
value: DAST_SITE_VALIDATION_METHOD_HTTP_HEADER,
text: s__('DastProfiles|Header validation'),
i18n: {
locationStepLabel: s__('DastProfiles|Step 3 - Confirm header location and validate'),
},
},
}; };
export const DAST_SITE_VALIDATION_STATUS = { export const DAST_SITE_VALIDATION_STATUS = {
...@@ -19,3 +28,4 @@ export const DAST_SITE_VALIDATION_STATUS = { ...@@ -19,3 +28,4 @@ export const DAST_SITE_VALIDATION_STATUS = {
}; };
export const DAST_SITE_VALIDATION_POLL_INTERVAL = 1000; export const DAST_SITE_VALIDATION_POLL_INTERVAL = 1000;
export const DAST_SITE_VALIDATION_HTTP_HEADER_KEY = 'Gitlab-On-Demand-DAST';
...@@ -6,6 +6,7 @@ module Projects ...@@ -6,6 +6,7 @@ module Projects
before_action do before_action do
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_site_validation, @project)
push_frontend_feature_flag(:security_on_demand_scans_http_header_validation, @project)
end end
feature_category :dynamic_application_security_testing feature_category :dynamic_application_security_testing
......
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { within } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils';
import { createMockClient } from 'mock-apollo-client'; import { createMockClient } from 'mock-apollo-client';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'jest/helpers/wait_for_promises'; import waitForPromises from 'jest/helpers/wait_for_promises';
...@@ -11,6 +11,7 @@ import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profile ...@@ -11,6 +11,7 @@ import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profile
import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock'; import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock';
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants'; import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
jest.mock('~/lib/utils/downloader'); jest.mock('~/lib/utils/downloader');
...@@ -22,6 +23,8 @@ const targetUrl = 'https://example.com/'; ...@@ -22,6 +23,8 @@ const targetUrl = 'https://example.com/';
const tokenId = '1'; const tokenId = '1';
const token = 'validation-token-123'; const token = 'validation-token-123';
const validationMethods = ['text file', 'header'];
const defaultProps = { const defaultProps = {
fullPath, fullPath,
targetUrl, targetUrl,
...@@ -72,6 +75,9 @@ describe('DastSiteValidation', () => { ...@@ -72,6 +75,9 @@ describe('DastSiteValidation', () => {
{}, {},
{ {
propsData: defaultProps, propsData: defaultProps,
provide: {
glFeatures: { securityOnDemandScansHttpHeaderValidation: true },
},
}, },
options, options,
{ {
...@@ -93,8 +99,14 @@ describe('DastSiteValidation', () => { ...@@ -93,8 +99,14 @@ describe('DastSiteValidation', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findErrorMessage = () => const findErrorMessage = () =>
withinComponent().queryByText( withinComponent().queryByText(
/validation failed, please make sure that you follow the steps above with the choosen method./i, /validation failed, please make sure that you follow the steps above with the chosen method./i,
); );
const findRadioInputForValidationMethod = validationMethod =>
withinComponent().queryByRole('radio', {
name: new RegExp(`${validationMethod} validation`, 'i'),
});
const enableValidationMethod = validationMethod =>
createWrapper(findRadioInputForValidationMethod(validationMethod)).trigger('click');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -105,10 +117,6 @@ describe('DastSiteValidation', () => { ...@@ -105,10 +117,6 @@ describe('DastSiteValidation', () => {
createFullComponent(); createFullComponent();
}); });
it('renders properly', () => {
expect(wrapper.html()).not.toBe('');
});
it('renders a download button containing the token', () => { it('renders a download button containing the token', () => {
const downloadButton = withinComponent().getByRole('button', { const downloadButton = withinComponent().getByRole('button', {
name: 'Download validation text file', name: 'Download validation text file',
...@@ -116,6 +124,10 @@ describe('DastSiteValidation', () => { ...@@ -116,6 +124,10 @@ describe('DastSiteValidation', () => {
expect(downloadButton).not.toBeNull(); expect(downloadButton).not.toBeNull();
}); });
it.each(validationMethods)('renders a radio input for "%s" validation', validationMethod => {
expect(findRadioInputForValidationMethod(validationMethod)).not.toBe(null);
});
it('renders an input group with the target URL prepended', () => { it('renders an input group with the target URL prepended', () => {
const inputGroup = withinComponent().getByRole('group', { const inputGroup = withinComponent().getByRole('group', {
name: 'Step 3 - Confirm text file location and validate', name: 'Step 3 - Confirm text file location and validate',
...@@ -125,77 +137,147 @@ describe('DastSiteValidation', () => { ...@@ -125,77 +137,147 @@ describe('DastSiteValidation', () => {
}); });
}); });
describe('text file validation', () => { describe('validation methods', () => {
it('clicking on the download button triggers a download of a text file containing the token', () => { describe.each(validationMethods)('common behaviour', validationMethod => {
createComponent(); const expectedFileName = `GitLab-DAST-Site-Validation-${token}.txt`;
findDownloadButton().vm.$emit('click');
describe.each`
targetUrl | expectedPrefix | expectedPath | expectedTextFilePath
${'https://example.com'} | ${'https://example.com/'} | ${''} | ${`${expectedFileName}`}
${'https://example.com/'} | ${'https://example.com/'} | ${''} | ${`${expectedFileName}`}
${'https://example.com/foo/bar'} | ${'https://example.com/'} | ${'foo/bar'} | ${`foo/${expectedFileName}`}
${'https://example.com/foo/bar/'} | ${'https://example.com/'} | ${'foo/bar/'} | ${`foo/bar/${expectedFileName}`}
${'https://sub.example.com/foo/bar'} | ${'https://sub.example.com/'} | ${'foo/bar'} | ${`foo/${expectedFileName}`}
${'https://example.com/foo/index.html'} | ${'https://example.com/'} | ${'foo/index.html'} | ${`foo/${expectedFileName}`}
${'https://example.com/foo/?bar="baz"'} | ${'https://example.com/'} | ${'foo/'} | ${`foo/${expectedFileName}`}
${'https://example.com:3000'} | ${'https://example.com:3000/'} | ${''} | ${`${expectedFileName}`}
${''} | ${''} | ${''} | ${`${expectedFileName}`}
`(
`validation path input when validationMethod is "${validationMethod}" and target URL is "$targetUrl"`,
({ targetUrl: url, expectedPrefix, expectedPath, expectedTextFilePath }) => {
beforeEach(async () => {
createFullComponent({
propsData: {
targetUrl: url,
},
});
await wrapper.vm.$nextTick();
enableValidationMethod(validationMethod);
});
expect(download).toHaveBeenCalledWith({ const expectedValue =
fileName: `GitLab-DAST-Site-Validation-${token}.txt`, validationMethod === 'text file' ? expectedTextFilePath : expectedPath;
fileData: btoa(token),
});
});
describe.each` it(`prefix is set to "${expectedPrefix}"`, () => {
targetUrl | expectedPrefix | expectedValue expect(findValidationPathPrefix().text()).toBe(expectedPrefix);
${'https://example.com'} | ${'https://example.com/'} | ${'GitLab-DAST-Site-Validation-validation-token-123.txt'} });
${'https://example.com/'} | ${'https://example.com/'} | ${'GitLab-DAST-Site-Validation-validation-token-123.txt'}
${'https://example.com/foo/bar'} | ${'https://example.com/'} | ${'foo/GitLab-DAST-Site-Validation-validation-token-123.txt'} it(`input value defaults to "${expectedValue}"`, () => {
${'https://example.com/foo/bar/'} | ${'https://example.com/'} | ${'foo/bar/GitLab-DAST-Site-Validation-validation-token-123.txt'} expect(findValidationPathInput().element.value).toBe(expectedValue);
${'https://sub.example.com/foo/bar'} | ${'https://sub.example.com/'} | ${'foo/GitLab-DAST-Site-Validation-validation-token-123.txt'}
${'https://example.com/foo/index.html'} | ${'https://example.com/'} | ${'foo/GitLab-DAST-Site-Validation-validation-token-123.txt'}
${'https://example.com/foo/?bar="baz"'} | ${'https://example.com/'} | ${'foo/GitLab-DAST-Site-Validation-validation-token-123.txt'}
${'https://example.com:3000'} | ${'https://example.com:3000/'} | ${'GitLab-DAST-Site-Validation-validation-token-123.txt'}
${''} | ${''} | ${'GitLab-DAST-Site-Validation-validation-token-123.txt'}
`(
'validation path input when target URL is $targetUrl',
({ targetUrl: url, expectedPrefix, expectedValue }) => {
beforeEach(() => {
createFullComponent({
propsData: {
targetUrl: url,
},
}); });
},
);
it("input value isn't automatically updated if it has been changed manually", async () => {
createFullComponent();
const customValidationPath = 'custom/validation/path.txt';
findValidationPathInput().setValue(customValidationPath);
await wrapper.setProps({
token: 'a-completely-new-token',
}); });
it(`prefix is set to ${expectedPrefix}`, () => { expect(findValidationPathInput().element.value).toBe(customValidationPath);
expect(findValidationPathPrefix().text()).toBe(expectedPrefix); });
});
describe('text file validation', () => {
it('clicking on the download button triggers a download of a text file containing the token', () => {
createComponent();
findDownloadButton().vm.$emit('click');
expect(download).toHaveBeenCalledWith({
fileName: `GitLab-DAST-Site-Validation-${token}.txt`,
fileData: btoa(token),
}); });
});
});
describe('header validation', () => {
beforeEach(async () => {
createFullComponent();
await wrapper.vm.$nextTick();
enableValidationMethod('header');
});
it(`input value defaults to ${expectedValue}`, () => { it.each([
expect(findValidationPathInput().element.value).toBe(expectedValue); /step 2 - add following http header to your site/i,
/step 3 - confirm header location and validate/i,
])('shows the correct descriptions', descriptionText => {
expect(withinComponent().getByText(descriptionText)).not.toBe(null);
});
it('shows a code block containing the http-header key with the given token', () => {
expect(
withinComponent().getByText(`Gitlab-On-Demand-DAST: uuid-code-${token}`, {
selector: 'code',
}),
).not.toBe(null);
});
it('shows a button that copies the http-header to the clipboard', () => {
const clipboardButton = wrapper.find(ClipboardButton);
expect(clipboardButton.exists()).toBe(true);
expect(clipboardButton.props()).toMatchObject({
text: `Gitlab-On-Demand-DAST: uuid-code-${token}`,
title: 'Copy HTTP header to clipboard',
}); });
}, });
); });
});
it("input value isn't automatically updated if it has been changed manually", async () => { describe('with the "securityOnDemandScansHttpHeaderValidation" feature flag disabled', () => {
createFullComponent(); beforeEach(() => {
const customValidationPath = 'custom/validation/path.txt'; createFullComponent({
findValidationPathInput().setValue(customValidationPath); provide: {
await wrapper.setProps({ glFeatures: {
token: 'a-completely-new-token', securityOnDemandScansHttpHeaderValidation: false,
},
},
}); });
});
expect(findValidationPathInput().element.value).toBe(customValidationPath); it('does not render the http-header validation method', () => {
expect(findRadioInputForValidationMethod('header')).toBe(null);
}); });
}); });
describe('validation', () => { describe.each(validationMethods)('"%s" validation submission', validationMethod => {
beforeEach(() => { beforeEach(() => {
createComponent(); createFullComponent();
}); });
describe('passed', () => { describe('passed', () => {
beforeEach(() => { beforeEach(() => {
findValidateButton().vm.$emit('click'); enableValidationMethod(validationMethod);
}); });
it('while validating, shows a loading state', () => { it('while validating, shows a loading state', async () => {
findValidateButton().trigger('click');
await wrapper.vm.$nextTick();
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
expect(wrapper.text()).toContain('Validating...'); expect(wrapper.text()).toContain('Validating...');
}); });
it('triggers the dastSiteValidationCreate GraphQL mutation', () => { it('triggers the dastSiteValidationCreate GraphQL mutation', () => {
findValidateButton().trigger('click');
expect(requestHandlers.dastSiteValidationCreate).toHaveBeenCalledWith({ expect(requestHandlers.dastSiteValidationCreate).toHaveBeenCalledWith({
projectFullPath: fullPath, projectFullPath: fullPath,
dastSiteTokenId: tokenId, dastSiteTokenId: tokenId,
...@@ -205,6 +287,14 @@ describe('DastSiteValidation', () => { ...@@ -205,6 +287,14 @@ describe('DastSiteValidation', () => {
}); });
it('on success, emits success event', async () => { it('on success, emits success event', async () => {
respondWith({
dastSiteValidation: jest
.fn()
.mockResolvedValue(responses.dastSiteValidation('PASSED_VALIDATION')),
});
findValidateButton().trigger('click');
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted('success')).toHaveLength(1); expect(wrapper.emitted('success')).toHaveLength(1);
......
...@@ -14,7 +14,7 @@ export const dastSiteValidation = (status = DAST_SITE_VALIDATION_STATUS.PENDING) ...@@ -14,7 +14,7 @@ export const dastSiteValidation = (status = DAST_SITE_VALIDATION_STATUS.PENDING)
export const dastSiteValidationCreate = (errors = []) => ({ export const dastSiteValidationCreate = (errors = []) => ({
data: { data: {
dastSiteValidationCreate: { status: DAST_SITE_VALIDATION_STATUS.PASSED, id: '1', errors }, dastSiteValidationCreate: { status: DAST_SITE_VALIDATION_STATUS.PENDING, id: '1', errors },
}, },
}); });
......
...@@ -8319,6 +8319,9 @@ msgstr "" ...@@ -8319,6 +8319,9 @@ msgstr ""
msgid "DastProfiles|Authentication URL" msgid "DastProfiles|Authentication URL"
msgstr "" msgstr ""
msgid "DastProfiles|Copy HTTP header to clipboard"
msgstr ""
msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later." msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -8385,6 +8388,9 @@ msgstr "" ...@@ -8385,6 +8388,9 @@ msgstr ""
msgid "DastProfiles|Error Details" msgid "DastProfiles|Error Details"
msgstr "" msgstr ""
msgid "DastProfiles|Header validation"
msgstr ""
msgid "DastProfiles|Hide debug messages" msgid "DastProfiles|Hide debug messages"
msgstr "" msgstr ""
...@@ -8469,9 +8475,15 @@ msgstr "" ...@@ -8469,9 +8475,15 @@ msgstr ""
msgid "DastProfiles|Step 1 - Choose site validation method" msgid "DastProfiles|Step 1 - Choose site validation method"
msgstr "" msgstr ""
msgid "DastProfiles|Step 2 - Add following HTTP header to your site"
msgstr ""
msgid "DastProfiles|Step 2 - Add following text to the target site" msgid "DastProfiles|Step 2 - Add following text to the target site"
msgstr "" msgstr ""
msgid "DastProfiles|Step 3 - Confirm header location and validate"
msgstr ""
msgid "DastProfiles|Step 3 - Confirm text file location and validate" msgid "DastProfiles|Step 3 - Confirm text file location and validate"
msgstr "" msgstr ""
...@@ -8508,7 +8520,7 @@ msgstr "" ...@@ -8508,7 +8520,7 @@ msgstr ""
msgid "DastProfiles|Validating..." msgid "DastProfiles|Validating..."
msgstr "" msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method." msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method."
msgstr "" msgstr ""
msgid "DastProfiles|Validation failed. Please try again." msgid "DastProfiles|Validation failed. Please try again."
......
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