diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d6fe56bea735d4a2bc14395768ce6500d3efaf6b..da2133e1f37b4c5721321b67efbdc7c942144419 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8565,6 +8565,7 @@ Represents a DAST Profile.
| Name | Type | Description |
| ---- | ---- | ----------- |
| `branch` | [`DastProfileBranch`](#dastprofilebranch) | The associated branch. |
+| `dastProfileSchedule` | [`DastProfileSchedule`](#dastprofileschedule) | Associated profile schedule. Will always return `null` if `dast_on_demand_scans_scheduler` feature flag is disabled. |
| `dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | The associated scanner profile. |
| `dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | The associated site profile. |
| `description` | [`String`](#string) | The description of the scan. |
@@ -8583,6 +8584,32 @@ Represents a DAST Profile Branch.
| `exists` | [`Boolean`](#boolean) | Indicates whether or not the branch exists. |
| `name` | [`String`](#string) | The name of the branch. |
+### `DastProfileCadence`
+
+Represents DAST Profile Cadence.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `duration` | [`Int`](#int) | Duration of the DAST profile cadence. |
+| `unit` | [`DastProfileCadenceUnit`](#dastprofilecadenceunit) | Unit for the duration of DAST profile cadence. |
+
+### `DastProfileSchedule`
+
+Represents a DAST profile schedule.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `active` | [`Boolean`](#boolean) | Status of the DAST profile schedule. |
+| `cadence` | [`DastProfileCadence`](#dastprofilecadence) | Cadence of the DAST profile schedule. |
+| `id` | [`DastProfileScheduleID!`](#dastprofilescheduleid) | ID of the DAST profile schedule. |
+| `nextRunAt` | [`Time`](#time) | Next run time of the DAST profile schedule in the given timezone. |
+| `startsAt` | [`Time`](#time) | Start time of the DAST profile schedule in the given timezone. |
+| `timezone` | [`String`](#string) | Time zone of the start time of the DAST profile schedule. |
+
### `DastScannerProfile`
Represents a DAST scanner profile.
@@ -16443,6 +16470,12 @@ A `DastProfileID` is a global ID. It is encoded as a string.
An example `DastProfileID` is: `"gid://gitlab/Dast::Profile/1"`.
+### `DastProfileScheduleID`
+
+A `DastProfileScheduleID` is a global ID. It is encoded as a string.
+
+An example `DastProfileScheduleID` is: `"gid://gitlab/Dast::ProfileSchedule/1"`.
+
### `DastScannerProfileID`
A `DastScannerProfileID` is a global ID. It is encoded as a string.
diff --git a/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb b/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb
index 01b78b298e25cba071ea35fb254212fb0b552160..654a156fa636d19645a83fc9739fdde9cadb882e 100644
--- a/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb
+++ b/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb
@@ -25,7 +25,8 @@ def resolve_with_lookahead(**args)
def preloads
{
dast_site_profile: [{ dast_site_profile: [:dast_site, :secret_variables] }],
- dast_scanner_profile: [:dast_scanner_profile]
+ dast_scanner_profile: [:dast_scanner_profile],
+ dast_profile_schedule: [:dast_profile_schedule]
}
end
diff --git a/ee/app/graphql/types/dast/profile_cadence_type.rb b/ee/app/graphql/types/dast/profile_cadence_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0097ff0faa38f7f536d92caa415758fa4cb0e733
--- /dev/null
+++ b/ee/app/graphql/types/dast/profile_cadence_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Disabling this cop as the auth check is happening in ProfileScheduleType.
+# ProfileCadenceType is a dependent entity on ProfileScheduleType and does not exist without it.
+# rubocop:disable Graphql/AuthorizeTypes
+
+module Types
+ module Dast
+ class ProfileCadenceType < BaseObject
+ graphql_name 'DastProfileCadence'
+ description 'Represents DAST Profile Cadence.'
+
+ field :unit, ::Types::Dast::ProfileCadenceUnitEnum,
+ null: true,
+ description: 'Unit for the duration of DAST profile cadence.'
+
+ field :duration, GraphQL::Types::Int,
+ null: true,
+ description: 'Duration of the DAST profile cadence.'
+ end
+ end
+end
diff --git a/ee/app/graphql/types/dast/profile_schedule_type.rb b/ee/app/graphql/types/dast/profile_schedule_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6b30f634a8016fff79e7e4d0f060af8135000b17
--- /dev/null
+++ b/ee/app/graphql/types/dast/profile_schedule_type.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Types
+ module Dast
+ class ProfileScheduleType < BaseObject
+ graphql_name 'DastProfileSchedule'
+ description 'Represents a DAST profile schedule.'
+
+ authorize :read_on_demand_scans
+
+ field :id, ::Types::GlobalIDType[::Dast::ProfileSchedule], null: false,
+ description: 'ID of the DAST profile schedule.'
+
+ field :active, GraphQL::Types::Boolean, null: true,
+ description: 'Status of the DAST profile schedule.'
+
+ field :starts_at, Types::TimeType, null: true,
+ description: 'Start time of the DAST profile schedule in the given timezone.'
+
+ field :timezone, GraphQL::Types::String, null: true,
+ description: 'Time zone of the start time of the DAST profile schedule.'
+
+ field :cadence, Types::Dast::ProfileCadenceType, null: true,
+ description: 'Cadence of the DAST profile schedule.'
+
+ field :next_run_at, Types::TimeType, null: true,
+ description: 'Next run time of the DAST profile schedule in the given timezone.'
+
+ def starts_at
+ return unless object.starts_at && object.timezone
+
+ object.starts_at.in_time_zone(object.timezone)
+ end
+
+ def next_run_at
+ return unless object.next_run_at && object.timezone
+
+ object.next_run_at.in_time_zone(object.timezone)
+ end
+ end
+ end
+end
diff --git a/ee/app/graphql/types/dast/profile_type.rb b/ee/app/graphql/types/dast/profile_type.rb
index e57e7af15ef7640cbbf6dcb069aaa388873ec45f..9a8437cfa9faaa13a9a436f2dcbadf90a7122e91 100644
--- a/ee/app/graphql/types/dast/profile_type.rb
+++ b/ee/app/graphql/types/dast/profile_type.rb
@@ -23,6 +23,10 @@ class ProfileType < BaseObject
field :dast_scanner_profile, DastScannerProfileType, null: true,
description: 'The associated scanner profile.'
+ field :dast_profile_schedule, ::Types::Dast::ProfileScheduleType, null: true,
+ description: 'Associated profile schedule. Will always return `null` ' \
+ 'if `dast_on_demand_scans_scheduler` feature flag is disabled.'
+
field :branch, Dast::ProfileBranchType, null: true,
description: 'The associated branch.',
calls_gitaly: true
@@ -33,6 +37,12 @@ class ProfileType < BaseObject
def edit_path
Gitlab::Routing.url_helpers.edit_project_on_demand_scan_path(object.project, object)
end
+
+ def dast_profile_schedule
+ return unless Feature.enabled?(:dast_on_demand_scans_scheduler, object.project, default_enabled: :yaml)
+
+ object.dast_profile_schedule
+ end
end
end
end
diff --git a/ee/app/policies/dast/profile_schedule_policy.rb b/ee/app/policies/dast/profile_schedule_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3b7064515509dc3122ea4d866d557d37b4a18fa4
--- /dev/null
+++ b/ee/app/policies/dast/profile_schedule_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Dast
+ class ProfileSchedulePolicy < BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb b/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85cca34f3481ca806daa554e4e23a4dc7e887bd6
--- /dev/null
+++ b/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DastProfileCadence'] do
+ include GraphqlHelpers
+
+ let_it_be(:fields) { %i[unit duration] }
+
+ specify { expect(described_class.graphql_name).to eq('DastProfileCadence') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb b/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ef9fefaeedc3337721e9f9329e2452f90ac8161
--- /dev/null
+++ b/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DastProfileSchedule'] do
+ include GraphqlHelpers
+
+ let_it_be(:fields) { %i[id active startsAt timezone nextRunAt cadence] }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let_it_be(:object) { create(:dast_profile_schedule, project: project, owner: user) }
+
+ specify { expect(described_class.graphql_name).to eq('DastProfileSchedule') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+
+ before do
+ stub_licensed_features(security_on_demand_scans: true)
+ end
+
+ describe 'startsAt field' do
+ it 'converts the startsAt to the timezone' do
+ expect(resolve_field(:starts_at, object, current_user: user)).to eq(object.starts_at.in_time_zone(object.timezone))
+ end
+ end
+
+ describe 'nextRunAt field' do
+ it 'converts the nextRunAt to the timezone' do
+ expect(resolve_field(:next_run_at, object, current_user: user)).to eq(object.next_run_at.in_time_zone(object.timezone))
+ end
+ end
+end
diff --git a/ee/spec/graphql/types/dast/profile_type_spec.rb b/ee/spec/graphql/types/dast/profile_type_spec.rb
index 3f7c85e45f99af39f49160fb9123c009846207e6..502d037bbca1d78f0c090805bb4a97e847c6a867 100644
--- a/ee/spec/graphql/types/dast/profile_type_spec.rb
+++ b/ee/spec/graphql/types/dast/profile_type_spec.rb
@@ -8,7 +8,7 @@
let_it_be(:project) { create(:project, :repository) }
let_it_be(:object) { create(:dast_profile, project: project) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
- let_it_be(:fields) { %i[id name description dastSiteProfile dastScannerProfile branch editPath] }
+ let_it_be(:fields) { %i[id name description dastSiteProfile dastScannerProfile dastProfileSchedule branch editPath] }
specify { expect(described_class.graphql_name).to eq('DastProfile') }
specify { expect(described_class).to require_graphql_authorizations(:read_on_demand_scans) }
@@ -35,4 +35,22 @@
expect(resolve_field(:edit_path, object, current_user: user)).to eq(expected_result)
end
end
+
+ describe 'dastProfileSchedule field' do
+ context 'when the feature flag is enabled' do
+ it 'correctly resolves the field' do
+ expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to eq(object.dast_profile_schedule)
+ end
+ end
+
+ context 'when the feature flag is not enabled' do
+ before do
+ stub_feature_flags(dast_on_demand_scans_scheduler: false)
+ end
+
+ it 'is nil' do
+ expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to be_nil
+ end
+ end
+ end
end
diff --git a/ee/spec/policies/dast/profile_schedule_policy_spec.rb b/ee/spec/policies/dast/profile_schedule_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7190ee60b980458491d5461dfd128676221dd3a1
--- /dev/null
+++ b/ee/spec/policies/dast/profile_schedule_policy_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Dast::ProfileSchedulePolicy do
+ it_behaves_like 'a dast on-demand scan policy' do
+ let_it_be(:record) { create(:dast_profile_schedule, project: project) }
+ end
+end