Commit 753957f5 authored by Markus Koller's avatar Markus Koller

Merge branch 'trigger-ondemand-dast-scan-using-existing-ci-service-218685' into 'master'

Move DAST scan service to use existing CI service

See merge request gitlab-org/gitlab!34603
parents b8809a40 5a2b20d2
...@@ -31,7 +31,7 @@ module Ci ...@@ -31,7 +31,7 @@ module Ci
merge_request_event: 10, merge_request_event: 10,
external_pull_request_event: 11, external_pull_request_event: 11,
parent_pipeline: 12, parent_pipeline: 12,
ondemand_scan: 13 ondemand_dast_scan: 13
} }
end end
......
...@@ -33,11 +33,14 @@ module Mutations ...@@ -33,11 +33,14 @@ module Mutations
project = authorized_find!(full_path: project_path) project = authorized_find!(full_path: project_path)
raise_resource_not_available_error! unless Feature.enabled?(:security_on_demand_scans_feature_flag, project) raise_resource_not_available_error! unless Feature.enabled?(:security_on_demand_scans_feature_flag, project)
service = Ci::RunDastScanService.new(project: project, user: current_user) service = Ci::RunDastScanService.new(project, current_user)
pipeline = service.execute(branch: branch, target_url: target_url) result = service.execute(branch: branch, target_url: target_url)
success_response(project: project, pipeline: pipeline)
rescue Ci::RunDastScanService::RunError => err if result.success?
error_response(err) success_response(project: project, pipeline: result.payload)
else
error_response(result.message)
end
end end
private private
...@@ -57,8 +60,8 @@ module Mutations ...@@ -57,8 +60,8 @@ module Mutations
} }
end end
def error_response(err) def error_response(message)
{ errors: err.full_messages } { errors: [message] }
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
class RunDastScanService class RunDastScanService < BaseService
DEFAULT_SHA_FOR_PROJECTS_WITHOUT_COMMITS = :placeholder DAST_CI_TEMPLATE = 'lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml'.freeze
class RunError < StandardError def self.ci_template
attr_reader :full_messages @ci_template ||= File.open(DAST_CI_TEMPLATE, 'r') { |f| YAML.safe_load(f.read) }.tap do |template|
template['stages'] = ['dast']
def initialize(msg, full_messages = []) template['dast'].delete('rules')
@full_messages = full_messages.unshift(msg)
super(msg)
end end
end end
def initialize(project:, user:)
@project = project
@user = user
end
def execute(branch:, target_url:) def execute(branch:, target_url:)
unless allowed? return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
msg = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
raise RunError.new(msg) service = Ci::CreatePipelineService.new(project, current_user, ref: branch)
end pipeline = service.execute(:ondemand_dast_scan, content: ci_yaml(target_url))
ActiveRecord::Base.transaction do if pipeline.created_successfully?
pipeline = create_pipeline!(branch) ServiceResponse.success(payload: pipeline)
stage = create_stage!(pipeline) else
build = create_build!(pipeline, stage, branch, target_url) ServiceResponse.error(message: pipeline.full_error_messages)
enqueue!(build)
pipeline
end end
end end
private private
attr_reader :project, :user
def allowed? def allowed?
Ability.allowed?(user, :create_pipeline, project) Ability.allowed?(current_user, :run_ondemand_dast_scan, project)
end
def create_pipeline!(branch)
reraise!(msg: 'Pipeline could not be created') do
Ci::Pipeline.create!(
project: project,
ref: branch,
sha: project.repository.commit&.id || DEFAULT_SHA_FOR_PROJECTS_WITHOUT_COMMITS,
source: :ondemand_scan,
user: user
)
end
end
def create_stage!(pipeline)
reraise!(msg: 'Stage could not be created') do
Ci::Stage.create!(
name: 'dast',
pipeline: pipeline,
project: project
)
end
end
def create_build!(pipeline, stage, branch, target_url)
reraise!(msg: 'Build could not be created') do
Ci::Build.create!(
name: 'DAST Scan',
pipeline: pipeline,
project: project,
ref: branch,
scheduling_type: :stage,
stage: stage.name,
options: options,
yaml_variables: yaml_variables(target_url)
)
end
end
def enqueue!(build)
reraise!(msg: 'Build could not be enqueued') do
build.enqueue!
end
end
def reraise!(msg:)
yield
rescue ActiveRecord::RecordInvalid => err
Gitlab::ErrorTracking.track_exception(err)
raise RunError.new(msg, err.record.errors.full_messages)
rescue => err
Gitlab::ErrorTracking.track_exception(err)
raise RunError.new(msg)
end
def options
{
image: {
name: '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION'
},
artifacts: {
reports: {
dast: [
'gl-dast-report.json'
]
}
},
script: [
'export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}',
'/analyze'
]
}
end end
def yaml_variables(target_url) def ci_yaml(target_url)
[ self.class.ci_template.deep_merge(
{ 'variables' => { 'DAST_WEBSITE' => target_url }
key: 'DAST_VERSION', ).to_yaml
value: '1',
public: true
},
{
key: 'SECURE_ANALYZERS_PREFIX',
value: 'registry.gitlab.com/gitlab-org/security-products/analyzers',
public: true
},
{
key: 'DAST_WEBSITE',
value: target_url,
public: true
},
{
key: 'GIT_STRATEGY',
value: 'none',
public: true
}
]
end end
end end
end end
...@@ -4,11 +4,11 @@ require 'spec_helper' ...@@ -4,11 +4,11 @@ require 'spec_helper'
RSpec.describe Mutations::Pipelines::RunDastScan do RSpec.describe Mutations::Pipelines::RunDastScan do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, group: group) } let(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:target_url) { FFaker::Internet.uri(:https) } let(:target_url) { FFaker::Internet.uri(:https) }
let(:branch) { SecureRandom.hex } let(:branch) { project.default_branch }
let(:scan_type) { Types::DastScanTypeEnum.enum[:passive] } let(:scan_type) { Types::DastScanTypeEnum.enum[:passive] }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
......
...@@ -5,11 +5,11 @@ require 'spec_helper' ...@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe 'Running a DAST Scan' do RSpec.describe 'Running a DAST Scan' do
include GraphqlHelpers include GraphqlHelpers
let(:project) { create(:project) } let(:project) { create(:project, :repository, creator: current_user) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:target_url) { FFaker::Internet.uri(:https) } let(:target_url) { FFaker::Internet.uri(:https) }
let(:branch) { SecureRandom.hex } let(:branch) { project.default_branch }
let(:scan_type) { Types::DastScanTypeEnum.enum[:passive] } let(:scan_type) { Types::DastScanTypeEnum.enum[:passive] }
let(:mutation) do let(:mutation) do
...@@ -58,36 +58,13 @@ RSpec.describe 'Running a DAST Scan' do ...@@ -58,36 +58,13 @@ RSpec.describe 'Running a DAST Scan' do
expect(mutation_response['pipelineUrl']).to eq(expected_url) expect(mutation_response['pipelineUrl']).to eq(expected_url)
end end
context 'when the pipeline could not be created' do context 'when pipeline creation fails' do
before do before do
allow(Ci::Pipeline).to receive(:create!).and_raise(StandardError) allow_any_instance_of(Ci::Pipeline).to receive(:created_successfully?).and_return(false)
allow_any_instance_of(Ci::Pipeline).to receive(:full_error_messages).and_return('error message')
end end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Pipeline could not be created'] it_behaves_like 'a mutation that returns errors in the response', errors: ['error message']
end
context 'when the stage could not be created' do
before do
allow(Ci::Stage).to receive(:create!).and_raise(StandardError)
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Stage could not be created']
end
context 'when the build could not be created' do
before do
allow(Ci::Build).to receive(:create!).and_raise(StandardError)
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Build could not be created']
end
context 'when the build could not be enqueued' do
before do
allow_any_instance_of(Ci::Build).to receive(:enqueue!).and_raise(StandardError)
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Build could not be enqueued']
end end
end end
end end
......
...@@ -3,19 +3,39 @@ ...@@ -3,19 +3,39 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::RunDastScanService do RSpec.describe Ci::RunDastScanService do
let(:project) { create(:project) }
let(:branch) { SecureRandom.hex }
let(:target_url) { FFaker::Internet.uri(:http) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
let(:branch) { project.default_branch }
let(:target_url) { FFaker::Internet.uri(:http) }
describe '.ci_template' do
it 'builds a hash' do
expect(described_class.ci_template).to be_a(Hash)
end
it 'has only one stage' do
expect(described_class.ci_template['stages']).to eq(['dast'])
end
it 'has has no rules' do
expect(described_class.ci_template['dast']['rules']).to be_nil
end
end
describe '#execute' do describe '#execute' do
subject { described_class.new(project: project, user: user).execute(branch: branch, target_url: target_url) } subject { described_class.new(project, user).execute(branch: branch, target_url: target_url) }
let(:status) { subject.status }
let(:pipeline) { subject.payload }
let(:message) { subject.message }
context 'when the user does not have permission to run a dast scan' do context 'when the user does not have permission to run a dast scan' do
it 'raises an exeception with #full_messages populated' do it 'returns an error status' do
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error| expect(status).to eq(:error)
expect(error.full_messages[0]).to include('you don\'t have permission to perform this action') end
end
it 'populates message' do
expect(message).to eq('Insufficient permissions')
end end
end end
...@@ -24,8 +44,12 @@ RSpec.describe Ci::RunDastScanService do ...@@ -24,8 +44,12 @@ RSpec.describe Ci::RunDastScanService do
project.add_developer(user) project.add_developer(user)
end end
it 'returns a success status' do
expect(status).to eq(:success)
end
it 'returns a pipeline' do it 'returns a pipeline' do
expect(subject).to be_a(Ci::Pipeline) expect(pipeline).to be_a(Ci::Pipeline)
end end
it 'creates a pipeline' do it 'creates a pipeline' do
...@@ -33,11 +57,11 @@ RSpec.describe Ci::RunDastScanService do ...@@ -33,11 +57,11 @@ RSpec.describe Ci::RunDastScanService do
end end
it 'sets the pipeline ref to the branch' do it 'sets the pipeline ref to the branch' do
expect(subject.ref).to eq(branch) expect(pipeline.ref).to eq(branch)
end end
it 'sets the source to indicate an ondemand scan' do it 'sets the source to indicate an ondemand scan' do
expect(subject.source).to eq('ondemand_scan') expect(pipeline.source).to eq('ondemand_dast_scan')
end end
it 'creates a stage' do it 'creates a stage' do
...@@ -49,18 +73,19 @@ RSpec.describe Ci::RunDastScanService do ...@@ -49,18 +73,19 @@ RSpec.describe Ci::RunDastScanService do
end end
it 'sets the build name to indicate a DAST scan' do it 'sets the build name to indicate a DAST scan' do
build = subject.builds.first build = pipeline.builds.first
expect(build.name).to eq('DAST Scan') expect(build.name).to eq('dast')
end end
it 'creates a build with appropriate options' do it 'creates a build with appropriate options' do
build = subject.builds.first build = pipeline.builds.first
expected_options = { expected_options = {
'image' => { 'image' => {
'name' => '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION' 'name' => '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION'
}, },
'script' => [ 'script' => [
'export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}', 'export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}',
'if [ -z "$DAST_WEBSITE$DAST_API_SPECIFICATION" ]; then echo "Either DAST_WEBSITE or DAST_API_SPECIFICATION must be set. See https://docs.gitlab.com/ee/user/application_security/dast/#configuration for more details." && exit 1; fi',
'/analyze' '/analyze'
], ],
'artifacts' => { 'artifacts' => {
...@@ -73,7 +98,7 @@ RSpec.describe Ci::RunDastScanService do ...@@ -73,7 +98,7 @@ RSpec.describe Ci::RunDastScanService do
end end
it 'creates a build with appropriate variables' do it 'creates a build with appropriate variables' do
build = subject.builds.first build = pipeline.builds.first
expected_variables = [ expected_variables = [
{ {
'key' => 'DAST_VERSION', 'key' => 'DAST_VERSION',
...@@ -97,90 +122,24 @@ RSpec.describe Ci::RunDastScanService do ...@@ -97,90 +122,24 @@ RSpec.describe Ci::RunDastScanService do
end end
it 'enqueues a build' do it 'enqueues a build' do
build = subject.builds.first build = pipeline.builds.first
expect(build.queued_at).not_to be_nil expect(build.queued_at).not_to be_nil
end end
context 'when the repository has no commits' do context 'when the pipeline fails to save' do
it 'uses a placeholder' do
expect(subject.sha).to eq('placeholder')
end
end
context 'when the pipeline could not be created' do
before do before do
allow(Ci::Pipeline).to receive(:create!).and_raise(StandardError) allow_any_instance_of(Ci::Pipeline).to receive(:created_successfully?).and_return(false)
allow_any_instance_of(Ci::Pipeline).to receive(:full_error_messages).and_return(full_error_messages)
end end
it 'raises an exeception with #full_messages populated' do let(:full_error_messages) { SecureRandom.hex }
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error|
expect(error.full_messages).to include('Pipeline could not be created')
end
end
end
context 'when the stage could not be created' do
before do
allow(Ci::Stage).to receive(:create!).and_raise(StandardError)
end
it 'raises an exeception with #full_messages populated' do it 'returns an error status' do
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error| expect(status).to eq(:error)
expect(error.full_messages).to include('Stage could not be created')
end
end
it 'does not create a pipeline' do
expect { subject rescue nil }.not_to change(Ci::Pipeline, :count)
end
end
context 'when the build could not be created' do
before do
allow(Ci::Build).to receive(:create!).and_raise(StandardError)
end
it 'raises an exeception with #full_messages populated' do
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error|
expect(error.full_messages).to include('Build could not be created')
end
end
it 'does not create a stage' do
expect { subject rescue nil }.not_to change(Ci::Pipeline, :count)
end
end
context 'when the build could not be enqueued' do
before do
allow_any_instance_of(Ci::Build).to receive(:enqueue!).and_raise(StandardError)
end
it 'raises an exeception with #full_messages populated' do
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error|
expect(error.full_messages).to include('Build could not be enqueued')
end
end
it 'does not create a build' do
expect { subject rescue nil }.not_to change(Ci::Pipeline, :count)
end
end
context 'when a validation error is raised' do
before do
klass = Ci::Pipeline
allow(klass).to receive(:create!).and_raise(
ActiveRecord::RecordInvalid, klass.new.tap do |pl|
pl.errors.add(:sha, 'can\'t be blank')
end
)
end end
it 'raises an exeception with #full_messages populated' do it 'populates message' do
expect { subject }.to raise_error(Ci::RunDastScanService::RunError) do |error| expect(message).to eq(full_error_messages)
expect(error.full_messages).to include('Sha can\'t be blank')
end
end end
end 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