Commit 6fc2a3c1 authored by Philip Cunningham's avatar Philip Cunningham Committed by Peter Leitzen

Add DastProfileRun GraphQL mutation

Allow users to run a scan using a Dast::Profile.
parent f34aebd7
......@@ -5902,6 +5902,46 @@ Identifier of Dast::Profile.
"""
scalar DastProfileID
"""
Autogenerated input type of DastProfileRun
"""
input DastProfileRunInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Full path for the project the scanner profile belongs to.
"""
fullPath: ID!
"""
ID of the profile to be used for the scan.
"""
id: DastProfileID!
}
"""
Autogenerated return type of DastProfileRun
"""
type DastProfileRunPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
URL of the pipeline that was created.
"""
pipelineUrl: String
}
enum DastScanTypeEnum {
"""
Active DAST scan. This scan will make active attacks against the target site.
......@@ -16399,6 +16439,7 @@ type Mutation {
dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload
dastProfileCreate(input: DastProfileCreateInput!): DastProfileCreatePayload
dastProfileDelete(input: DastProfileDeleteInput!): DastProfileDeletePayload
dastProfileRun(input: DastProfileRunInput!): DastProfileRunPayload
dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload
dastScannerProfileDelete(input: DastScannerProfileDeleteInput!): DastScannerProfileDeletePayload
dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload
......
......@@ -16059,6 +16059,122 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DastProfileRunInput",
"description": "Autogenerated input type of DastProfileRun",
"fields": null,
"inputFields": [
{
"name": "fullPath",
"description": "Full path for the project the scanner profile belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "id",
"description": "ID of the profile to be used for the scan.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastProfileID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastProfileRunPayload",
"description": "Autogenerated return type of DastProfileRun",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipelineUrl",
"description": "URL of the pipeline that was created.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "DastScanTypeEnum",
......@@ -45855,6 +45971,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastProfileRun",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DastProfileRunInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastProfileRunPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastScannerProfileCreate",
"description": null,
......@@ -952,6 +952,16 @@ Autogenerated return type of DastProfileDelete.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DastProfileRunPayload
Autogenerated return type of DastProfileRun.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | URL of the pipeline that was created. |
### DastScannerProfile
Represents a DAST scanner profile.
......
......@@ -44,6 +44,7 @@ module EE
mount_mutation ::Mutations::DastOnDemandScans::Create
mount_mutation ::Mutations::Dast::Profiles::Create
mount_mutation ::Mutations::Dast::Profiles::Delete
mount_mutation ::Mutations::Dast::Profiles::Run
mount_mutation ::Mutations::DastSiteProfiles::Create
mount_mutation ::Mutations::DastSiteProfiles::Update
mount_mutation ::Mutations::DastSiteProfiles::Delete
......
# frozen_string_literal: true
module Mutations
module Dast
module Profiles
class Run < BaseMutation
include FindsProject
graphql_name 'DastProfileRun'
ProfileID = ::Types::GlobalIDType[::Dast::Profile]
field :pipeline_url, GraphQL::STRING_TYPE,
null: true,
description: 'URL of the pipeline that was created.'
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path for the project the scanner profile belongs to.'
argument :id, ProfileID,
required: true,
description: 'ID of the profile to be used for the scan.'
authorize :create_on_demand_dast_scan
def resolve(full_path:, id:)
project = authorized_find!(full_path)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project)
# TODO: remove this line once the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ProfileID.coerce_isolated_input(id).model_id
dast_profile = find_dast_profile(project, id)
return { errors: ['Profile not found for given parameters'] } unless dast_profile
response = create_on_demand_dast_scan(project, dast_profile)
return { errors: response.errors } if response.error?
{ errors: [], pipeline_url: response.payload.fetch(:pipeline_url) }
end
private
def allowed?(project)
project.feature_available?(:security_on_demand_scans) &&
Feature.enabled?(:dast_saved_scans, project, default_enabled: :yaml)
end
def find_dast_profile(project, id)
::Dast::ProfilesFinder.new(project_id: project.id, id: id)
.execute
.first
end
def create_on_demand_dast_scan(project, dast_profile)
::DastOnDemandScans::CreateService.new(
container: project,
current_user: current_user,
params: {
dast_site_profile: dast_profile.dast_site_profile,
dast_scanner_profile: dast_profile.dast_scanner_profile
}
).execute
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Dast::Profiles::Run do
let_it_be_with_refind(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let(:full_path) { project.full_path }
let(:dast_profile_id) { dast_profile.to_global_id }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
full_path: full_path,
id: dast_profile_id
)
end
context 'when the feature flag dast_saved_scans is disabled' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: true)
stub_feature_flags(dast_saved_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
stub_feature_flags(dast_saved_scans: true)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the feature is enabled' do
before do
stub_licensed_features(security_on_demand_scans: true)
stub_feature_flags(dast_saved_scans: true)
end
context 'when the project does not exist' do
let(:full_path) { SecureRandom.hex }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
end
it 'returns a pipeline_url containing the correct path' do
actual_url = subject[:pipeline_url]
pipeline = Ci::Pipeline.last
expected_url = Gitlab::Routing.url_helpers.project_pipeline_url(
project,
pipeline
)
expect(actual_url).to eq(expected_url)
end
context 'when the dast_profile does not exist' do
let(:dast_profile_id) { Gitlab::GlobalId.build(nil, model_name: 'Dast::Profile', id: 'does_not_exist') }
it 'communicates failure' do
expect(subject[:errors]).to include('Profile not found for given parameters')
end
end
context 'when scan_type=active' do
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, scan_type: 'active') }
let(:dast_profile) { create(:dast_profile, project: project, dast_scanner_profile: dast_scanner_profile) }
context 'when target is not validated' do
it 'communicates failure' do
expect(subject[:errors]).to include('Cannot run active scan against unvalidated target')
end
end
context 'when target is validated' do
it 'has no errors' do
create(:dast_site_validation, state: :passed, dast_site_token: create(:dast_site_token, project: project, url: dast_profile.dast_site_profile.dast_site.url))
expect(subject[:errors]).to be_empty
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Running a DAST Profile' do
include GraphqlHelpers
let!(:dast_profile) { create(:dast_profile, project: project) }
let(:mutation_name) { :dast_profile_run }
let(:mutation) do
graphql_mutation(
mutation_name,
full_path: project.full_path,
id: global_id_of(dast_profile)
)
end
it_behaves_like 'an on-demand scan mutation when user cannot run an on-demand scan'
it_behaves_like 'an on-demand scan mutation when user can run an on-demand scan' do
it 'returns a pipeline_url containing the correct path' do
post_graphql_mutation(mutation, current_user: current_user)
pipeline = Ci::Pipeline.last
expected_url = Gitlab::Routing.url_helpers.project_pipeline_url(
project,
pipeline
)
expect(mutation_response['pipelineUrl']).to eq(expected_url)
end
context 'when pipeline creation fails' do
before do
allow_next_instance_of(Ci::Pipeline) do |instance|
allow(instance).to receive(:created_successfully?).and_return(false)
allow(instance).to receive(:full_error_messages).and_return('error message')
end
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['error message']
end
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