Commit 9769bf8d authored by Philip Cunningham's avatar Philip Cunningham Committed by Ash McKenzie

Add project.dastProfiles GraphQL field

Adds new field, policy and specs.
parent 14cf0060
...@@ -5502,6 +5502,81 @@ type DastOnDemandScanCreatePayload { ...@@ -5502,6 +5502,81 @@ type DastOnDemandScanCreatePayload {
pipelineUrl: String pipelineUrl: String
} }
"""
Represents a DAST Profile
"""
type DastProfile {
"""
The associated scanner profile.
"""
dastScannerProfile: DastScannerProfile
"""
The associated site profile.
"""
dastSiteProfile: DastSiteProfile
"""
The description of the scan.
"""
description: String
"""
Relative web path to the edit page of a profile.
"""
editPath: String
"""
ID of the profile.
"""
id: DastProfileID!
"""
The name of the profile.
"""
name: String
}
"""
The connection type for DastProfile.
"""
type DastProfileConnection {
"""
A list of edges.
"""
edges: [DastProfileEdge]
"""
A list of nodes.
"""
nodes: [DastProfile]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type DastProfileEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DastProfile
}
"""
Identifier of Dast::Profile.
"""
scalar DastProfileID
enum DastScanTypeEnum { enum DastScanTypeEnum {
""" """
Active DAST scan. This scan will make active attacks against the target site. Active DAST scan. This scan will make active attacks against the target site.
...@@ -18195,7 +18270,32 @@ type Project { ...@@ -18195,7 +18270,32 @@ type Project {
createdAt: Time createdAt: Time
""" """
The DAST scanner profiles associated with the project DAST Profiles associated with the project. Always returns no nodes if `dast_saved_scans` is disabled.
"""
dastProfiles(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DastProfileConnection
"""
The DAST scanner profiles associated with the project.
""" """
dastScannerProfiles( dastScannerProfiles(
""" """
...@@ -18220,7 +18320,7 @@ type Project { ...@@ -18220,7 +18320,7 @@ type Project {
): DastScannerProfileConnection ): DastScannerProfileConnection
""" """
DAST Site Profile associated with the project DAST Site Profile associated with the project.
""" """
dastSiteProfile( dastSiteProfile(
""" """
...@@ -18230,7 +18330,7 @@ type Project { ...@@ -18230,7 +18330,7 @@ type Project {
): DastSiteProfile ): DastSiteProfile
""" """
DAST Site Profiles associated with the project DAST Site Profiles associated with the project.
""" """
dastSiteProfiles( dastSiteProfiles(
""" """
...@@ -18255,8 +18355,8 @@ type Project { ...@@ -18255,8 +18355,8 @@ type Project {
): DastSiteProfileConnection ): DastSiteProfileConnection
""" """
DAST Site Validations associated with the project. Will always return no nodes DAST Site Validations associated with the project. Always returns no nodes if
if `security_on_demand_scans_site_validation` is disabled `security_on_demand_scans_site_validation` is disabled.
""" """
dastSiteValidations( dastSiteValidations(
""" """
......
...@@ -15047,6 +15047,229 @@ ...@@ -15047,6 +15047,229 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "DastProfile",
"description": "Represents a DAST Profile",
"fields": [
{
"name": "dastScannerProfile",
"description": "The associated scanner profile.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DastScannerProfile",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastSiteProfile",
"description": "The associated site profile.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DastSiteProfile",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "The description of the scan.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "editPath",
"description": "Relative web path to the edit page of a profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the profile.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastProfileID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "The name of the profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastProfileConnection",
"description": "The connection type for DastProfile.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastProfileEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastProfile",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastProfileEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DastProfile",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "DastProfileID",
"description": "Identifier of Dast::Profile.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "ENUM", "kind": "ENUM",
"name": "DastScanTypeEnum", "name": "DastScanTypeEnum",
...@@ -53583,9 +53806,62 @@ ...@@ -53583,9 +53806,62 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "dastProfiles",
"description": "DAST Profiles associated with the project. Always returns no nodes if `dast_saved_scans` is disabled.",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastProfileConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "dastScannerProfiles", "name": "dastScannerProfiles",
"description": "The DAST scanner profiles associated with the project", "description": "The DAST scanner profiles associated with the project.",
"args": [ "args": [
{ {
"name": "after", "name": "after",
...@@ -53638,7 +53914,7 @@ ...@@ -53638,7 +53914,7 @@
}, },
{ {
"name": "dastSiteProfile", "name": "dastSiteProfile",
"description": "DAST Site Profile associated with the project", "description": "DAST Site Profile associated with the project.",
"args": [ "args": [
{ {
"name": "id", "name": "id",
...@@ -53665,7 +53941,7 @@ ...@@ -53665,7 +53941,7 @@
}, },
{ {
"name": "dastSiteProfiles", "name": "dastSiteProfiles",
"description": "DAST Site Profiles associated with the project", "description": "DAST Site Profiles associated with the project.",
"args": [ "args": [
{ {
"name": "after", "name": "after",
...@@ -53718,7 +53994,7 @@ ...@@ -53718,7 +53994,7 @@
}, },
{ {
"name": "dastSiteValidations", "name": "dastSiteValidations",
"description": "DAST Site Validations associated with the project. Will always return no nodes if `security_on_demand_scans_site_validation` is disabled", "description": "DAST Site Validations associated with the project. Always returns no nodes if `security_on_demand_scans_site_validation` is disabled.",
"args": [ "args": [
{ {
"name": "normalizedTargetUrls", "name": "normalizedTargetUrls",
...@@ -878,6 +878,19 @@ Autogenerated return type of DastOnDemandScanCreate. ...@@ -878,6 +878,19 @@ Autogenerated return type of DastOnDemandScanCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | URL of the pipeline that was created. | | `pipelineUrl` | String | URL of the pipeline that was created. |
### DastProfile
Represents a DAST Profile.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `dastScannerProfile` | DastScannerProfile | The associated scanner profile. |
| `dastSiteProfile` | DastSiteProfile | The associated site profile. |
| `description` | String | The description of the scan. |
| `editPath` | String | Relative web path to the edit page of a profile. |
| `id` | DastProfileID! | ID of the profile. |
| `name` | String | The name of the profile. |
### DastScannerProfile ### DastScannerProfile
Represents a DAST scanner profile. Represents a DAST scanner profile.
...@@ -2745,10 +2758,11 @@ Autogenerated return type of PipelineRetry. ...@@ -2745,10 +2758,11 @@ Autogenerated return type of PipelineRetry.
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the project | | `containerRepositories` | ContainerRepositoryConnection | Container repositories of the project |
| `containerRepositoriesCount` | Int! | Number of container repositories in the project | | `containerRepositoriesCount` | Int! | Number of container repositories in the project |
| `createdAt` | Time | Timestamp of the project creation | | `createdAt` | Time | Timestamp of the project creation |
| `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project | | `dastProfiles` | DastProfileConnection | DAST Profiles associated with the project. Always returns no nodes if `dast_saved_scans` is disabled. |
| `dastSiteProfile` | DastSiteProfile | DAST Site Profile associated with the project | | `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project. |
| `dastSiteProfiles` | DastSiteProfileConnection | DAST Site Profiles associated with the project | | `dastSiteProfile` | DastSiteProfile | DAST Site Profile associated with the project. |
| `dastSiteValidations` | DastSiteValidationConnection | DAST Site Validations associated with the project. Will always return no nodes if `security_on_demand_scans_site_validation` is disabled | | `dastSiteProfiles` | DastSiteProfileConnection | DAST Site Profiles associated with the project. |
| `dastSiteValidations` | DastSiteValidationConnection | DAST Site Validations associated with the project. Always returns no nodes if `security_on_demand_scans_site_validation` is disabled. |
| `description` | String | Short description of the project | | `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `environment` | Environment | A single environment of the project | | `environment` | Environment | A single environment of the project |
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Dast module Dast
class ProfilesFinder class ProfilesFinder
DEFAULT_SORT = { id: :asc }.freeze DEFAULT_SORT = { id: :desc }.freeze
def initialize(params = {}) def initialize(params = {})
@params = params @params = params
......
...@@ -10,11 +10,6 @@ module EE ...@@ -10,11 +10,6 @@ module EE
description: 'Information about security analyzers used in the project', description: 'Information about security analyzers used in the project',
method: :itself method: :itself
field :dast_scanner_profiles,
::Types::DastScannerProfileType.connection_type,
null: true,
description: 'The DAST scanner profiles associated with the project'
field :vulnerabilities, field :vulnerabilities,
::Types::VulnerabilityType.connection_type, ::Types::VulnerabilityType.connection_type,
null: true, null: true,
...@@ -61,24 +56,35 @@ module EE ...@@ -61,24 +56,35 @@ module EE
description: 'Find iterations', description: 'Find iterations',
resolver: ::Resolvers::IterationsResolver resolver: ::Resolvers::IterationsResolver
field :dast_profiles,
::Types::Dast::ProfileType.connection_type,
null: true,
description: 'DAST Profiles associated with the project. Always returns no nodes ' \
'if `dast_saved_scans` is disabled.'
field :dast_site_profile, field :dast_site_profile,
::Types::DastSiteProfileType, ::Types::DastSiteProfileType,
null: true, null: true,
resolver: ::Resolvers::DastSiteProfileResolver.single, resolver: ::Resolvers::DastSiteProfileResolver.single,
description: 'DAST Site Profile associated with the project' description: 'DAST Site Profile associated with the project.'
field :dast_site_profiles, field :dast_site_profiles,
::Types::DastSiteProfileType.connection_type, ::Types::DastSiteProfileType.connection_type,
null: true, null: true,
description: 'DAST Site Profiles associated with the project', description: 'DAST Site Profiles associated with the project.',
resolver: ::Resolvers::DastSiteProfileResolver resolver: ::Resolvers::DastSiteProfileResolver
field :dast_scanner_profiles,
::Types::DastScannerProfileType.connection_type,
null: true,
description: 'The DAST scanner profiles associated with the project.'
field :dast_site_validations, field :dast_site_validations,
::Types::DastSiteValidationType.connection_type, ::Types::DastSiteValidationType.connection_type,
null: true, null: true,
resolver: ::Resolvers::DastSiteValidationResolver, resolver: ::Resolvers::DastSiteValidationResolver,
description: 'DAST Site Validations associated with the project. Will always return no nodes ' \ description: 'DAST Site Validations associated with the project. Always returns no nodes ' \
'if `security_on_demand_scans_site_validation` is disabled' 'if `security_on_demand_scans_site_validation` is disabled.'
field :cluster_agent, field :cluster_agent,
::Types::Clusters::AgentType, ::Types::Clusters::AgentType,
...@@ -117,6 +123,12 @@ module EE ...@@ -117,6 +123,12 @@ module EE
resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver
end end
def dast_profiles
return Dast::Profile.none unless ::Feature.enabled?(:dast_saved_scans, object, default_enabled: :yaml)
Dast::ProfilesFinder.new(project_id: object.id).execute
end
def dast_scanner_profiles def dast_scanner_profiles
DastScannerProfilesFinder.new(project_ids: [object.id]).execute DastScannerProfilesFinder.new(project_ids: [object.id]).execute
end end
......
# frozen_string_literal: true
module Types
module Dast
class ProfileType < BaseObject
graphql_name 'DastProfile'
description 'Represents a DAST Profile'
authorize :create_on_demand_dast_scan
field :id, ::Types::GlobalIDType[::Dast::Profile], null: false,
description: 'ID of the profile.'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'The name of the profile.'
field :description, GraphQL::STRING_TYPE, null: true,
description: 'The description of the scan.'
field :dast_site_profile, DastSiteProfileType, null: true,
description: 'The associated site profile.'
field :dast_scanner_profile, DastScannerProfileType, null: true,
description: 'The associated scanner profile.'
field :edit_path, GraphQL::STRING_TYPE, null: true,
description: 'Relative web path to the edit page of a profile.'
def edit_path
Gitlab::Routing.url_helpers.edit_project_on_demand_scan_path(object.project, object)
end
end
end
end
# frozen_string_literal: true
module Dast
class ProfilePolicy < BasePolicy
delegate { @subject.project }
end
end
...@@ -20,7 +20,7 @@ RSpec.describe Dast::ProfilesFinder do ...@@ -20,7 +20,7 @@ RSpec.describe Dast::ProfilesFinder do
aggregate_failures do aggregate_failures do
expect(Dast::Profile).to receive(:limit).with(100).and_call_original expect(Dast::Profile).to receive(:limit).with(100).and_call_original
expect(subject).to contain_exactly(dast_profile1, dast_profile2, dast_profile3) expect(subject).to contain_exactly(dast_profile3, dast_profile2, dast_profile1)
end end
end end
...@@ -36,7 +36,7 @@ RSpec.describe Dast::ProfilesFinder do ...@@ -36,7 +36,7 @@ RSpec.describe Dast::ProfilesFinder do
let(:params) { { project_id: project1.id } } let(:params) { { project_id: project1.id } }
it 'returns the matching dast_profiles' do it 'returns the matching dast_profiles' do
expect(subject).to contain_exactly(dast_profile1, dast_profile3) expect(subject).to contain_exactly(dast_profile3, dast_profile1)
end end
end end
...@@ -49,8 +49,8 @@ RSpec.describe Dast::ProfilesFinder do ...@@ -49,8 +49,8 @@ RSpec.describe Dast::ProfilesFinder do
end end
context 'sorting' do context 'sorting' do
it 'orders by id asc by default' do it 'orders by id desc by default' do
expect(subject).to be_sorted(:id, :asc) expect(subject).to be_sorted(:id, :desc)
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['DastProfile'] do
include GraphqlHelpers
let_it_be(:object) { create(:dast_profile) }
let_it_be(:fields) { %i[id name description dastSiteProfile dastScannerProfile editPath] }
specify { expect(described_class.graphql_name).to eq('DastProfile') }
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
it { expect(described_class).to have_graphql_fields(fields) }
describe 'editPath field' do
it 'correctly renders the field' do
expected_result = Gitlab::Routing.url_helpers.edit_project_on_demand_scan_path(object.project, object)
expect(resolve_field(:edit_path, object)).to eq(expected_result)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dast::ProfilePolicy do
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_profile, project: project) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).dastProfiles' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:dast_profile1) { create(:dast_profile, project: project) }
let_it_be(:dast_profile2) { create(:dast_profile, project: project) }
let_it_be(:dast_profile3) { create(:dast_profile, project: project) }
let_it_be(:dast_profile4) { create(:dast_profile, project: project) }
subject do
fields = all_graphql_fields_for('DastProfile')
query = graphql_query_for(
:project,
{ full_path: project.full_path },
query_nodes(:dast_profiles, fields)
)
post_graphql(
query,
current_user: current_user,
variables: {
fullPath: project.full_path
}
)
end
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it 'returns a null project' do
subject
expect(graphql_data_at(:project)).to be_nil
end
end
context 'when a user does not have access to dast_profiles' do
it 'returns an empty nodes array' do
project.add_guest(current_user)
subject
expect(graphql_data_at(:project, :dast_profiles, :nodes)).to be_empty
end
end
context 'when a user has access to dast_profiles' do
before do
project.add_developer(current_user)
end
let(:data_path) { [:project, :dast_profiles] }
def pagination_results_data(dast_profiles)
dast_profiles.map { |dast_profile| dast_profile['id'] }
end
it_behaves_like 'sorted paginated query' do
let(:sort_param) { nil }
let(:first_param) { 3 }
let(:expected_results) do
[dast_profile4, dast_profile3, dast_profile2, dast_profile1].map { |validation| global_id_of(validation)}
end
end
context 'when the feature is disabled' do
it 'returns no nodes' do
stub_feature_flags(dast_saved_scans: false)
subject
expect(graphql_data_at(:project, :dast_profiles, :nodes)).to be_empty
end
end
end
def pagination_query(arguments)
graphql_query_for(
:project,
{ full_path: project.full_path },
query_nodes(:dast_profiles, 'id', include_pagination_info: true, args: arguments)
)
end
end
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