Commit 6317ab84 authored by Philip Cunningham's avatar Philip Cunningham Committed by Mikołaj Wawrzyniak

Define CI Pipeline and DAST Profile relationship

- Add habtm relationship between Dast::Profile and Ci::Pipeline
- Amend services to rely on Dast::Profile and create association
- Add EE Contextable override to retrieve Dast::Profile variables
- Extract shared example
parent ba818874
---
title: Create database table dast_profiles_pipelines
merge_request: 56821
author:
type: added
# frozen_string_literal: true
class CreateDastProfilesPipelines < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
table_comment = { owner: 'group::dynamic analysis', description: 'Join table between DAST Profiles and CI Pipelines' }
create_table :dast_profiles_pipelines, primary_key: [:dast_profile_id, :ci_pipeline_id], comment: table_comment.to_json do |t|
t.bigint :dast_profile_id, null: false
t.bigint :ci_pipeline_id, null: false
t.index :ci_pipeline_id, unique: true, name: :index_dast_profiles_pipelines_on_ci_pipeline_id
end
end
def down
drop_table :dast_profiles_pipelines
end
end
# frozen_string_literal: true
class AddDastProfileIdFkToDastProfilesPipelines < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :dast_profiles_pipelines, :dast_profiles, column: :dast_profile_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :dast_profiles_pipelines, column: :dast_profile_id
end
end
end
# frozen_string_literal: true
class AddCiPipelineIdFkToDastProfilesPipelines < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :dast_profiles_pipelines, :ci_pipelines, column: :ci_pipeline_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :dast_profiles_pipelines, column: :ci_pipeline_id
end
end
end
6fb6381e969d062f19b5269b4958306c3bf9a1b7cf06e5b0eb25beb005952d07
\ No newline at end of file
d520fe71ca271c135b9684dc7a03ede27832659459f7476787798d11460c4736
\ No newline at end of file
fdf858a31e27fb2ce4071642b6e2d76082db95f6ebbec63ce627f92ddf7edfcf
\ No newline at end of file
...@@ -11850,6 +11850,13 @@ CREATE SEQUENCE dast_profiles_id_seq ...@@ -11850,6 +11850,13 @@ CREATE SEQUENCE dast_profiles_id_seq
ALTER SEQUENCE dast_profiles_id_seq OWNED BY dast_profiles.id; ALTER SEQUENCE dast_profiles_id_seq OWNED BY dast_profiles.id;
CREATE TABLE dast_profiles_pipelines (
dast_profile_id bigint NOT NULL,
ci_pipeline_id bigint NOT NULL
);
COMMENT ON TABLE dast_profiles_pipelines IS '{"owner":"group::dynamic analysis","description":"Join table between DAST Profiles and CI Pipelines"}';
CREATE TABLE dast_scanner_profiles ( CREATE TABLE dast_scanner_profiles (
id bigint NOT NULL, id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
...@@ -20535,6 +20542,9 @@ ALTER TABLE ONLY csv_issue_imports ...@@ -20535,6 +20542,9 @@ ALTER TABLE ONLY csv_issue_imports
ALTER TABLE ONLY custom_emoji ALTER TABLE ONLY custom_emoji
ADD CONSTRAINT custom_emoji_pkey PRIMARY KEY (id); ADD CONSTRAINT custom_emoji_pkey PRIMARY KEY (id);
ALTER TABLE ONLY dast_profiles_pipelines
ADD CONSTRAINT dast_profiles_pipelines_pkey PRIMARY KEY (dast_profile_id, ci_pipeline_id);
ALTER TABLE ONLY dast_profiles ALTER TABLE ONLY dast_profiles
ADD CONSTRAINT dast_profiles_pkey PRIMARY KEY (id); ADD CONSTRAINT dast_profiles_pkey PRIMARY KEY (id);
...@@ -22470,6 +22480,8 @@ CREATE INDEX index_dast_profiles_on_dast_site_profile_id ON dast_profiles USING ...@@ -22470,6 +22480,8 @@ CREATE INDEX index_dast_profiles_on_dast_site_profile_id ON dast_profiles USING
CREATE UNIQUE INDEX index_dast_profiles_on_project_id_and_name ON dast_profiles USING btree (project_id, name); CREATE UNIQUE INDEX index_dast_profiles_on_project_id_and_name ON dast_profiles USING btree (project_id, name);
CREATE UNIQUE INDEX index_dast_profiles_pipelines_on_ci_pipeline_id ON dast_profiles_pipelines USING btree (ci_pipeline_id);
CREATE UNIQUE INDEX index_dast_scanner_profiles_on_project_id_and_name ON dast_scanner_profiles USING btree (project_id, name); CREATE UNIQUE INDEX index_dast_scanner_profiles_on_project_id_and_name ON dast_scanner_profiles USING btree (project_id, name);
CREATE INDEX index_dast_site_profiles_on_dast_site_id ON dast_site_profiles USING btree (dast_site_id); CREATE INDEX index_dast_site_profiles_on_dast_site_id ON dast_site_profiles USING btree (dast_site_id);
...@@ -25141,6 +25153,9 @@ ALTER TABLE ONLY bulk_import_entities ...@@ -25141,6 +25153,9 @@ ALTER TABLE ONLY bulk_import_entities
ALTER TABLE ONLY users ALTER TABLE ONLY users
ADD CONSTRAINT fk_a4b8fefe3e FOREIGN KEY (managing_group_id) REFERENCES namespaces(id) ON DELETE SET NULL; ADD CONSTRAINT fk_a4b8fefe3e FOREIGN KEY (managing_group_id) REFERENCES namespaces(id) ON DELETE SET NULL;
ALTER TABLE ONLY dast_profiles_pipelines
ADD CONSTRAINT fk_a60cad829d FOREIGN KEY (ci_pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_requests ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_a6963e8447 FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_a6963e8447 FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE;
...@@ -25246,6 +25261,9 @@ ALTER TABLE ONLY external_approval_rules_protected_branches ...@@ -25246,6 +25261,9 @@ ALTER TABLE ONLY external_approval_rules_protected_branches
ALTER TABLE ONLY external_approval_rules_protected_branches ALTER TABLE ONLY external_approval_rules_protected_branches
ADD CONSTRAINT fk_ca2ffb55e6 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_ca2ffb55e6 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE;
ALTER TABLE ONLY dast_profiles_pipelines
ADD CONSTRAINT fk_cc206a8c13 FOREIGN KEY (dast_profile_id) REFERENCES dast_profiles(id) ON DELETE CASCADE;
ALTER TABLE ONLY experiment_subjects ALTER TABLE ONLY experiment_subjects
ADD CONSTRAINT fk_ccc28f8ceb FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_ccc28f8ceb FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
...@@ -58,11 +58,7 @@ module Mutations ...@@ -58,11 +58,7 @@ module Mutations
::DastOnDemandScans::CreateService.new( ::DastOnDemandScans::CreateService.new(
container: project, container: project,
current_user: current_user, current_user: current_user,
params: { params: { dast_profile: dast_profile }
branch: dast_profile.branch_name,
dast_site_profile: dast_profile.dast_site_profile,
dast_scanner_profile: dast_profile.dast_scanner_profile
}
).execute ).execute
end end
end end
......
...@@ -10,6 +10,9 @@ module Dast ...@@ -10,6 +10,9 @@ module Dast
has_many :secret_variables, through: :dast_site_profile, class_name: 'Dast::SiteProfileSecretVariable' has_many :secret_variables, through: :dast_site_profile, class_name: 'Dast::SiteProfileSecretVariable'
has_many :dast_profiles_pipelines, class_name: 'Dast::ProfilesPipeline', foreign_key: :dast_profile_id, inverse_of: :dast_profile
has_many :ci_pipelines, class_name: 'Ci::Pipeline', through: :dast_profiles_pipelines
validates :description, length: { maximum: 255 } validates :description, length: { maximum: 255 }
validates :name, length: { maximum: 255 }, uniqueness: { scope: :project_id }, presence: true validates :name, length: { maximum: 255 }, uniqueness: { scope: :project_id }, presence: true
validates :branch_name, length: { maximum: 255 } validates :branch_name, length: { maximum: 255 }
...@@ -28,6 +31,10 @@ module Dast ...@@ -28,6 +31,10 @@ module Dast
Dast::Branch.new(self) Dast::Branch.new(self)
end end
def ci_variables
::Gitlab::Ci::Variables::Collection.new(secret_variables)
end
private private
def project_ids_match def project_ids_match
......
# frozen_string_literal: true
module Dast
class ProfilesPipeline < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
self.table_name = 'dast_profiles_pipelines'
belongs_to :ci_pipeline, class_name: 'Ci::Pipeline', optional: false, inverse_of: :dast_profiles_pipeline
belongs_to :dast_profile, class_name: 'Dast::Profile', optional: false, inverse_of: :dast_profiles_pipelines
end
end
...@@ -43,6 +43,17 @@ module EE ...@@ -43,6 +43,17 @@ module EE
end end
end end
override :variables
def variables
strong_memoize(:variables) do
super.tap do |collection|
if pipeline.triggered_for_ondemand_dast_scan? && pipeline.dast_profile
collection.concat(pipeline.dast_profile.ci_variables)
end
end
end
end
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
project.shared_runners_minutes_limit_enabled? && runner&.minutes_cost_factor(project.visibility_level)&.positive? project.shared_runners_minutes_limit_enabled? && runner&.minutes_cost_factor(project.visibility_level)&.positive?
end end
......
...@@ -20,6 +20,8 @@ module EE ...@@ -20,6 +20,8 @@ module EE
has_many :security_scans, class_name: 'Security::Scan', through: :builds has_many :security_scans, class_name: 'Security::Scan', through: :builds
has_many :security_findings, class_name: 'Security::Finding', through: :security_scans, source: :findings has_many :security_findings, class_name: 'Security::Finding', through: :security_scans, source: :findings
has_one :dast_profiles_pipeline, class_name: 'Dast::ProfilesPipeline', foreign_key: :ci_pipeline_id, inverse_of: :ci_pipeline
has_one :dast_profile, class_name: 'Dast::Profile', through: :dast_profiles_pipeline
has_one :source_project, class_name: 'Ci::Sources::Project', foreign_key: :pipeline_id has_one :source_project, class_name: 'Ci::Sources::Project', foreign_key: :pipeline_id
# Legacy way to fetch security reports based on job name. This has been replaced by the reports feature. # Legacy way to fetch security reports based on job name. This has been replaced by the reports feature.
...@@ -166,6 +168,11 @@ module EE ...@@ -166,6 +168,11 @@ module EE
security_findings.exists? security_findings.exists?
end end
def triggered_for_ondemand_dast_scan?
ondemand_dast_scan? && parameter_source? &&
::Feature.enabled?(:security_dast_site_profiles_additional_fields, project, default_enabled: :yaml)
end
private private
def has_security_reports? def has_security_reports?
......
...@@ -2,11 +2,14 @@ ...@@ -2,11 +2,14 @@
module Ci module Ci
class RunDastScanService < BaseService class RunDastScanService < BaseService
def execute(branch:, **args) def execute(branch:, dast_profile: nil, **args)
return ServiceResponse.error(message: 'Insufficient permissions') unless allowed? return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
service = Ci::CreatePipelineService.new(project, current_user, ref: branch) service = Ci::CreatePipelineService.new(project, current_user, ref: branch)
pipeline = service.execute(:ondemand_dast_scan, content: ci_yaml(args))
pipeline = service.execute(:ondemand_dast_scan, content: ci_yaml(args)) do |pipeline|
pipeline.dast_profile = dast_profile
end
if pipeline.created_successfully? if pipeline.created_successfully?
ServiceResponse.success(payload: pipeline) ServiceResponse.success(payload: pipeline)
......
...@@ -20,11 +20,7 @@ module Dast ...@@ -20,11 +20,7 @@ module Dast
response = ::DastOnDemandScans::CreateService.new( response = ::DastOnDemandScans::CreateService.new(
container: container, container: container,
current_user: current_user, current_user: current_user,
params: { params: { dast_profile: dast_profile }
branch: dast_profile.branch_name,
dast_site_profile: dast_site_profile,
dast_scanner_profile: dast_scanner_profile
}
).execute ).execute
return response if response.error? return response if response.error?
......
...@@ -47,16 +47,10 @@ module Dast ...@@ -47,16 +47,10 @@ module Dast
end end
def create_scan(dast_profile) def create_scan(dast_profile)
params = {
branch: dast_profile.branch_name,
dast_site_profile: dast_profile.dast_site_profile,
dast_scanner_profile: dast_profile.dast_scanner_profile
}
::DastOnDemandScans::CreateService.new( ::DastOnDemandScans::CreateService.new(
container: container, container: container,
current_user: current_user, current_user: current_user,
params: params params: { dast_profile: dast_profile }
).execute ).execute
end end
end end
......
...@@ -31,21 +31,27 @@ module DastOnDemandScans ...@@ -31,21 +31,27 @@ module DastOnDemandScans
end end
end end
def dast_site def dast_profile
strong_memoize(:dast_site) do strong_memoize(:dast_profile) do
dast_site_profile&.dast_site params[:dast_profile]
end end
end end
def dast_site_profile def dast_site_profile
strong_memoize(:dast_site_profile) do strong_memoize(:dast_site_profile) do
params[:dast_site_profile] dast_profile&.dast_site_profile || params[:dast_site_profile]
end end
end end
def dast_scanner_profile def dast_scanner_profile
strong_memoize(:dast_scanner_profile) do strong_memoize(:dast_scanner_profile) do
params[:dast_scanner_profile] dast_profile&.dast_scanner_profile || params[:dast_scanner_profile]
end
end
def dast_site
strong_memoize(:dast_site) do
dast_site_profile&.dast_site
end end
end end
...@@ -57,6 +63,7 @@ module DastOnDemandScans ...@@ -57,6 +63,7 @@ module DastOnDemandScans
def default_config def default_config
{ {
dast_profile: dast_profile,
branch: branch, branch: branch,
target_url: dast_site&.url target_url: dast_site&.url
} }
......
# frozen_string_literal: true
FactoryBot.define do
factory :dast_profiles_pipeline, class: 'Dast::ProfilesPipeline' do
dast_profile
ci_pipeline { association :ci_pipeline, project: dast_profile.project}
end
end
...@@ -44,19 +44,10 @@ RSpec.describe Mutations::Dast::Profiles::Create do ...@@ -44,19 +44,10 @@ RSpec.describe Mutations::Dast::Profiles::Create do
context 'when run_after_create=true' do context 'when run_after_create=true' do
let(:run_after_create) { true } let(:run_after_create) { true }
it 'returns the pipeline_url' do it_behaves_like 'it creates a DAST on-demand scan pipeline'
actual_url = subject[:pipeline_url]
pipeline = Ci::Pipeline.find_by( it_behaves_like 'it delegates scan creation to another service' do
project: project, let(:delegated_params) { hash_including(dast_profile: instance_of(Dast::Profile)) }
sha: project.repository.commits('orphaned-branch', limit: 1)[0].id,
source: :ondemand_dast_scan,
config_source: :parameter_source
)
expected_url = Rails.application.routes.url_helpers.project_pipeline_url(
project,
pipeline
)
expect(actual_url).to eq(expected_url)
end end
end end
......
...@@ -48,21 +48,23 @@ RSpec.describe Mutations::Dast::Profiles::Run do ...@@ -48,21 +48,23 @@ RSpec.describe Mutations::Dast::Profiles::Run do
project.add_developer(user) project.add_developer(user)
end end
it_behaves_like 'it delegates scan creation to another service' do it_behaves_like 'it creates a DAST on-demand scan pipeline' do
let(:delegated_params) do context 'when there is a dast_site_profile_secret_variable associated with the dast_profile' do
{ branch: dast_profile.branch_name, dast_site_profile: dast_profile.dast_site_profile, dast_scanner_profile: dast_profile.dast_scanner_profile } let_it_be(:dast_site_profile_secret_variable) { create(:dast_site_profile_secret_variable, dast_site_profile: dast_profile.dast_site_profile, raw_value: 'hello, world') }
it 'makes the variable available to the dast build' do
subject
dast_build = pipeline.builds.find_by!(name: 'dast')
variable = dast_build.variables.find { |var| var[:key] == dast_site_profile_secret_variable.key }
expect(Base64.strict_decode64(variable.value)).to include('hello, world')
end
end end
end end
it 'returns a pipeline_url containing the correct path' do it_behaves_like 'it delegates scan creation to another service' do
actual_url = subject[:pipeline_url] let(:delegated_params) { hash_including(dast_profile: dast_profile) }
pipeline = Ci::Pipeline.last
expected_url = Gitlab::Routing.url_helpers.project_pipeline_url(
project,
pipeline
)
expect(actual_url).to eq(expected_url)
end end
context 'when the dast_profile does not exist' do context 'when the dast_profile does not exist' do
......
...@@ -5,11 +5,12 @@ require 'spec_helper' ...@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe Mutations::Dast::Profiles::Update do RSpec.describe Mutations::Dast::Profiles::Update do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:dast_profile, reload: true) { create(:dast_profile, project: project, branch_name: 'audio') } let_it_be(:dast_profile, reload: true) { create(:dast_profile, project: project, branch_name: 'audio') }
let(:dast_profile_gid) { dast_profile.to_global_id } let(:dast_profile_gid) { dast_profile.to_global_id }
let(:run_after_update) { false }
let(:params) do let(:params) do
{ {
...@@ -18,7 +19,8 @@ RSpec.describe Mutations::Dast::Profiles::Update do ...@@ -18,7 +19,8 @@ RSpec.describe Mutations::Dast::Profiles::Update do
description: SecureRandom.hex, description: SecureRandom.hex,
branch_name: 'orphaned-branch', branch_name: 'orphaned-branch',
dast_site_profile_id: global_id_of(create(:dast_site_profile, project: project)), dast_site_profile_id: global_id_of(create(:dast_site_profile, project: project)),
dast_scanner_profile_id: global_id_of(create(:dast_scanner_profile, project: project)) dast_scanner_profile_id: global_id_of(create(:dast_scanner_profile, project: project)),
run_after_update: run_after_update
} }
end end
...@@ -77,6 +79,16 @@ RSpec.describe Mutations::Dast::Profiles::Update do ...@@ -77,6 +79,16 @@ RSpec.describe Mutations::Dast::Profiles::Update do
end end
end end
context 'when run_after_update=true' do
let(:run_after_update) { true }
it_behaves_like 'it creates a DAST on-demand scan pipeline'
it_behaves_like 'it delegates scan creation to another service' do
let(:delegated_params) { hash_including(dast_profile: dast_profile) }
end
end
context 'when the feature flag dast_branch_selection is disabled' do context 'when the feature flag dast_branch_selection is disabled' do
it 'does not set the branch_name' do it 'does not set the branch_name' do
stub_feature_flags(dast_branch_selection: false) stub_feature_flags(dast_branch_selection: false)
......
...@@ -147,6 +147,49 @@ RSpec.describe Ci::Build do ...@@ -147,6 +147,49 @@ RSpec.describe Ci::Build do
expect(features_variable[:value]).to include('multiple_ldap_servers') expect(features_variable[:value]).to include('multiple_ldap_servers')
end end
end end
context 'when there is a dast_profile associated with the pipeline' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let_it_be(:dast_site_profile_secret_variable) { create(:dast_site_profile_secret_variable, key: 'DAST_PASSWORD_BASE64', dast_site_profile: dast_profile.dast_site_profile) }
let(:pipeline) { create(:ci_pipeline, pipeline_params.merge!(project: project, dast_profile: dast_profile) ) }
let(:key) { dast_site_profile_secret_variable.key }
let(:value) { dast_site_profile_secret_variable.value }
shared_examples 'a pipeline with no dast on-demand variables' do
it 'does not include variables associated with the profile' do
keys = subject.to_runner_variables.map { |var| var[:key] }
expect(keys).not_to include(key)
end
end
it_behaves_like 'a pipeline with no dast on-demand variables' do
let(:pipeline_params) { { config_source: :parameter_source } }
end
it_behaves_like 'a pipeline with no dast on-demand variables' do
let(:pipeline_params) { { source: :ondemand_dast_scan } }
end
context 'when the dast on-demand pipeline is correctly configured' do
let(:pipeline_params) { { source: :ondemand_dast_scan, config_source: :parameter_source } }
it 'includes variables associated with the profile' do
expect(subject.to_runner_variables).to include(key: key, value: value, public: false, masked: true)
end
end
it_behaves_like 'a pipeline with no dast on-demand variables' do
let(:pipeline_params) { { source: :ondemand_dast_scan, config_source: :parameter_source } }
before do
stub_feature_flags(security_dast_site_profiles_additional_fields: false)
end
end
end
end end
describe 'variable CI_HAS_OPEN_REQUIREMENTS' do describe 'variable CI_HAS_OPEN_REQUIREMENTS' do
......
...@@ -12,11 +12,15 @@ RSpec.describe Ci::Pipeline do ...@@ -12,11 +12,15 @@ RSpec.describe Ci::Pipeline do
create(:ci_empty_pipeline, status: :created, project: project) create(:ci_empty_pipeline, status: :created, project: project)
end end
it { is_expected.to have_many(:security_scans).through(:builds).class_name('Security::Scan') } describe 'associations' do
it { is_expected.to have_many(:security_findings).through(:security_scans).class_name('Security::Finding').source(:findings) } it { is_expected.to have_many(:security_scans).through(:builds).class_name('Security::Scan') }
it { is_expected.to have_many(:downstream_bridges) } it { is_expected.to have_many(:security_findings).through(:security_scans).class_name('Security::Finding').source(:findings) }
it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::Finding') } it { is_expected.to have_many(:downstream_bridges) }
it { is_expected.to have_many(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::FindingPipeline') } it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::Finding') }
it { is_expected.to have_many(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::FindingPipeline') }
it { is_expected.to have_one(:dast_profiles_pipeline).class_name('Dast::ProfilesPipeline').with_foreign_key(:ci_pipeline_id).inverse_of(:ci_pipeline) }
it { is_expected.to have_one(:dast_profile).class_name('Dast::Profile').through(:dast_profiles_pipeline) }
end
describe '.failure_reasons' do describe '.failure_reasons' do
it 'contains failure reasons about exceeded limits' do it 'contains failure reasons about exceeded limits' do
...@@ -616,6 +620,37 @@ RSpec.describe Ci::Pipeline do ...@@ -616,6 +620,37 @@ RSpec.describe Ci::Pipeline do
end end
end end
describe '#triggered_for_ondemand_dast_scan?' do
let(:pipeline_params) { { source: :ondemand_dast_scan, config_source: :parameter_source } }
let(:pipeline) { build(:ci_pipeline, pipeline_params) }
subject { pipeline.triggered_for_ondemand_dast_scan? }
context 'when the feature flag is enabled' do
it { is_expected.to be_truthy }
context 'when the pipeline only has the correct source' do
let(:pipeline_params) { { source: :ondemand_dast_scan } }
it { is_expected.to be_falsey }
end
context 'when the pipeline only has the correct config_source' do
let(:pipeline_params) { { config_source: :parameter_source } }
it { is_expected.to be_falsey }
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(security_dast_site_profiles_additional_fields: false)
end
it { is_expected.to be_falsey }
end
end
describe '#needs_touch?' do describe '#needs_touch?' do
subject { pipeline.needs_touch? } subject { pipeline.needs_touch? }
......
...@@ -3,13 +3,17 @@ ...@@ -3,13 +3,17 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Dast::Profile, type: :model do RSpec.describe Dast::Profile, type: :model do
subject { create(:dast_profile) } let_it_be(:project) { create(:project) }
subject { create(:dast_profile, project: project) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:dast_site_profile) } it { is_expected.to belong_to(:dast_site_profile) }
it { is_expected.to belong_to(:dast_scanner_profile) } it { is_expected.to belong_to(:dast_scanner_profile) }
it { is_expected.to have_many(:secret_variables).through(:dast_site_profile).class_name('Dast::SiteProfileSecretVariable') } it { is_expected.to have_many(:secret_variables).through(:dast_site_profile).class_name('Dast::SiteProfileSecretVariable') }
it { is_expected.to have_many(:dast_profiles_pipelines).class_name('Dast::ProfilesPipeline').with_foreign_key(:dast_profile_id).inverse_of(:dast_profile) }
it { is_expected.to have_many(:ci_pipelines).through(:dast_profiles_pipelines).class_name('Ci::Pipeline') }
end end
describe 'validations' do describe 'validations' do
...@@ -24,7 +28,6 @@ RSpec.describe Dast::Profile, type: :model do ...@@ -24,7 +28,6 @@ RSpec.describe Dast::Profile, type: :model do
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
context 'when the project_id and dast_site_profile.project_id do not match' do context 'when the project_id and dast_site_profile.project_id do not match' do
let(:project) { create(:project) }
let(:dast_site_profile) { create(:dast_site_profile) } let(:dast_site_profile) { create(:dast_site_profile) }
subject { build(:dast_profile, project: project, dast_site_profile: dast_site_profile) } subject { build(:dast_profile, project: project, dast_site_profile: dast_site_profile) }
...@@ -38,7 +41,6 @@ RSpec.describe Dast::Profile, type: :model do ...@@ -38,7 +41,6 @@ RSpec.describe Dast::Profile, type: :model do
end end
context 'when the project_id and dast_scanner_profile.project_id do not match' do context 'when the project_id and dast_scanner_profile.project_id do not match' do
let(:project) { create(:project) }
let(:dast_scanner_profile) { create(:dast_scanner_profile) } let(:dast_scanner_profile) { create(:dast_scanner_profile) }
subject { build(:dast_profile, project: project, dast_scanner_profile: dast_scanner_profile) } subject { build(:dast_profile, project: project, dast_scanner_profile: dast_scanner_profile) }
...@@ -96,5 +98,21 @@ RSpec.describe Dast::Profile, type: :model do ...@@ -96,5 +98,21 @@ RSpec.describe Dast::Profile, type: :model do
end end
end end
end end
describe '#ci_variables' do
context 'when there are no secret_variables' do
it 'returns an empty collection' do
expect(subject.ci_variables.size).to be_zero
end
end
context 'when there are secret_variables' do
it 'returns a collection containing that variable' do
variable = create(:dast_site_profile_secret_variable, dast_site_profile: subject.dast_site_profile)
expect(subject.ci_variables.to_runner_variables).to include(key: variable.key, value: variable.value, public: false, masked: true)
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dast::Profile, type: :model do
subject { create(:dast_profiles_pipeline) }
describe 'associations' do
it { is_expected.to belong_to(:ci_pipeline).class_name('Ci::Pipeline').inverse_of(:dast_profiles_pipeline).required }
it { is_expected.to belong_to(:dast_profile).class_name('Dast::Profile').inverse_of(:dast_profiles_pipelines).required }
end
end
...@@ -3,17 +3,18 @@ ...@@ -3,17 +3,18 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::RunDastScanService do RSpec.describe Ci::RunDastScanService do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) } let_it_be(:project) { create(:project, :repository, creator: user) }
let(:branch) { project.default_branch } let_it_be(:dast_profile) { create(:dast_profile) }
let(:spider_timeout) { 42 } let_it_be(:branch) { project.default_branch }
let(:target_timeout) { 21 } let_it_be(:spider_timeout) { 42 }
let(:target_url) { generate(:url) } let_it_be(:target_timeout) { 21 }
let(:use_ajax_spider) { true } let_it_be(:target_url) { generate(:url) }
let(:show_debug_messages) { false } let_it_be(:use_ajax_spider) { true }
let(:full_scan_enabled) { true } let_it_be(:show_debug_messages) { false }
let(:excluded_urls) { "#{target_url}/hello,#{target_url}/world" } let_it_be(:full_scan_enabled) { true }
let(:auth_url) { "#{target_url}/login" } let_it_be(:excluded_urls) { "#{target_url}/hello,#{target_url}/world" }
let_it_be(:auth_url) { "#{target_url}/login" }
before do before do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
...@@ -33,7 +34,8 @@ RSpec.describe Ci::RunDastScanService do ...@@ -33,7 +34,8 @@ RSpec.describe Ci::RunDastScanService do
auth_url: auth_url, auth_url: auth_url,
auth_username_field: 'session[username]', auth_username_field: 'session[username]',
auth_password_field: 'session[password]', auth_password_field: 'session[password]',
auth_username: 'tanuki' auth_username: 'tanuki',
dast_profile: dast_profile
) )
end end
...@@ -173,6 +175,26 @@ RSpec.describe Ci::RunDastScanService do ...@@ -173,6 +175,26 @@ RSpec.describe Ci::RunDastScanService do
expect(build.yaml_variables).to contain_exactly(*expected_variables) expect(build.yaml_variables).to contain_exactly(*expected_variables)
end end
it 'associates the dast_profile with the pipeline' do
expect(pipeline.dast_profile).to eq(dast_profile)
end
context 'when no dast_profile is provided' do
let(:dast_profile) { nil }
it 'does not create a new association' do
expect(pipeline.dast_profile).to be_nil
end
end
context 'when creating the association betweeen the dast_profile and the pipeline fails' do
let_it_be(:dast_profile) { build(:dast_site_profile) }
it 'does not create a Ci::Pipeline' do
expect { subject }.to raise_error(ActiveRecord::AssociationTypeMismatch).and change { Ci::Pipeline.count }.by(0)
end
end
context 'when the pipeline fails to save' do context 'when the pipeline fails to save' do
before do before do
allow_any_instance_of(Ci::Pipeline).to receive(:created_successfully?).and_return(false) allow_any_instance_of(Ci::Pipeline).to receive(:created_successfully?).and_return(false)
......
...@@ -52,9 +52,7 @@ RSpec.describe Dast::Profiles::CreateService do ...@@ -52,9 +52,7 @@ RSpec.describe Dast::Profiles::CreateService do
let(:params) { default_params.merge(run_after_create: true) } let(:params) { default_params.merge(run_after_create: true) }
it_behaves_like 'it delegates scan creation to another service' do it_behaves_like 'it delegates scan creation to another service' do
let(:delegated_params) do let(:delegated_params) { hash_including(dast_profile: instance_of(Dast::Profile)) }
{ branch: default_params[:branch_name], dast_site_profile: dast_site_profile, dast_scanner_profile: dast_scanner_profile }
end
end end
it 'creates a ci_pipeline' do it 'creates a ci_pipeline' do
......
...@@ -80,9 +80,7 @@ RSpec.describe Dast::Profiles::UpdateService do ...@@ -80,9 +80,7 @@ RSpec.describe Dast::Profiles::UpdateService do
let(:params) { default_params.merge(run_after_update: true) } let(:params) { default_params.merge(run_after_update: true) }
it_behaves_like 'it delegates scan creation to another service' do it_behaves_like 'it delegates scan creation to another service' do
let(:delegated_params) do let(:delegated_params) { hash_including(dast_profile: dast_profile) }
{ branch: dast_profile.branch_name, dast_site_profile: dast_site_profile, dast_scanner_profile: dast_scanner_profile }
end
end end
it 'creates a ci_pipeline' do it 'creates a ci_pipeline' do
......
...@@ -18,6 +18,18 @@ RSpec.describe DastOnDemandScans::CreateService do ...@@ -18,6 +18,18 @@ RSpec.describe DastOnDemandScans::CreateService do
).execute ).execute
end end
shared_examples 'a service that calls Ci::RunDastScanService' do
it 'delegates pipeline creation to Ci::RunDastScanService', :aggregate_failures do
service = double(Ci::RunDastScanService)
response = ServiceResponse.error(message: 'Stubbed response')
expect(Ci::RunDastScanService).to receive(:new).and_return(service)
expect(service).to receive(:execute).with(expected_params).and_return(response)
subject
end
end
describe 'execute' do describe 'execute' do
context 'when on demand scan licensed feature is not available' do context 'when on demand scan licensed feature is not available' do
context 'when the user cannot run an on demand scan' do context 'when the user cannot run an on demand scan' do
...@@ -49,28 +61,23 @@ RSpec.describe DastOnDemandScans::CreateService do ...@@ -49,28 +61,23 @@ RSpec.describe DastOnDemandScans::CreateService do
expect(subject.payload[:pipeline_url]).to be_a(String) expect(subject.payload[:pipeline_url]).to be_a(String)
end end
it 'delegates pipeline creation to Ci::RunDastScanService', :aggregate_failures do it_behaves_like 'a service that calls Ci::RunDastScanService' do
expected_params = { let(:expected_params) do
auth_password_field: dast_site_profile.auth_password_field, {
auth_username: dast_site_profile.auth_username, auth_password_field: dast_site_profile.auth_password_field,
auth_username_field: dast_site_profile.auth_username_field, auth_username: dast_site_profile.auth_username,
branch: project.default_branch_or_master, auth_username_field: dast_site_profile.auth_username_field,
excluded_urls: dast_site_profile.excluded_urls.join(','), branch: project.default_branch_or_master,
full_scan_enabled: false, dast_profile: nil,
show_debug_messages: false, excluded_urls: dast_site_profile.excluded_urls.join(','),
spider_timeout: nil, full_scan_enabled: false,
target_timeout: nil, show_debug_messages: false,
target_url: dast_site_profile.dast_site.url, spider_timeout: nil,
use_ajax_spider: false target_timeout: nil,
} target_url: dast_site_profile.dast_site.url,
use_ajax_spider: false
service = double(Ci::RunDastScanService) }
response = ServiceResponse.error(message: 'Stubbed response') end
expect(Ci::RunDastScanService).to receive(:new).and_return(service)
expect(service).to receive(:execute).with(expected_params).and_return(response)
subject
end end
context 'when a branch is specified' do context 'when a branch is specified' do
...@@ -100,6 +107,20 @@ RSpec.describe DastOnDemandScans::CreateService do ...@@ -100,6 +107,20 @@ RSpec.describe DastOnDemandScans::CreateService do
end end
end end
context 'when dast_profile is specified' do
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let(:params) { { dast_profile: dast_profile } }
it 'communicates success' do
expect(subject.status).to eq(:success)
end
it_behaves_like 'a service that calls Ci::RunDastScanService' do
let(:expected_params) { hash_including(dast_profile: dast_profile) }
end
end
context 'when target is not validated and an active scan is requested' do context 'when target is not validated and an active scan is requested' do
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, scan_type: 'active') } let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, scan_type: 'active') }
......
...@@ -39,12 +39,13 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do ...@@ -39,12 +39,13 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do
it 'returns prepared scanner params in the payload' do it 'returns prepared scanner params in the payload' do
expect(subject.payload).to eq( expect(subject.payload).to eq(
auth_password_field: dast_site_profile.auth_password_field,
auth_username: dast_site_profile.auth_username,
auth_username_field: dast_site_profile.auth_username_field,
branch: project.default_branch, branch: project.default_branch,
target_url: dast_site_profile.dast_site.url, dast_profile: nil,
excluded_urls: dast_site_profile.excluded_urls.join(','), excluded_urls: dast_site_profile.excluded_urls.join(','),
auth_username_field: dast_site_profile.auth_username_field, target_url: dast_site_profile.dast_site.url
auth_password_field: dast_site_profile.auth_password_field,
auth_username: dast_site_profile.auth_username
) )
end end
end end
...@@ -54,16 +55,17 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do ...@@ -54,16 +55,17 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do
it 'returns prepared scanner params in the payload' do it 'returns prepared scanner params in the payload' do
expect(subject.payload).to eq( expect(subject.payload).to eq(
branch: project.default_branch,
target_url: dast_site_profile.dast_site.url,
excluded_urls: dast_site_profile.excluded_urls.join(','),
auth_username_field: dast_site_profile.auth_username_field,
auth_password_field: dast_site_profile.auth_password_field, auth_password_field: dast_site_profile.auth_password_field,
auth_username: dast_site_profile.auth_username, auth_username: dast_site_profile.auth_username,
auth_username_field: dast_site_profile.auth_username_field,
branch: project.default_branch,
dast_profile: nil,
excluded_urls: dast_site_profile.excluded_urls.join(','),
full_scan_enabled: false, full_scan_enabled: false,
show_debug_messages: false, show_debug_messages: false,
spider_timeout: nil, spider_timeout: nil,
target_timeout: nil, target_timeout: nil,
target_url: dast_site_profile.dast_site.url,
use_ajax_spider: false use_ajax_spider: false
) )
end end
...@@ -80,5 +82,28 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do ...@@ -80,5 +82,28 @@ RSpec.describe DastOnDemandScans::ParamsCreateService do
end end
end end
end end
context 'when the dast_profile is provided' do
let_it_be(:dast_profile) { create(:dast_profile, project: project, dast_site_profile: dast_site_profile, dast_scanner_profile: dast_scanner_profile) }
let(:params) { { dast_profile: dast_profile } }
it 'returns prepared scanner params in the payload' do
expect(subject.payload).to eq(
auth_password_field: dast_site_profile.auth_password_field,
auth_username: dast_site_profile.auth_username,
auth_username_field: dast_site_profile.auth_username_field,
branch: project.default_branch,
dast_profile: dast_profile,
excluded_urls: dast_site_profile.excluded_urls.join(','),
full_scan_enabled: false,
show_debug_messages: false,
spider_timeout: nil,
target_timeout: nil,
target_url: dast_site_profile.dast_site.url,
use_ajax_spider: false
)
end
end
end end
end end
...@@ -53,6 +53,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::OnDemandScanPipelineConf ...@@ -53,6 +53,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::OnDemandScanPipelineConf
auth_password_field: site_profile.auth_password_field, auth_password_field: site_profile.auth_password_field,
auth_username: site_profile.auth_username, auth_username: site_profile.auth_username,
auth_username_field: site_profile.auth_username_field, auth_username_field: site_profile.auth_username_field,
dast_profile: nil,
branch: project.default_branch_or_master, branch: project.default_branch_or_master,
excluded_urls: site_profile.excluded_urls.join(','), excluded_urls: site_profile.excluded_urls.join(','),
full_scan_enabled: false, full_scan_enabled: false,
......
# frozen_string_literal: true
require 'spec_helper'
# There must be a methods or lets called `project` and `dast_profile` defined.
RSpec.shared_examples 'it creates a DAST on-demand scan pipeline' do
let(:pipeline) do
Ci::Pipeline.find_by!(
project: project,
sha: project.repository.commit.sha,
source: :ondemand_dast_scan,
config_source: :parameter_source
)
end
it 'creates a new ci_pipeline for the given project', :aggregate_failures do
expect { subject }.to change { Ci::Pipeline.where(project: project).count }.by(1)
expect(pipeline.triggered_for_ondemand_dast_scan?).to be_truthy
end
it 'creates a single build associated with the ci_pipeline' do
subject
expect(pipeline.builds.map(&:name)).to eq(['dast'])
end
it 'creates an association between the dast_profile and the ci_pipeline' do
subject
expect(dast_profile.ci_pipelines).to include(pipeline)
end
it 'returns the pipeline_url' do
subject
expected_url = Rails.application.routes.url_helpers.project_pipeline_url(
project,
pipeline
)
expect(subject[:pipeline_url]).to eq(expected_url)
end
end
...@@ -256,6 +256,8 @@ ci_pipelines: ...@@ -256,6 +256,8 @@ ci_pipelines:
- messages - messages
- pipeline_artifacts - pipeline_artifacts
- latest_statuses - latest_statuses
- dast_profile
- dast_profiles_pipeline
ci_refs: ci_refs:
- project - project
- ci_pipelines - ci_pipelines
......
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