Use existing scanner profiles in on-demand scans

Added the ability to select existing DAST scanner profiles when running
an on-demand DAST scan. This currently requires the
securityOnDemandScansScannerProfiles feature flag to be enabled.
parent aa96300a
......@@ -25,14 +25,6 @@ export default {
type: String,
required: true,
},
profilesLibraryPath: {
type: String,
required: true,
},
newSiteProfilePath: {
type: String,
required: true,
},
},
data() {
return {
......@@ -49,8 +41,6 @@ export default {
:help-page-path="helpPagePath"
:project-path="projectPath"
:default-branch="defaultBranch"
:profiles-library-path="profilesLibraryPath"
:new-site-profile-path="newSiteProfilePath"
@cancel="showForm = false"
/>
</template>
......
......@@ -14,17 +14,22 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import dastScannerProfilesQuery from 'ee/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastSiteProfilesQuery from 'ee/dast_profiles/graphql/dast_site_profiles.query.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql';
import { SCAN_TYPES } from '../constants';
const ERROR_RUN_SCAN = 'ERROR_RUN_SCAN';
const ERROR_FETCH_SCANNER_PROFILES = 'ERROR_FETCH_SCANNER_PROFILES';
const ERROR_FETCH_SITE_PROFILES = 'ERROR_FETCH_SITE_PROFILES';
const ERROR_MESSAGES = {
[ERROR_RUN_SCAN]: s__('OnDemandScans|Could not run the scan. Please try again.'),
[ERROR_FETCH_SCANNER_PROFILES]: s__(
'OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later.',
),
[ERROR_FETCH_SITE_PROFILES]: s__(
'OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later.',
),
......@@ -53,7 +58,27 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
scannerProfiles: {
query: dastScannerProfilesQuery,
variables() {
return {
fullPath: this.projectPath,
};
},
update(data) {
const scannerProfilesEdges = data?.project?.scannerProfiles?.edges ?? [];
return scannerProfilesEdges.map(({ node }) => node);
},
error(e) {
Sentry.captureException(e);
this.showErrors(ERROR_FETCH_SCANNER_PROFILES);
},
skip() {
return !this.glFeatures.securityOnDemandScansScannerProfiles;
},
},
siteProfiles: {
query: dastSiteProfilesQuery,
variables() {
......@@ -84,21 +109,29 @@ export default {
type: String,
required: true,
},
profilesLibraryPath: {
type: String,
required: true,
},
inject: {
scannerProfilesLibraryPath: {
default: '',
},
siteProfilesLibraryPath: {
default: '',
},
newScannerProfilePath: {
default: '',
},
newSiteProfilePath: {
type: String,
required: true,
default: '',
},
},
data() {
return {
scannerProfiles: [],
siteProfiles: [],
form: {
scanType: initField(SCAN_TYPES.PASSIVE),
branch: initField(this.defaultBranch),
...(this.glFeatures.securityOnDemandScansScannerProfiles
? { dastScannerProfileId: initField(null) }
: {}),
dastSiteProfileId: initField(null),
},
loading: false,
......@@ -112,10 +145,12 @@ export default {
return ERROR_MESSAGES[this.errorType] || null;
},
isLoadingProfiles() {
return this.$apollo.queries.siteProfiles.loading;
return ['scanner', 'site'].some(
profileType => this.$apollo.queries[`${profileType}Profiles`].loading,
);
},
failedToLoadProfiles() {
return [ERROR_FETCH_SITE_PROFILES].includes(this.errorType);
return [ERROR_FETCH_SCANNER_PROFILES, ERROR_FETCH_SITE_PROFILES].includes(this.errorType);
},
formData() {
return {
......@@ -132,12 +167,24 @@ export default {
isSubmitDisabled() {
return this.formHasErrors || this.someFieldEmpty;
},
selectedScannerProfile() {
const selectedScannerProfile = this.form.dastScannerProfileId.value;
return selectedScannerProfile === null
? null
: this.scannerProfiles.find(({ id }) => id === selectedScannerProfile);
},
selectedSiteProfile() {
const selectedSiteProfileId = this.form.dastSiteProfileId.value;
return selectedSiteProfileId === null
? null
: this.siteProfiles.find(({ id }) => id === selectedSiteProfileId);
},
scannerProfileText() {
const { selectedScannerProfile } = this;
return selectedScannerProfile
? selectedScannerProfile.profileName
: s__('OnDemandScans|Select one of the existing profiles');
},
siteProfileText() {
const { selectedSiteProfile } = this;
return selectedSiteProfile
......@@ -146,6 +193,9 @@ export default {
},
},
methods: {
setScannerProfile({ id }) {
this.form.dastScannerProfileId.value = id;
},
setSiteProfile({ id }) {
this.form.dastSiteProfileId.value = id;
},
......@@ -239,9 +289,105 @@ export default {
<template v-else-if="!failedToLoadProfiles">
<gl-card>
<template #header>
<h3 class="gl-font-lg gl-display-inline">{{ s__('OnDemandScans|Scanner settings') }}</h3>
<div class="row">
<div class="col-7">
<h3 class="gl-font-lg gl-display-inline">
{{ s__('OnDemandScans|Scanner settings') }}
</h3>
</div>
<div v-if="glFeatures.securityOnDemandScansScannerProfiles" class="col-5 gl-text-right">
<gl-button
:href="scannerProfiles.length ? scannerProfilesLibraryPath : null"
:disabled="!scannerProfiles.length"
variant="success"
category="secondary"
size="small"
data-testid="manage-scanner-profiles-button"
>
{{ s__('OnDemandScans|Manage profiles') }}
</gl-button>
</div>
</div>
</template>
<template v-if="glFeatures.securityOnDemandScansScannerProfiles">
<gl-form-group v-if="scannerProfiles.length">
<template #label>
{{ s__('OnDemandScans|Use existing scanner profile') }}
</template>
<gl-dropdown
v-model="form.dastScannerProfileId.value"
:text="scannerProfileText"
class="mw-460"
data-testid="scanner-profiles-dropdown"
>
<gl-dropdown-item
v-for="scannerProfile in scannerProfiles"
:key="scannerProfile.id"
:is-checked="form.dastScannerProfileId.value === scannerProfile.id"
is-check-item
@click="setScannerProfile(scannerProfile)"
>
{{ scannerProfile.profileName }}
</gl-dropdown-item>
</gl-dropdown>
<template v-if="selectedScannerProfile">
<hr />
<div data-testid="scanner-profile-summary">
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Scan mode') }}:</div>
<div class="col-md-9">
<strong>{{ s__('DastProfiles|Passive') }}</strong>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Spider timeout') }}:</div>
<div class="col-md-9">
<strong>
{{ n__('%d minute', '%d minutes', selectedScannerProfile.spiderTimeout) }}
</strong>
</div>
</div>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-3">{{ s__('DastProfiles|Target timeout') }}:</div>
<div class="col-md-9">
<strong>
{{ n__('%d second', '%d seconds', selectedScannerProfile.targetTimeout) }}
</strong>
</div>
</div>
</div>
</div>
</div>
</template>
</gl-form-group>
<template v-else>
<p class="gl-text-gray-700">
{{
s__(
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile.',
)
}}
</p>
<gl-button
:href="newScannerProfilePath"
variant="success"
category="secondary"
data-testid="create-scanner-profile-link"
>
{{ s__('OnDemandScans|Create a new scanner profile') }}
</gl-button>
</template>
</template>
<template v-else>
<gl-form-group class="gl-mt-4">
<template #label>
{{ s__('OnDemandScans|Scan mode') }}
......@@ -267,6 +413,7 @@ export default {
</template>
{{ defaultBranch }}
</gl-form-group>
</template>
</gl-card>
<gl-card>
......@@ -277,7 +424,7 @@ export default {
</div>
<div class="col-5 gl-text-right">
<gl-button
:href="siteProfiles.length ? profilesLibraryPath : null"
:href="siteProfiles.length ? siteProfilesLibraryPath : null"
:disabled="!siteProfiles.length"
variant="success"
category="secondary"
......
export const SCAN_TYPES = {
PASSIVE: 'PASSIVE',
};
mutation dastOnDemandScanCreate($fullPath: ID!, $dastSiteProfileId: DastSiteProfileID!) {
dastOnDemandScanCreate(input: { fullPath: $fullPath, dastSiteProfileId: $dastSiteProfileId }) {
mutation dastOnDemandScanCreate(
$fullPath: ID!
$dastScannerProfileId: DastScannerProfileID
$dastSiteProfileId: DastSiteProfileID!
) {
dastOnDemandScanCreate(
input: {
fullPath: $fullPath
dastScannerProfileId: $dastScannerProfileId
dastSiteProfileId: $dastSiteProfileId
}
) {
pipelineUrl
errors
}
......
......@@ -13,13 +13,21 @@ export default () => {
emptyStateSvgPath,
projectPath,
defaultBranch,
profilesLibraryPath,
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newSiteProfilePath,
newScannerProfilePath,
} = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newScannerProfilePath,
newSiteProfilePath,
},
render(h) {
return h(OnDemandScansApp, {
props: {
......@@ -27,8 +35,6 @@ export default () => {
emptyStateSvgPath,
projectPath,
defaultBranch,
profilesLibraryPath,
newSiteProfilePath,
},
});
},
......
......@@ -4,6 +4,7 @@ module Projects
class OnDemandScansController < Projects::ApplicationController
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_scanner_profiles)
end
def index
......
......@@ -7,7 +7,9 @@ module Projects::OnDemandScansHelper
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_profiles_path(project),
'scanner-profiles-library-path' => project_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_dast_scanner_profile_path(project),
'new-site-profile-path' => new_project_dast_site_profile_path(project)
}
end
......
......@@ -9,7 +9,6 @@ const helpPagePath = `${TEST_HOST}/application_security/dast/index#on-demand-sca
const projectPath = 'group/project';
const defaultBranch = 'master';
const emptyStateSvgPath = `${TEST_HOST}/assets/illustrations/alert-management-empty-state.svg`;
const profilesLibraryPath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`;
const newSiteProfilePath = `${TEST_HOST}/${projectPath}/-/on_demand_scans/profiles`;
describe('OnDemandScansApp', () => {
......@@ -39,7 +38,6 @@ describe('OnDemandScansApp', () => {
projectPath,
defaultBranch,
emptyStateSvgPath,
profilesLibraryPath,
newSiteProfilePath,
},
},
......@@ -85,8 +83,6 @@ describe('OnDemandScansApp', () => {
helpPagePath,
projectPath,
defaultBranch,
profilesLibraryPath,
newSiteProfilePath,
});
});
......
......@@ -12,7 +12,9 @@ RSpec.describe Projects::OnDemandScansHelper do
'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_profiles_path(project),
'scanner-profiles-library-path' => project_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_dast_scanner_profile_path(project),
'new-site-profile-path' => new_project_dast_site_profile_path(project)
)
end
......
......@@ -7846,6 +7846,9 @@ msgstr ""
msgid "DastProfiles|No profiles created yet"
msgstr ""
msgid "DastProfiles|Passive"
msgstr ""
msgid "DastProfiles|Please enter a valid URL format, ex: http://www.example.com/home"
msgstr ""
......@@ -7861,6 +7864,9 @@ msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
msgid "DastProfiles|Scan mode"
msgstr ""
msgid "DastProfiles|Scanner Profile"
msgstr ""
......@@ -17245,12 +17251,18 @@ msgstr ""
msgid "OnDemandScans|Attached branch is where the scan job runs."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not run the scan. Please try again."
msgstr ""
msgid "OnDemandScans|Create a new scanner profile"
msgstr ""
msgid "OnDemandScans|Create a new site profile"
msgstr ""
......@@ -17263,6 +17275,9 @@ msgstr ""
msgid "OnDemandScans|New on-demand DAST scan"
msgstr ""
msgid "OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile."
msgstr ""
msgid "OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile."
msgstr ""
......@@ -17296,6 +17311,9 @@ msgstr ""
msgid "OnDemandScans|Site profiles"
msgstr ""
msgid "OnDemandScans|Use existing scanner profile"
msgstr ""
msgid "OnDemandScans|Use existing site profile"
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