Commit 0fa2a7b4 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by James Fargher

Add validation to check if branches are valid for security policy

Changelog: added
EE: true
parent fa814c9e
...@@ -8,6 +8,9 @@ module Security ...@@ -8,6 +8,9 @@ module Security
return error('Security Policy Project does not exist') unless policy_configuration.present? return error('Security Policy Project does not exist') unless policy_configuration.present?
validation_result = validate_policy_yaml
return error(validation_result[:message], :bad_request) if validation_result[:status] != :success
process_policy_result = process_policy process_policy_result = process_policy
return process_policy_result if process_policy_result[:status] != :success return process_policy_result if process_policy_result[:status] != :success
...@@ -21,6 +24,12 @@ module Security ...@@ -21,6 +24,12 @@ module Security
private private
def validate_policy_yaml
Security::SecurityOrchestrationPolicies::ValidatePolicyService
.new(project: project, params: { policy: policy })
.execute
end
def process_policy def process_policy
ProcessPolicyService.new( ProcessPolicyService.new(
policy_configuration: policy_configuration, policy_configuration: policy_configuration,
......
...@@ -16,7 +16,6 @@ module Security ...@@ -16,7 +16,6 @@ module Security
name = params[:name] name = params[:name]
operation = params[:operation] operation = params[:operation]
return error("Invalid policy type", :bad_request) unless Security::OrchestrationPolicyConfiguration::AVAILABLE_POLICY_TYPES.include?(type)
return error("Name should be same as the policy name", :bad_request) if name && operation != :replace && policy[:name] != name return error("Name should be same as the policy name", :bad_request) if name && operation != :replace && policy[:name] != name
policy_hash = policy_configuration.policy_hash.dup || {} policy_hash = policy_configuration.policy_hash.dup || {}
......
# frozen_string_literal: true
module Security
module SecurityOrchestrationPolicies
class ValidatePolicyService < ::BaseProjectService
def execute
return success if policy_disabled?
return error(s_('SecurityOrchestration|Invalid policy type')) if invalid_policy_type?
return error(s_('SecurityOrchestration|Policy cannot be enabled without branch information')) if blank_branch_for_rule?
return error(s_('SecurityOrchestration|Policy cannot be enabled for non-existing branches (%{branches})') % { branches: missing_branch_names.join(', ') }) if missing_branch_for_rule?
success
end
private
def policy_disabled?
!policy&.[](:enabled)
end
def invalid_policy_type?
return true if policy[:type].blank?
!Security::OrchestrationPolicyConfiguration::AVAILABLE_POLICY_TYPES.include?(policy[:type].to_sym)
end
def blank_branch_for_rule?
policy[:rules].any? { |rule| rule[:clusters].blank? && rule[:branches].blank? }
end
def missing_branch_for_rule?
return false if project.blank?
missing_branch_names.present?
end
def missing_branch_names
strong_memoize(:missing_branch_names) do
policy[:rules]
.select { |rule| rule[:clusters].blank? }
.flat_map { |rule| rule[:branches] }
.compact
.uniq
.select { |pattern| RefMatcher.new(pattern).matching(branches_for_project).blank? }
end
end
def policy
@policy ||= params[:policy]
end
def branches_for_project
strong_memoize(:branches_for_project) do
repository.branch_names
end
end
end
end
end
...@@ -17,11 +17,11 @@ FactoryBot.define do ...@@ -17,11 +17,11 @@ FactoryBot.define do
sequence(:name) { |n| "test-policy-#{n}" } sequence(:name) { |n| "test-policy-#{n}" }
description { 'This policy enforces to run DAST for every pipeline within the project' } description { 'This policy enforces to run DAST for every pipeline within the project' }
enabled { true } enabled { true }
rules { [{ type: 'pipeline', branches: %w[production] }] } rules { [{ type: 'pipeline', branches: %w[master] }] }
actions { [{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' }] } actions { [{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' }] }
trait :with_schedule do trait :with_schedule do
rules { [{ type: 'schedule', branches: %w[production], cadence: '*/15 * * * *' }] } rules { [{ type: 'schedule', branches: %w[master], cadence: '*/15 * * * *' }] }
end end
end end
......
...@@ -6,7 +6,7 @@ RSpec.describe Mutations::SecurityPolicy::CommitScanExecutionPolicy do ...@@ -6,7 +6,7 @@ RSpec.describe Mutations::SecurityPolicy::CommitScanExecutionPolicy do
describe '#resolve' do describe '#resolve' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) } let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
let_it_be(:policy_management_project) { create(:project, :repository, namespace: user.namespace) } let_it_be(:policy_management_project) { create(:project, :repository, namespace: user.namespace) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, security_policy_management_project: policy_management_project, project: project) } let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, security_policy_management_project: policy_management_project, project: project) }
let_it_be(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } let_it_be(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
......
...@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Ci::Config do
describe 'with security orchestration policy' do describe 'with security orchestration policy' do
let(:source) { 'push' } let(:source) { 'push' }
let_it_be(:ref) { 'master' } let(:ref) { 'master' }
let_it_be_with_refind(:project) { create(:project, :repository) } let_it_be_with_refind(:project) { create(:project, :repository) }
let_it_be(:policies_repository) { create(:project, :repository) } let_it_be(:policies_repository) { create(:project, :repository) }
...@@ -70,13 +70,15 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -70,13 +70,15 @@ RSpec.describe Gitlab::Ci::Config do
end end
context 'when policy is not applicable on branch from the pipeline' do context 'when policy is not applicable on branch from the pipeline' do
let(:ref) { 'another-branch' }
it 'does not modify the config' do it 'does not modify the config' do
expect(config.to_hash).to eq(sample_job: { script: ["echo 'test'"] }) expect(config.to_hash).to eq(sample_job: { script: ["echo 'test'"] })
end end
end end
context 'when policy is not applicable on branch from the pipeline' do context 'when policy is applicable on branch from the pipeline' do
let_it_be(:ref) { 'production' } let(:ref) { 'master' }
context 'when DAST profiles are not found' do context 'when DAST profiles are not found' do
it 'adds a job with error message' do it 'adds a job with error message' do
......
...@@ -69,21 +69,23 @@ RSpec.describe Gitlab::Ci::Config::SecurityOrchestrationPolicies::Processor do ...@@ -69,21 +69,23 @@ RSpec.describe Gitlab::Ci::Config::SecurityOrchestrationPolicies::Processor do
end end
context 'when policy is not applicable on branch from the pipeline' do context 'when policy is not applicable on branch from the pipeline' do
let(:ref) { 'refs/head/another-branch' }
it 'does not modify the config' do it 'does not modify the config' do
expect(subject).to eq(config) expect(subject).to eq(config)
end end
end end
context 'when ref is a tag' do context 'when ref is a tag' do
let_it_be(:ref) { 'refs/tags/v1.1.0' } let(:ref) { 'refs/tags/v1.1.0' }
it 'does not modify the config' do it 'does not modify the config' do
expect(subject).to eq(config) expect(subject).to eq(config)
end end
end end
context 'when policy is not applicable on branch from the pipeline' do context 'when policy is applicable on branch from the pipeline' do
let_it_be(:ref) { 'refs/heads/production' } let(:ref) { 'refs/heads/master' }
context 'when DAST profiles are not found' do context 'when DAST profiles are not found' do
it 'does not modify the config' do it 'does not modify the config' do
......
...@@ -226,7 +226,7 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do ...@@ -226,7 +226,7 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do
let(:expected_active_policies) do let(:expected_active_policies) do
[ [
build(:scan_execution_policy, name: 'Run DAST in every pipeline'), build(:scan_execution_policy, name: 'Run DAST in every pipeline', rules: [{ type: 'pipeline', branches: %w[production] }]),
build(:scan_execution_policy, name: 'Run DAST in every pipeline_v1', rules: [{ type: 'pipeline', branches: %w[master] }]), build(:scan_execution_policy, name: 'Run DAST in every pipeline_v1', rules: [{ type: 'pipeline', branches: %w[master] }]),
build(:scan_execution_policy, name: 'Run DAST in every pipeline_v3', rules: [{ type: 'pipeline', branches: %w[master] }]), build(:scan_execution_policy, name: 'Run DAST in every pipeline_v3', rules: [{ type: 'pipeline', branches: %w[master] }]),
build(:scan_execution_policy, name: 'Run DAST in every pipeline_v4', rules: [{ type: 'pipeline', branches: %w[master] }]), build(:scan_execution_policy, name: 'Run DAST in every pipeline_v4', rules: [{ type: 'pipeline', branches: %w[master] }]),
...@@ -291,7 +291,7 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do ...@@ -291,7 +291,7 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do
end end
subject(:pipeline_scan_actions) do subject(:pipeline_scan_actions) do
security_orchestration_policy_configuration.pipeline_scan_actions('refs/heads/production') security_orchestration_policy_configuration.pipeline_scan_actions('refs/heads/master')
end end
it 'returns only actions for pipeline scans applicable for branch' do it 'returns only actions for pipeline scans applicable for branch' do
......
...@@ -48,7 +48,7 @@ RSpec.describe 'Create scan execution policy for a project' do ...@@ -48,7 +48,7 @@ RSpec.describe 'Create scan execution policy for a project' do
end end
context 'when provided policy is invalid' do context 'when provided policy is invalid' do
let_it_be(:policy_yaml) { build(:scan_execution_policy, name: policy_name).merge(type: 'scan_execution_policy', rules: [{ type: 'invalid_type' }]).to_yaml } let_it_be(:policy_yaml) { build(:scan_execution_policy, name: policy_name).merge(type: 'scan_execution_policy', rules: [{ type: 'invalid_type', branches: ['master'] }]).to_yaml }
it 'returns error with detailed information' do it 'returns error with detailed information' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
......
...@@ -6,7 +6,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do ...@@ -6,7 +6,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do
include RepoHelpers include RepoHelpers
describe '#execute' do describe '#execute' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { project.first_owner } let_it_be(:current_user) { project.first_owner }
let_it_be(:policy_management_project) { create(:project, :repository, creator: current_user) } let_it_be(:policy_management_project) { create(:project, :repository, creator: current_user) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, security_policy_management_project: policy_management_project, project: project) } let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, security_policy_management_project: policy_management_project, project: project) }
...@@ -46,8 +46,21 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do ...@@ -46,8 +46,21 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do
end end
end end
context 'when defined branch is missing' do
let(:policy_hash) { build(:scan_execution_policy, name: 'Test Policy', rules: [{ type: 'pipeline' }]) }
let(:params) { { policy_yaml: input_policy_yaml, operation: operation } }
it 'returns error' do
response = service.execute
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('Policy cannot be enabled without branch information')
end
end
context 'when security_orchestration_policies_configuration does not exist for project' do context 'when security_orchestration_policies_configuration does not exist for project' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :repository) }
it 'does not create new project' do it 'does not create new project' do
response = service.execute response = service.execute
......
...@@ -49,17 +49,6 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -49,17 +49,6 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
end end
end end
context 'when type is invalid' do
let(:type) { :invalid_type }
it 'returns error' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid policy type')
end
end
context 'append policy' do context 'append policy' do
context 'when policy is present in repository' do context 'when policy is present in repository' do
before do before do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::SecurityOrchestrationPolicies::ValidatePolicyService do
describe '#execute' do
let(:service) { described_class.new(project: project, params: { policy: policy }) }
let(:enabled) { true }
let(:policy_type) { 'scan_execution_policy' }
let(:rule) { { clusters: { production: {} } } }
let(:policy) do
{
type: policy_type,
enabled: enabled,
rules: [rule]
}
end
subject(:result) { service.execute }
shared_examples 'checks only if policy is enabled' do
let(:enabled) { false }
it { expect(result[:status]).to eq(:success) }
end
shared_examples 'checks policy type' do
context 'when policy type is not provided' do
let(:policy_type) { nil }
it { expect(result[:status]).to eq(:error) }
it { expect(result[:message]).to eq('Invalid policy type') }
end
context 'when policy type is invalid' do
let(:policy_type) { 'invalid_policy_type' }
it { expect(result[:status]).to eq(:error) }
it { expect(result[:message]).to eq('Invalid policy type') }
end
context 'when policy type is valid' do
it { expect(result[:status]).to eq(:success) }
end
end
shared_examples 'checks if branches are provided in rule' do
context 'when rule has clusters defined' do
let(:rule) do
{
clusters: {
production: {}
},
branches: branches
}
end
context 'when branches are missing' do
let(:branches) { nil }
it { expect(result[:status]).to eq(:success) }
end
context 'when branches are provided' do
let(:branches) { ['master'] }
it { expect(result[:status]).to eq(:success) }
end
end
context 'when rule does not have clusters defined' do
let(:rule) do
{
branches: branches
}
end
context 'when branches are missing' do
let(:branches) { nil }
it { expect(result[:status]).to eq(:error) }
it { expect(result[:message]).to eq('Policy cannot be enabled without branch information') }
it_behaves_like 'checks only if policy is enabled'
end
context 'when branches are provided' do
let(:branches) { ['master'] }
it { expect(result[:status]).to eq(:success) }
end
end
end
shared_examples 'checks if branches are defined in the project' do
context 'when rule has clusters defined' do
let(:rule) do
{
clusters: {
production: {}
},
branches: branches
}
end
context 'when branches are defined for project' do
let(:branches) { ['master'] }
it { expect(result[:status]).to eq(:success) }
end
context 'when branches are not defined for project' do
let(:branches) { ['non-exising-branch'] }
it { expect(result[:status]).to eq(:success) }
end
context 'when pattern does not match any branch defined for project' do
let(:branches) { ['master', 'production-*', 'test-*'] }
it { expect(result[:status]).to eq(:success) }
end
end
context 'when rule does not have clusters defined' do
let(:rule) do
{
branches: branches
}
end
context 'when branches are defined for project' do
let(:branches) { ['master'] }
it { expect(result[:status]).to eq(:success) }
end
context 'when branches are not defined for project' do
let(:branches) { ['non-exising-branch'] }
it { expect(result[:status]).to eq(:error) }
it { expect(result[:message]).to eq('Policy cannot be enabled for non-existing branches (non-exising-branch)') }
it_behaves_like 'checks only if policy is enabled'
end
context 'when branches are defined as pattern' do
context 'when pattern matches at least one branch defined for project' do
let(:branches) { ['*'] }
it { expect(result[:status]).to eq(:success) }
end
context 'when pattern does not match any branch defined for project' do
let(:branches) { ['master', 'production-*', 'test-*'] }
it { expect(result[:status]).to eq(:error) }
it { expect(result[:message]).to eq('Policy cannot be enabled for non-existing branches (production-*, test-*)') }
it_behaves_like 'checks only if policy is enabled'
end
end
end
end
context 'when project is not provided' do
let_it_be(:project) { nil }
it_behaves_like 'checks policy type'
it_behaves_like 'checks if branches are provided in rule'
end
context 'when project is provided' do
let_it_be(:project) { create(:project, :repository) }
it_behaves_like 'checks policy type'
it_behaves_like 'checks if branches are provided in rule'
it_behaves_like 'checks if branches are defined in the project'
end
end
end
...@@ -31846,6 +31846,9 @@ msgstr "" ...@@ -31846,6 +31846,9 @@ msgstr ""
msgid "SecurityOrchestration|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}." msgid "SecurityOrchestration|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}."
msgstr "" msgstr ""
msgid "SecurityOrchestration|Invalid policy type"
msgstr ""
msgid "SecurityOrchestration|Latest scan" msgid "SecurityOrchestration|Latest scan"
msgstr "" msgstr ""
...@@ -31864,6 +31867,12 @@ msgstr "" ...@@ -31864,6 +31867,12 @@ msgstr ""
msgid "SecurityOrchestration|Policies" msgid "SecurityOrchestration|Policies"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Policy cannot be enabled for non-existing branches (%{branches})"
msgstr ""
msgid "SecurityOrchestration|Policy cannot be enabled without branch information"
msgstr ""
msgid "SecurityOrchestration|Policy description" msgid "SecurityOrchestration|Policy description"
msgstr "" msgstr ""
......
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