From b236c6fddd3078492721f046931c6e12b91c00f5 Mon Sep 17 00:00:00 2001 From: atiwari71 Date: Thu, 12 Aug 2021 16:23:38 +0530 Subject: [PATCH] Update DastProfileCreate mutation to include DastProfileSchedule Updated Dast Profile Create mutation to include schedule Changelog: changed EE: true --- doc/api/graphql/reference/index.md | 36 +++++++++ .../graphql/mutations/dast/profiles/create.rb | 32 ++++++-- .../types/dast/profile_cadence_input_type.rb | 18 +++++ .../types/dast/profile_cadence_unit_enum.rb | 15 ++++ .../types/dast/profile_schedule_input_type.rb | 25 ++++++ .../app_sec/dast/profiles/create_service.rb | 78 +++++++++++++++---- ee/spec/factories/dast/profile_schedules.rb | 3 +- .../mutations/dast/profiles/create_spec.rb | 28 ++++++- .../types/dast/profile_cadence_enum_spec.rb | 11 +++ .../dast/profile_cadence_input_type_spec.rb | 13 ++++ .../dast/profile_schedule_input_type_spec.rb | 13 ++++ .../mutations/dast/profiles/create_spec.rb | 27 +++++++ .../dast/profiles/create_service_spec.rb | 67 +++++++++++++++- 13 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 ee/app/graphql/types/dast/profile_cadence_input_type.rb create mode 100644 ee/app/graphql/types/dast/profile_cadence_unit_enum.rb create mode 100644 ee/app/graphql/types/dast/profile_schedule_input_type.rb create mode 100644 ee/spec/graphql/types/dast/profile_cadence_enum_spec.rb create mode 100644 ee/spec/graphql/types/dast/profile_cadence_input_type_spec.rb create mode 100644 ee/spec/graphql/types/dast/profile_schedule_input_type_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4622618db42eec..d6fe56bea735d4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1437,6 +1437,7 @@ Input type: `DastProfileCreateInput` | ---- | ---- | ----------- | | `branchName` | [`String`](#string) | Associated branch. | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST Profile Schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled. | | `dastScannerProfileId` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be associated. | | `dastSiteProfileId` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be associated. | | `description` | [`String`](#string) | Description of the profile. Defaults to an empty string. | @@ -15179,6 +15180,17 @@ Status of a container repository. | `DELETE_FAILED` | Delete Failed status. | | `DELETE_SCHEDULED` | Delete Scheduled status. | +### `DastProfileCadenceUnit` + +Unit for the duration of Dast Profile Cadence. + +| Value | Description | +| ----- | ----------- | +| `DAY` | DAST Profile Cadence duration in days. | +| `MONTH` | DAST Profile Cadence duration in months. | +| `WEEK` | DAST Profile Cadence duration in weeks. | +| `YEAR` | DAST Profile Cadence duration in years. | + ### `DastScanTypeEnum` | Value | Description | @@ -17396,6 +17408,30 @@ Field that are available while modifying the custom mapping attributes for an HT | `name` | [`String`](#string) | New name for the compliance framework. | | `pipelineConfigurationFullPath` | [`String`](#string) | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hipaa` **(ULTIMATE)**. | +### `DastProfileCadenceInput` + +Represents DAST Profile Cadence. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `duration` | [`Int`](#int) | Duration of the DAST Profile Cadence. | +| `unit` | [`DastProfileCadenceUnit`](#dastprofilecadenceunit) | Unit for the duration of DAST Profile Cadence. | + +### `DastProfileScheduleInput` + +Input type for DAST Profile Schedules. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `active` | [`Boolean`](#boolean) | Status of a Dast Profile Schedule. | +| `cadence` | [`DastProfileCadenceInput`](#dastprofilecadenceinput) | Cadence of a Dast Profile Schedule. | +| `startsAt` | [`Time`](#time) | Start time of a Dast Profile Schedule. | +| `timezone` | [`String`](#string) | Time Zone for the Start time of a Dast Profile Schedule. | + ### `DastSiteProfileAuthInput` Input type for DastSiteProfile authentication. diff --git a/ee/app/graphql/mutations/dast/profiles/create.rb b/ee/app/graphql/mutations/dast/profiles/create.rb index 0e26e765ca0e39..33c10d74a99e67 100644 --- a/ee/app/graphql/mutations/dast/profiles/create.rb +++ b/ee/app/graphql/mutations/dast/profiles/create.rb @@ -20,6 +20,10 @@ class Create < BaseMutation required: true, description: 'Project the profile belongs to.' + argument :dast_profile_schedule, ::Types::Dast::ProfileScheduleInputType, + required: false, + description: 'Represents a DAST Profile Schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled.' + argument :name, GraphQL::Types::String, required: true, description: 'Name of the profile.' @@ -48,9 +52,9 @@ class Create < BaseMutation authorize :create_on_demand_dast_scan - def resolve(full_path:, name:, description: '', branch_name: nil, dast_site_profile_id:, dast_scanner_profile_id:, run_after_create: false) + def resolve(full_path:, name:, description: '', branch_name: nil, dast_site_profile_id:, dast_scanner_profile_id:, run_after_create: false, dast_profile_schedule: nil) project = authorized_find!(full_path) - raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project, dast_profile_schedule) # TODO: remove explicit coercion once compatibility layer is removed # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 @@ -70,19 +74,35 @@ def resolve(full_path:, name:, description: '', branch_name: nil, dast_site_prof branch_name: branch_name, dast_site_profile: dast_site_profile, dast_scanner_profile: dast_scanner_profile, - run_after_create: run_after_create + run_after_create: run_after_create, + dast_profile_schedule: dast_profile_schedule } ).execute return { errors: response.errors } if response.error? - { errors: [], dast_profile: response.payload.fetch(:dast_profile), pipeline_url: response.payload.fetch(:pipeline_url) } + build_response(response.payload) end private - def allowed?(project) - project.feature_available?(:security_on_demand_scans) + def allowed?(project, dast_profile_schedule) + scheduler_flag_enabled?(dast_profile_schedule, project) + end + + def scheduler_flag_enabled?(dast_profile_schedule, project) + return true unless dast_profile_schedule + + Feature.enabled?(:dast_on_demand_scans_scheduler, project, default_enabled: :yaml) + end + + def build_response(payload) + { + errors: [], + dast_profile: payload.fetch(:dast_profile), + pipeline_url: payload.fetch(:pipeline_url), + dast_profile_schedule: payload.fetch(:dast_profile_schedule) + } end end end diff --git a/ee/app/graphql/types/dast/profile_cadence_input_type.rb b/ee/app/graphql/types/dast/profile_cadence_input_type.rb new file mode 100644 index 00000000000000..d63ab54754b775 --- /dev/null +++ b/ee/app/graphql/types/dast/profile_cadence_input_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Dast + class ProfileCadenceInputType < BaseInputObject + graphql_name 'DastProfileCadenceInput' + description 'Represents DAST Profile Cadence.' + + argument :unit, ::Types::Dast::ProfileCadenceUnitEnum, + required: false, + description: 'Unit for the duration of DAST Profile Cadence.' + + argument :duration, GraphQL::Types::Int, + required: false, + description: 'Duration of the DAST Profile Cadence.' + end + end +end diff --git a/ee/app/graphql/types/dast/profile_cadence_unit_enum.rb b/ee/app/graphql/types/dast/profile_cadence_unit_enum.rb new file mode 100644 index 00000000000000..d374c97fcf8961 --- /dev/null +++ b/ee/app/graphql/types/dast/profile_cadence_unit_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Dast + class ProfileCadenceUnitEnum < BaseEnum + graphql_name 'DastProfileCadenceUnit' + description 'Unit for the duration of Dast Profile Cadence.' + + value 'DAY', value: 'day', description: 'DAST Profile Cadence duration in days.' + value 'WEEK', value: 'week', description: 'DAST Profile Cadence duration in weeks.' + value 'MONTH', value: 'month', description: 'DAST Profile Cadence duration in months.' + value 'YEAR', value: 'year', description: 'DAST Profile Cadence duration in years.' + end + end +end diff --git a/ee/app/graphql/types/dast/profile_schedule_input_type.rb b/ee/app/graphql/types/dast/profile_schedule_input_type.rb new file mode 100644 index 00000000000000..bd6aec857bf1f1 --- /dev/null +++ b/ee/app/graphql/types/dast/profile_schedule_input_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Dast + class ProfileScheduleInputType < BaseInputObject + graphql_name 'DastProfileScheduleInput' + description 'Input type for DAST Profile Schedules' + argument :active, GraphQL::Types::Boolean, + required: false, + description: 'Status of a Dast Profile Schedule.' + + argument :starts_at, Types::TimeType, + required: false, + description: 'Start time of a Dast Profile Schedule.' + + argument :timezone, GraphQL::Types::String, + required: false, + description: 'Time Zone for the Start time of a Dast Profile Schedule.' + + argument :cadence, ::Types::Dast::ProfileCadenceInputType, + required: false, + description: 'Cadence of a Dast Profile Schedule.' + end + end +end diff --git a/ee/app/services/app_sec/dast/profiles/create_service.rb b/ee/app/services/app_sec/dast/profiles/create_service.rb index 93660242590f8b..168f4ae7504e4b 100644 --- a/ee/app/services/app_sec/dast/profiles/create_service.rb +++ b/ee/app/services/app_sec/dast/profiles/create_service.rb @@ -7,7 +7,38 @@ class CreateService < BaseContainerService def execute return ServiceResponse.error(message: 'Insufficient permissions') unless allowed? - dast_profile = ::Dast::Profile.create!( + ApplicationRecord.transaction do + @dast_profile = create_profile + @schedule = create_schedule(@dast_profile) if params.dig(:dast_profile_schedule, :active) + end + + create_audit_event(@dast_profile, @schedule) + + if params.fetch(:run_after_create) + on_demand_scan = create_on_demand_scan(@dast_profile) + + return on_demand_scan if on_demand_scan.error? + + pipeline_url = on_demand_scan.payload.fetch(:pipeline_url) + end + + ServiceResponse.success( + payload: { + dast_profile: @dast_profile, + pipeline_url: pipeline_url, + dast_profile_schedule: @schedule + } + ) + rescue ActiveRecord::RecordInvalid => err + ServiceResponse.error(message: err.record.errors.full_messages) + rescue KeyError => err + ServiceResponse.error(message: err.message.capitalize) + end + + private + + def create_profile + ::Dast::Profile.create!( project: container, name: params.fetch(:name), description: params.fetch(:description), @@ -15,28 +46,27 @@ def execute dast_site_profile: dast_site_profile, dast_scanner_profile: dast_scanner_profile ) + end - create_audit_event(dast_profile) - - return ServiceResponse.success(payload: { dast_profile: dast_profile, pipeline_url: nil }) unless params.fetch(:run_after_create) + def create_schedule(dast_profile) + ::Dast::ProfileSchedule.create!( + owner: current_user, + dast_profile: dast_profile, + project_id: container.id, + cadence: dast_profile_schedule[:cadence], + timezone: dast_profile_schedule[:timezone], + starts_at: dast_profile_schedule[:starts_at] + ) + end - response = ::DastOnDemandScans::CreateService.new( + def create_on_demand_scan(dast_profile) + ::DastOnDemandScans::CreateService.new( container: container, current_user: current_user, params: { dast_profile: dast_profile } ).execute - - return response if response.error? - - ServiceResponse.success(payload: { dast_profile: dast_profile, pipeline_url: response.payload.fetch(:pipeline_url) }) - rescue ActiveRecord::RecordInvalid => err - ServiceResponse.error(message: err.record.errors.full_messages) - rescue KeyError => err - ServiceResponse.error(message: err.message.capitalize) end - private - def allowed? container.licensed_feature_available?(:security_on_demand_scans) end @@ -49,14 +79,28 @@ def dast_scanner_profile @dast_scanner_profile ||= params.fetch(:dast_scanner_profile) end - def create_audit_event(profile) + def dast_profile_schedule + params[:dast_profile_schedule] + end + + def create_audit_event(dast_profile, schedule) ::Gitlab::Audit::Auditor.audit( name: 'dast_profile_create', author: current_user, scope: container, - target: profile, + target: dast_profile, message: "Added DAST profile" ) + + if schedule + ::Gitlab::Audit::Auditor.audit( + name: 'dast_profile_schedule_create', + author: current_user, + scope: container, + target: schedule, + message: 'Added DAST profile schedule' + ) + end end end end diff --git a/ee/spec/factories/dast/profile_schedules.rb b/ee/spec/factories/dast/profile_schedules.rb index d00f03fc308263..e0618e2e05ef22 100644 --- a/ee/spec/factories/dast/profile_schedules.rb +++ b/ee/spec/factories/dast/profile_schedules.rb @@ -5,8 +5,9 @@ project dast_profile owner { association(:user) } - timezone { FFaker::Address.time_zone } + timezone { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier }.sample } starts_at { Time.now } cadence { { unit: %w(day month year week).sample, duration: 1 } } + active { true } end end diff --git a/ee/spec/graphql/mutations/dast/profiles/create_spec.rb b/ee/spec/graphql/mutations/dast/profiles/create_spec.rb index 01646441c0da8e..5e9ac96b9b758e 100644 --- a/ee/spec/graphql/mutations/dast/profiles/create_spec.rb +++ b/ee/spec/graphql/mutations/dast/profiles/create_spec.rb @@ -14,6 +14,7 @@ let(:run_after_create) { false } let(:dast_profile) { Dast::Profile.find_by(project: project, name: name) } + let(:dast_profile_schedule) { nil } subject(:mutation) { described_class.new(object: nil, context: { current_user: developer }, field: nil) } @@ -32,7 +33,8 @@ branch_name: branch_name, dast_site_profile_id: dast_site_profile.to_global_id.to_s, dast_scanner_profile_id: dast_scanner_profile.to_global_id.to_s, - run_after_create: run_after_create + run_after_create: run_after_create, + dast_profile_schedule: dast_profile_schedule ) end @@ -52,6 +54,30 @@ let(:delegated_params) { hash_including(dast_profile: instance_of(Dast::Profile)) } end end + + context 'when dast_on_demand_scans_scheduler feature is enabled' do + let(:dast_profile_schedule) { attributes_for(:dast_profile_schedule) } + + before do + stub_feature_flags(dast_on_demand_scans_scheduler: true) + end + + it 'returns the dast_profile_schedule' do + expect(subject[:dast_profile_schedule]).to eq(dast_profile.dast_profile_schedule) + end + end + + context 'when dast_on_demand_scans_scheduler feature is disabled' do + let(:dast_profile_schedule) { attributes_for(:dast_profile_schedule) } + + before do + stub_feature_flags(dast_on_demand_scans_scheduler: false) + end + + it 'returns the dast_profile_schedule' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end end end end diff --git a/ee/spec/graphql/types/dast/profile_cadence_enum_spec.rb b/ee/spec/graphql/types/dast/profile_cadence_enum_spec.rb new file mode 100644 index 00000000000000..996fcd5b1ea075 --- /dev/null +++ b/ee/spec/graphql/types/dast/profile_cadence_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DastProfileCadenceUnit'] do + it 'exposes all alert field names' do + expect(described_class.values.keys).to match_array( + %w(DAY WEEK MONTH YEAR) + ) + end +end diff --git a/ee/spec/graphql/types/dast/profile_cadence_input_type_spec.rb b/ee/spec/graphql/types/dast/profile_cadence_input_type_spec.rb new file mode 100644 index 00000000000000..0dc7668f27e01a --- /dev/null +++ b/ee/spec/graphql/types/dast/profile_cadence_input_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DastProfileCadenceInput'] do + include GraphqlHelpers + + specify { expect(described_class.graphql_name).to eq('DastProfileCadenceInput') } + + it 'has the correct arguments' do + expect(described_class.arguments.keys).to match_array(%w[unit duration]) + end +end diff --git a/ee/spec/graphql/types/dast/profile_schedule_input_type_spec.rb b/ee/spec/graphql/types/dast/profile_schedule_input_type_spec.rb new file mode 100644 index 00000000000000..45850ad3026a0b --- /dev/null +++ b/ee/spec/graphql/types/dast/profile_schedule_input_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DastProfileScheduleInput'] do + include GraphqlHelpers + + specify { expect(described_class.graphql_name).to eq('DastProfileScheduleInput') } + + it 'has the correct arguments' do + expect(described_class.arguments.keys).to match_array(%w[active startsAt timezone cadence]) + end +end diff --git a/ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb b/ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb index 1ee1c3b97be052..e08330e5bb38b7 100644 --- a/ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb @@ -43,5 +43,32 @@ expect(mutation_response['pipelineUrl']).not_to be_blank end + + context 'when dastProfileSchedule is present' do + let(:mutation) do + graphql_mutation( + mutation_name, + full_path: full_path, + name: name, + branch_name: project.default_branch, + dast_site_profile_id: global_id_of(dast_site_profile), + dast_scanner_profile_id: global_id_of(dast_scanner_profile), + run_after_create: true, + dast_profile_schedule: { + starts_at: Time.zone.now, + active: true, + cadence: { + duration: 1, + unit: "DAY" + }, + timezone: "America/New_York" + } + ) + end + + it 'creates dastProfileSchedule when passed' do + expect { subject }.to change { Dast::ProfileSchedule.count }.by(1) + end + end end end diff --git a/ee/spec/services/app_sec/dast/profiles/create_service_spec.rb b/ee/spec/services/app_sec/dast/profiles/create_service_spec.rb index 6e6c18ff9e1964..304a17cc5d9cd6 100644 --- a/ee/spec/services/app_sec/dast/profiles/create_service_spec.rb +++ b/ee/spec/services/app_sec/dast/profiles/create_service_spec.rb @@ -7,7 +7,7 @@ let_it_be(:developer) { create(:user, developer_projects: [project] ) } let_it_be(:dast_site_profile) { create(:dast_site_profile, project: project) } let_it_be(:dast_scanner_profile) { create(:dast_scanner_profile, project: project) } - + let_it_be(:time_zone) { Time.zone.tzinfo.name } let_it_be(:default_params) do { name: SecureRandom.hex, @@ -81,6 +81,71 @@ end end + context 'when param dast_profile_schedule is present' do + let(:params) do + default_params.merge( + dast_profile_schedule: { + active: true, + starts_at: Time.zone.now, + timezone: time_zone, + cadence: { unit: 'day', duration: 1 } + } + ) + end + + it 'creates the dast_profile_schedule' do + expect { subject }.to change { ::Dast::ProfileSchedule.count }.by(1) + end + + it 'responds with dast_profile_schedule' do + expect(subject.payload[:dast_profile_schedule]).to be_a ::Dast::ProfileSchedule + end + + it 'audits the creation' do + schedule = subject.payload[:dast_profile_schedule] + + audit_event = AuditEvent.find_by(target_id: schedule.id) + + aggregate_failures do + expect(audit_event.author).to eq(developer) + expect(audit_event.entity).to eq(project) + expect(audit_event.target_id).to eq(schedule.id) + expect(audit_event.target_type).to eq('Dast::ProfileSchedule') + expect(audit_event.details).to eq({ + author_name: developer.name, + custom_message: 'Added DAST profile schedule', + target_id: schedule.id, + target_type: 'Dast::ProfileSchedule', + target_details: developer.name + }) + end + end + + context 'when invalid schedule it present' do + let(:bad_time_zone) { 'BadZone' } + + let(:params) do + default_params.merge( + dast_profile_schedule: { + active: true, + starts_at: Time.zone.now, + timezone: bad_time_zone, + cadence: { unit: 'bad_day', duration: 100 } + } + ) + end + + it 'rollback the transaction' do + expect { subject }.to change { ::Dast::ProfileSchedule.count }.by(0) + .and change { ::Dast::Profile.count }.by(0) + end + + it 'returns the error service response' do + expect(subject.error?).to be true + end + end + end + context 'when a param is missing' do let(:params) { default_params.except(:run_after_create) } -- GitLab