ee/spec/models/security/orchestration_policy_configuration_spec.rb (3,157 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::OrchestrationPolicyConfiguration, feature_category: :security_policy_management do
let_it_be(:security_policy_management_project) { create(:project, :repository) }
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, security_policy_management_project: security_policy_management_project)
end
let(:default_branch) { security_policy_management_project.default_branch }
let(:repository) { instance_double(Repository, root_ref: 'master', empty?: false) }
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [build(:scan_execution_policy, name: 'Run DAST in every pipeline')], approval_policy: [build(:approval_policy, name: 'Contain security critical severities')]) }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
shared_examples 'captures git errors' do |repository_method|
context 'when repository is unavailable' do
before do
allow(repository).to receive(repository_method).and_raise(GRPC::BadStatus, GRPC::Core::StatusCodes::DEADLINE_EXCEEDED)
end
it { is_expected.to be_nil }
it 'tracks the exception' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(Gitlab::Git::CommandTimedOut, action: repository_method, security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
subject
end
end
end
shared_examples 'does not deletes merge request approval rules of merged MR' do
context 'with approval rules for merged MRs' do
let(:merge_request_to_be_merged) do
create(:merge_request,
target_project: project,
source_project: project,
source_branch: 'feature-1')
end
let!(:approval_merge_rule_merged_mr) do
create(:report_approver_rule,
:scan_finding,
merge_request: merge_request_to_be_merged,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
before do
merge_request_to_be_merged.mark_as_merged!
end
it 'does not deletes merge request approval rules of merged MRs' do
subject
expect(ApprovalMergeRequestRule.find(approval_merge_rule_merged_mr.id)).to be_present
end
end
end
describe 'associations' do
it { is_expected.to belong_to(:project).inverse_of(:security_orchestration_policy_configuration) }
it { is_expected.to belong_to(:namespace).inverse_of(:security_orchestration_policy_configuration) }
it { is_expected.to belong_to(:security_policy_management_project).class_name('Project') }
it { is_expected.to have_many(:rule_schedules).class_name('Security::OrchestrationPolicyRuleSchedule').inverse_of(:security_orchestration_policy_configuration) }
it { is_expected.to have_many(:compliance_framework_security_policies).class_name('ComplianceManagement::ComplianceFramework::SecurityPolicy') }
it { is_expected.to have_many(:security_policies).class_name('Security::Policy') }
end
describe 'validations' do
subject(:configuration) { create(:security_orchestration_policy_configuration) }
context 'when created for project' do
it { is_expected.not_to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_uniqueness_of(:project) }
end
context 'when created for namespace' do
subject { create(:security_orchestration_policy_configuration, :namespace) }
it { is_expected.not_to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_uniqueness_of(:namespace) }
end
it { is_expected.to validate_presence_of(:security_policy_management_project) }
describe 'experiments field' do
context 'when provided nil as experiments value' do
it 'is valid' do
configuration.experiments = nil
expect(configuration).to be_valid
end
end
context 'when provided {} as experiments value' do
it 'is valid' do
configuration.experiments = {}
expect(configuration).to be_valid
end
end
context 'when provided invalid experiments value' do
it 'is invalid' do
configuration.experiments = { test_feature: true }
expect(configuration).to be_invalid
end
end
context 'when provided valid experiments value' do
it 'is valid' do
configuration.experiments = { test_feature: { enabled: true, configuration: { key: 'value' } } }
expect(configuration).to be_valid
end
end
end
end
describe '.for_project' do
let_it_be(:security_orchestration_policy_configuration_1) { create(:security_orchestration_policy_configuration) }
let_it_be(:security_orchestration_policy_configuration_2) { create(:security_orchestration_policy_configuration) }
let_it_be(:security_orchestration_policy_configuration_3) { create(:security_orchestration_policy_configuration) }
subject { described_class.for_project([security_orchestration_policy_configuration_2.project, security_orchestration_policy_configuration_3.project]) }
it 'returns configuration for given projects' do
is_expected.to contain_exactly(security_orchestration_policy_configuration_2, security_orchestration_policy_configuration_3)
end
end
describe '.for_namespace' do
let_it_be(:security_orchestration_policy_configuration_1) { create(:security_orchestration_policy_configuration, :namespace) }
let_it_be(:security_orchestration_policy_configuration_2) { create(:security_orchestration_policy_configuration, :namespace) }
let_it_be(:security_orchestration_policy_configuration_3) { create(:security_orchestration_policy_configuration, :namespace) }
subject { described_class.for_namespace([security_orchestration_policy_configuration_2.namespace, security_orchestration_policy_configuration_3.namespace]) }
it 'returns configuration for given namespaces' do
is_expected.to contain_exactly(security_orchestration_policy_configuration_2, security_orchestration_policy_configuration_3)
end
end
describe '.for_management_project' do
let_it_be(:security_orchestration_policy_configuration_1) { create(:security_orchestration_policy_configuration, security_policy_management_project: security_policy_management_project) }
let_it_be(:security_orchestration_policy_configuration_2) { create(:security_orchestration_policy_configuration, security_policy_management_project: security_policy_management_project) }
let_it_be(:security_orchestration_policy_configuration_3) { create(:security_orchestration_policy_configuration) }
subject { described_class.for_management_project(security_policy_management_project) }
it 'returns configuration for given the policy management project' do
is_expected.to contain_exactly(security_orchestration_policy_configuration_1, security_orchestration_policy_configuration_2)
end
end
describe '.with_outdated_configuration' do
let!(:security_orchestration_policy_configuration_1) { create(:security_orchestration_policy_configuration, configured_at: nil) }
let!(:security_orchestration_policy_configuration_2) { create(:security_orchestration_policy_configuration, configured_at: Time.zone.now - 1.hour) }
let!(:security_orchestration_policy_configuration_3) { create(:security_orchestration_policy_configuration, configured_at: Time.zone.now + 1.hour) }
subject { described_class.with_outdated_configuration }
it 'returns configuration with outdated configurations' do
is_expected.to contain_exactly(security_orchestration_policy_configuration_1, security_orchestration_policy_configuration_2)
end
end
describe '.for_management_project_within_descendants' do
let_it_be(:top_level_group) { create(:group) }
let_it_be(:subgroup_a) { create(:group, parent: top_level_group) }
let_it_be(:subgroup_b) { create(:group, parent: subgroup_a) }
let_it_be(:top_level_group_project) { create(:project, group: top_level_group) }
let_it_be(:subgroup_project) { create(:project, group: subgroup_a) }
let!(:policy_configuration_a) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: top_level_group.id)
end
let!(:policy_configuration_b) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: subgroup_b.id,
security_policy_management_project_id: policy_project_id)
end
let!(:policy_configuration_c) do
create(
:security_orchestration_policy_configuration,
project: top_level_group_project,
security_policy_management_project_id: policy_project_id)
end
let!(:policy_configuration_d) do
create(
:security_orchestration_policy_configuration,
project: subgroup_project,
security_policy_management_project_id: policy_project_id)
end
let!(:other_policy_configuration) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: subgroup_a.id)
end
let(:policy_project_id) { policy_configuration_a.security_policy_management_project_id }
subject { described_class.for_management_project_within_descendants(policy_project_id, top_level_group) }
it { is_expected.to contain_exactly(policy_configuration_b, policy_configuration_c, policy_configuration_d) }
end
describe '.for_namespace_and_projects' do
let_it_be(:top_level_group) { create(:group) }
let_it_be(:subgroup_a) { create(:group, parent: top_level_group) }
let_it_be(:subgroup_b) { create(:group, parent: subgroup_a) }
let_it_be(:top_level_group_project) { create(:project, group: top_level_group) }
let_it_be(:subgroup_project) { create(:project, group: subgroup_a) }
let_it_be(:policy_project) { create(:project) }
let!(:policy_configuration_a) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: top_level_group.id)
end
let!(:policy_configuration_b) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: subgroup_b.id,
security_policy_management_project_id: policy_project.id)
end
let!(:policy_configuration_c) do
create(
:security_orchestration_policy_configuration,
project: top_level_group_project,
security_policy_management_project_id: policy_project.id)
end
let!(:policy_configuration_d) do
create(
:security_orchestration_policy_configuration,
project: subgroup_project,
security_policy_management_project_id: policy_project.id)
end
let!(:other_policy_configuration) do
create(
:security_orchestration_policy_configuration,
:namespace,
namespace_id: subgroup_a.id)
end
subject { described_class.for_namespace_and_projects(subgroup_a.self_and_descendant_ids, subgroup_a.all_project_ids) }
it { is_expected.to contain_exactly(policy_configuration_b, policy_configuration_d, other_policy_configuration) }
end
describe '.policy_management_project?' do
before do
create(:security_orchestration_policy_configuration, security_policy_management_project: security_policy_management_project)
end
it 'returns true when security_policy_management_project with id exists' do
expect(described_class.policy_management_project?(security_policy_management_project.id)).to be_truthy
end
it 'returns false when security_policy_management_project with id does not exist' do
expect(described_class.policy_management_project?(non_existing_record_id)).to be_falsey
end
end
describe '.valid_scan_type?' do
it 'returns true when scan type is valid' do
expect(Security::ScanExecutionPolicy.valid_scan_type?('secret_detection')).to be_truthy
end
it 'returns false when scan type is invalid' do
expect(Security::ScanExecutionPolicy.valid_scan_type?('invalid')).to be_falsey
end
end
describe '#policy_configuration_exists?' do
subject { security_orchestration_policy_configuration.policy_configuration_exists? }
context 'when file is missing' do
let(:policy_yaml) { nil }
it { is_expected.to eq(false) }
end
context 'when policy is present' do
it { is_expected.to eq(true) }
end
end
describe '#policy_hash' do
subject { security_orchestration_policy_configuration.policy_hash }
let(:cache_key) do
"security_orchestration_policy_configurations:#{security_orchestration_policy_configuration.id}:policy_yaml"
end
context 'when policy is present' do
it { expect(subject.dig(:scan_execution_policy, 0, :name)).to eq('Run DAST in every pipeline') }
end
context 'when policy has invalid YAML format' do
let(:policy_yaml) do
'cadence: * 1 2 3'
end
it { expect(subject).to be_nil }
end
context 'when policy is nil' do
let(:policy_yaml) { nil }
it { expect(subject).to be_nil }
end
it_behaves_like 'captures git errors', :blob_data_at
context 'with cache enabled' do
it 'fetches from cache' do
expect(Rails.cache).to receive(:fetch).with(cache_key, { expires_in: described_class::CACHE_DURATION }).and_call_original
subject
end
end
end
describe '#invalidate_policy_yaml_cache' do
subject { security_orchestration_policy_configuration.invalidate_policy_yaml_cache }
let(:cache_key) do
"security_orchestration_policy_configurations:#{security_orchestration_policy_configuration.id}:policy_yaml"
end
it 'invalidates cache' do
expect(Rails.cache).to receive(:delete).with(cache_key).and_call_original
subject
end
end
describe '#policy_by_type' do
subject(:policies) { security_orchestration_policy_configuration.policy_by_type(type) }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
context 'when policy is present' do
let(:policy_names) do
{
approval_policy: 'Require approvals for approval policy',
scan_execution_policy: 'Run DAST in every pipeline',
pipeline_execution_policy: 'Run custom pipeline configuration',
pipeline_execution_schedule_policy: 'Run custom pipeline schedule configuration',
vulnerability_management_policy: 'Resolve no longer detected vulnerabilities',
ci_component_publishing_policy: 'Allow publishing from auth sources'
}
end
let(:policy_yaml) do
build(:orchestration_policy_yaml,
policy_names.each_with_object({}) do |(type, name), hash|
hash[type] = [build(type, name: name)]
end
)
end
described_class::AVAILABLE_POLICY_TYPES.each do |policy_type|
context "when type is #{policy_type}" do
context 'when type is a string' do
let(:type) { policy_type.to_s }
it 'retrieves policy by type' do
expect(policies.first[:name]).to eq(policy_names[policy_type])
end
end
context 'when type is a symbol' do
let(:type) { policy_type }
it 'retrieves policy by type' do
expect(policies.first[:name]).to eq(policy_names[policy_type])
end
end
end
end
context 'when type is a symbol for ci_component_publishing_policy' do
let(:type) { :ci_component_publishing_policy }
it 'retrieves policy by type' do
expect(policies.first[:name]).to eq('Allow publishing from auth sources')
end
end
context 'when type is an array' do
let(:type) { %i[pipeline_execution_policy approval_policy] }
it 'retrieves all applicable policies by type' do
expect(policies.size).to eq(2)
expect(policies.pluck(:name))
.to contain_exactly 'Run custom pipeline configuration', 'Require approvals for approval policy'
end
end
end
context 'when type does not match any existing policy' do
let(:type) { :approval_policy }
let(:policy_yaml) do
build(:orchestration_policy_yaml,
scan_execution_policy: [build(:scan_execution_policy, name: 'Run DAST in every pipeline')])
end
it 'returns an empty array' do
expect(policies).to eq([])
end
end
context 'when policy is nil' do
let(:policy_yaml) { nil }
let(:type) { :approval_policy }
shared_examples_for 'returns an empty array' do
it { expect(policies).to eq([]) }
end
context 'when type is a symbol' do
let(:type) { :approval_policy }
it_behaves_like 'returns an empty array'
end
context 'when type is a string' do
let(:type) { 'approval_policy' }
it_behaves_like 'returns an empty array'
end
context 'when type is an array' do
let(:type) { %i[approval_policy scan_execution_policy] }
it_behaves_like 'returns an empty array'
end
end
end
describe '#policy_configuration_valid?' do
subject { security_orchestration_policy_configuration.policy_configuration_valid? }
describe 'metadata' do
context 'when metadata is invalid' do
context 'when metadata is not an object' do
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy:
[build(:scan_execution_policy, metadata: { 'test' => { 'key' => 'value' } })])
end
it { is_expected.to eq(false) }
end
end
context 'when metadata is valid' do
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy:
[build(:scan_execution_policy, metadata: { 'test' => true })])
end
it { is_expected.to eq(true) }
end
end
context 'when file is invalid' do
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy:
[build(:scan_execution_policy, rules: [{ type: 'pipeline', branches: 'production' }])])
end
it { is_expected.to eq(false) }
end
context 'when file has invalid name' do
let(:invalid_name) { 'a' * 256 }
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy:
[build(:scan_execution_policy, name: invalid_name)])
end
it { is_expected.to be false }
end
context 'when file is valid' do
it { is_expected.to eq(true) }
context 'with license_scanning policy' do
let(:policy_yaml) do
build(
:orchestration_policy_yaml,
scan_execution_policy: [],
approval_policy: [build(:approval_policy, :license_finding)]
)
end
it { is_expected.to eq(true) }
end
end
context 'when policy is passed as argument' do
let_it_be(:policy_yaml) { nil }
let_it_be(:policy) { { scan_execution_policy: [build(:scan_execution_policy)] } }
context 'when scan type is secret_detection' do
it 'returns false if extra fields are present' do
invalid_policy = policy.deep_dup
invalid_policy[:scan_execution_policy][0][:actions][0][:scan] = 'secret_detection'
invalid_policy[:scan_execution_policy][0][:actions][0][:variables] = { 'SECRET_DETECTION_HISTORIC_SCAN' => 'false' }
invalid_policy[:scan_execution_policy][0][:actions][0][:tags] = %w[linux]
invalid_policy[:scan_execution_policy][0][:actions][0][:site_profile] = 'Site Profile'
invalid_policy[:scan_execution_policy][0][:actions][0][:scanner_profile] = 'Scanner Profile'
invalid_policy[:scan_execution_policy][0][:actions][0][:scan_settings] = { 'ignore_default_before_after_script' => true }
expect(security_orchestration_policy_configuration.policy_configuration_valid?(invalid_policy)).to be_falsey
end
it 'returns true if extra fields are not present' do
valid_policy = policy.deep_dup
valid_policy[:scan_execution_policy][0][:actions][0] = { scan: 'secret_detection' }
expect(security_orchestration_policy_configuration.policy_configuration_valid?(valid_policy)).to be_truthy
end
end
context 'when scan type is sast' do
it 'returns false if extra fields are present' do
invalid_policy = policy.deep_dup
invalid_policy[:scan_execution_policy][0][:actions][0] = {
scan: 'sast',
variables: { 'SAST_CONFIG_OPTION' => 'false' },
tags: %w[linux],
template: 'latest',
scan_settings: { 'ignore_default_before_after_script' => true },
site_profile: 'Site Profile'
}
expect(security_orchestration_policy_configuration.policy_configuration_valid?(invalid_policy)).to be_falsey
end
it 'returns true if no more fields than allowed max fields are present' do
valid_policy = policy.deep_dup
valid_policy[:scan_execution_policy][0][:actions][0] = {
scan: 'sast',
variables: { 'SAST_CONFIG_OPTION' => 'false' },
tags: %w[linux],
template: 'latest',
scan_settings: { 'ignore_default_before_after_script' => true }
}
expect(security_orchestration_policy_configuration.policy_configuration_valid?(valid_policy)).to be_truthy
end
end
context 'for schedule policy rule' do
using RSpec::Parameterized::TableSyntax
let_it_be(:schedule_policy) { { scan_execution_policy: [build(:scan_execution_policy, :with_schedule)] } }
subject { security_orchestration_policy_configuration.policy_configuration_valid?(schedule_policy) }
where(:cadence, :is_valid) do
"@weekly" | true
"@yearly" | true
"@annually" | true
"@monthly" | true
"@weekly" | true
"@daily" | true
"@midnight" | true
"@noon" | true
"@hourly" | true
"* * * * *" | true
"0 0 2 3 *" | true
"* * L * *" | true
"* * -6 * *" | true
"* * -3 * *" | true
"* * 12 * *" | true
"0 9 -4 * *" | true
"0 0 -8 * *" | true
"7 10 * * *" | true
"00 07 * * *" | true
"* * * * tue" | true
"* * * * TUE" | true
"12 10 0 * *" | true
"52 20 * * 2" | true
"* * last * *" | true
"0 2 last * *" | true
"52 9 2-5 * 2" | true
"0 0 27 3 1,5" | true
"0 0 11 * 3-6" | true
"0 0 -7-L * *" | true
"0 0 -1,-2 * *" | true
"10/30 * * * *" | true
"21 37 4,12 * 3" | true
"02 07 21 jan *" | true
"02 07 21 JAN *" | true
"0 1 L * wed-fri" | true
"0 1 L * wed-FRI" | true
"0 1 L * WED-fri" | true
"0 1 L * WED-FRI" | true
"0 0 21 4 sat,sun" | true
"0 0 21 4 SAT,SUN" | true
"10-30/30 * * * *" | true
"" | false
"1" | false
"2 3 4" | false
"invalid" | false
"@WEEKLY" | false
"@YEARLY" | false
"@ANNUALLY" | false
"@MONTHLY" | false
"@WEEKLY" | false
"@DAILY" | false
"@MIDNIGHT" | false
"@NOON" | false
"@HOURLY" | false
end
with_them do
before do
schedule_policy[:scan_execution_policy][0][:rules][0][:cadence] = cadence
end
it { is_expected.to eq(is_valid) }
end
end
end
context 'with approval policies' do
let(:policy_name) { 'Contains security critical severities' }
let(:approval_policy) { build(:approval_policy, name: policy_name) }
let(:policy_yaml) { build(:orchestration_policy_yaml, approval_policy: [approval_policy]) }
it { is_expected.to eq(true) }
context 'with various approvers' do
using RSpec::Parameterized::TableSyntax
where(:user_approvers, :user_approvers_ids, :group_approvers, :group_approvers_ids, :role_approvers, :is_valid) do
[] | nil | nil | nil | nil | false
['username'] | nil | nil | nil | nil | true
nil | [] | nil | nil | nil | false
nil | [1] | nil | nil | nil | true
nil | nil | [] | nil | nil | false
nil | nil | ['group_path'] | nil | nil | true
nil | nil | nil | [] | nil | false
nil | nil | nil | [2] | nil | true
nil | nil | nil | nil | [] | false
nil | nil | nil | nil | ['developer'] | true
end
with_them do
let(:action) do
{ type: 'require_approval',
approvals_required: 1,
user_approvers: user_approvers,
user_approvers_ids: user_approvers_ids,
group_approvers: group_approvers,
group_approvers_ids: group_approvers_ids,
role_approvers: role_approvers }.compact
end
let(:approval_policy) { build(:approval_policy, name: 'Contains security critical severities', actions: [action]) }
it { is_expected.to eq(is_valid) }
end
end
context 'with various policy names' do
using RSpec::Parameterized::TableSyntax
where(:policy_name, :expected_to_be_valid) do
ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT | false
ApprovalRuleLike::DEFAULT_NAME_FOR_COVERAGE | false
"New #{ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT}" | true
"#{ApprovalRuleLike::DEFAULT_NAME_FOR_COVERAGE} through policies" | true
end
with_them do
it { is_expected.to eq(expected_to_be_valid) }
end
end
end
end
describe '#policy_configuration_validation_errors' do
let(:scan_execution_policy) { nil }
let(:approval_policy) { nil }
let(:pipeline_execution_policy) { nil }
let(:pipeline_execution_schedule_policy) { nil }
let(:experiments) { {} }
let(:policy_yaml) do
{
scan_execution_policy: [scan_execution_policy].compact,
approval_policy: [approval_policy].compact,
pipeline_execution_policy: [pipeline_execution_policy].compact,
pipeline_execution_schedule_policy: [pipeline_execution_schedule_policy].compact,
experiments: experiments
}
end
subject(:errors) do
security_orchestration_policy_configuration.policy_configuration_validation_errors(policy_yaml)
end
context "without policies" do
let(:policy_yaml) { {} }
specify do
expect(errors).to contain_exactly("root is missing required keys: scan_execution_policy",
"root is missing required keys: approval_policy",
"root is missing required keys: pipeline_execution_policy",
"root is missing required keys: ci_component_publishing_policy",
"root is missing required keys: vulnerability_management_policy",
"root is missing required keys: pipeline_execution_schedule_policy")
end
end
shared_examples "branch_exceptions" do
let(:valid_exceptions) do
[
%w[master develop],
[{ name: "master", full_path: "foobar" }],
["master", { name: "develop", full_path: "foobar" }]
]
end
specify do
valid_exceptions.each do |exceptions|
rule[:branch_exceptions] = exceptions
expect(errors).not_to include(match("branch_exceptions"))
end
end
context "with empty branch_exceptions" do
let(:empty_exceptions) do
[[], [""]]
end
specify do
empty_exceptions.each do |exceptions|
rule[:branch_exceptions] = exceptions
expect(errors).to include(match("property '/.*branch_exceptions' is invalid: error_type=minItems"))
end
end
end
context "with repeated items" do
specify do
rule[:branch_exceptions] = %w[master master]
expect(errors).to include(match(%r{property '/.*branch_exceptions' is invalid: error_type=uniqueItems}))
end
end
context "with invalid branch_exceptions" do
let(:invalid_exceptions) { [{}, { name: "master" }, { full_path: "foobar" }] }
specify do
invalid_exceptions.each do |exceptions|
rule[:branch_exceptions] = [exceptions]
expect(errors).to include(match(%r{property '/.*branch_exceptions/0' is missing required keys}))
end
end
end
end
shared_examples "policy_scope" do
context 'with empty object' do
let(:policy_scope) { {} }
specify { expect(errors).to be_empty }
end
context 'with allowed properties' do
let(:policy_scope) do
{
compliance_frameworks: [
{ id: 1 },
{ id: 2 }
],
projects: {
including: [
{ id: 1 }
],
excluding: [
{ id: 2 }
]
}
}
end
specify { expect(errors).to be_empty }
end
context 'with invalid properties' do
let(:policy_scope) do
{
compliance_frameworks: {},
projects: [
{ id: 3 }
]
}
end
specify { expect(errors).not_to be_empty }
end
end
describe 'experiments' do
context 'with empty object' do
let(:experiments) { {} }
specify { expect(errors).to be_empty }
end
context 'with valid experiments configuration' do
let(:experiments) do
{
'test_feature' => {
'enabled' => true,
'configuration' => {
'option1' => 'value1',
'option2' => 42
}
},
'another_feature' => {
'enabled' => false
}
}
end
specify { expect(errors).to be_empty }
end
context 'with invalid feature name format' do
let(:experiments) do
{
'Invalid-Feature' => {
'enabled' => true
}
}
end
specify { expect(errors).not_to be_empty }
end
context 'with missing enabled field' do
let(:experiments) do
{
'test_feature' => {
'configuration' => {
'option1' => 'value1'
}
}
}
end
specify { expect(errors).not_to be_empty }
end
context 'with invalid enabled field type' do
let(:experiments) do
{
'test_feature' => {
'enabled' => 'yes'
}
}
end
specify { expect(errors).not_to be_empty }
end
end
describe "scan execution policies" do
let(:scan_execution_policy) { build(:scan_execution_policy, rules: rules, actions: actions, policy_scope: policy_scope) }
let(:rules) { [rule].compact }
let(:rule) { nil }
let(:actions) { [action].compact }
let(:action) { nil }
let(:policy_scope) { {} }
%i[name enabled rules actions].each do |key|
context "without #{key}" do
before do
scan_execution_policy.delete(key)
end
specify do
expect(errors).to contain_exactly("property '/scan_execution_policy/0' is missing required keys: #{key}")
end
end
end
describe "name" do
context "when too short" do
before do
scan_execution_policy[:name] = ""
end
specify do
expect(errors).to contain_exactly("property '/scan_execution_policy/0/name' is invalid: error_type=minLength")
end
end
context "when too long" do
before do
scan_execution_policy[:name] = "a" * 256
end
specify do
expect(errors).to contain_exactly("property '/scan_execution_policy/0/name' is invalid: error_type=maxLength")
end
end
end
describe "rules" do
context "with invalid type" do
let(:rule) { { type: "foobar" } }
specify do
expect(errors.count).to be(4)
expect(errors.last).to match("property '/scan_execution_policy/0/rules/0/type' is not one of")
end
end
context "with schedule type" do
let(:rule) { { type: "schedule", branches: %w[master], cadence: "5 4 * * *" } }
specify { expect(errors).to be_empty }
context "with invalid cadence" do
before do
rule[:cadence] = "foobar"
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/cadence' does not match pattern")
end
end
context "with time window" do
context "when the distribution and the value are valid" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
distribution: 'random',
value: 3600
}
}
end
specify { expect(errors).to be_empty }
end
context "when the distribution is missing" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
value: 3600
}
}
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/time_window' is missing required keys: distribution")
end
end
context "when the distribution is invalid" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
distribution: 'invalid distribution',
value: 3600
}
}
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/time_window/distribution' is not one of: [\"random\"]")
end
end
context "when the value is missing" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
distribution: 'random'
}
}
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/time_window' is missing required keys: value")
end
end
context "when the value is smaller than the minimum allowed" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
distribution: 'random',
value: 1
}
}
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/time_window/value' is invalid: error_type=minimum")
end
end
context "when the value is greater than the maximum allowed" do
let(:rule) do
{
type: 'schedule',
branches: %w[master],
cadence: '5 4 * * *',
time_window: {
distribution: 'random',
value: 99999
}
}
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/time_window/value' is invalid: error_type=maximum")
end
end
end
end
context "with schedule type and agent" do
let(:rule) { { type: "schedule", agents: { foo: { namespaces: %w[bar] } }, cadence: "5 4 * * *" } }
specify { expect(errors).to be_empty }
context "with invalid agent name" do
before do
rule[:agents][:"with spaces"] = rule[:agents].delete(:foo)
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match(
"property '/scan_execution_policy/0/rules/0/agents/with spaces' is invalid: error_type=schema")
end
end
end
context "with branches" do
let(:rule) { { type: "pipeline", branches: ["master"] } }
specify { expect(errors).to be_empty }
context "with branch_type" do
before do
rule[:branch_type] = "all"
end
specify do
expect(errors).to contain_exactly("property '/scan_execution_policy/0/rules/0' is invalid: error_type=oneOf")
end
end
end
context "with branch_type" do
context 'when defined as protected' do
let(:rule) { { type: "pipeline", branch_type: "protected" } }
specify { expect(errors).to be_empty }
end
context 'when defined as default' do
let(:rule) { { type: "pipeline", branch_type: "default" } }
specify { expect(errors).to be_empty }
end
context 'when defined as target_default' do
let(:rule) { { type: "pipeline", branch_type: "target_default" } }
specify { expect(errors).to be_empty }
end
context 'when defined as target_protected' do
let(:rule) { { type: "pipeline", branch_type: "target_protected" } }
specify { expect(errors).to be_empty }
end
context 'when defined as unsupported' do
let(:rule) { { type: "pipeline", branch_type: "unsupported" } }
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/rules/0/branch_type' is not one of: [\"default\", \"protected\", \"all\", \"target_default\", \"target_protected\"]")
end
end
end
context "with branch_exceptions" do
let(:rule) { {} }
it_behaves_like "branch_exceptions"
end
end
describe "actions" do
let(:action) { { scan: "container_scanning" } }
specify { expect(errors).to be_empty }
context "with invalid scan" do
before do
action[:scan] = "foobar"
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/scan_execution_policy/0/actions/0/scan' is not one of")
end
end
context "with DAST scan" do
let(:action) { { scan: "dast", site_profile: "Site Profile", scanner_profile: "Scanner Profile" } }
specify { expect(errors).to be_empty }
context "without site profile" do
before do
action.delete(:site_profile)
end
specify do
expect(errors).to contain_exactly(
"property '/scan_execution_policy/0/actions/0' is missing required keys: site_profile")
end
end
context "without scanner profile" do
before do
action.delete(:scanner_profile)
end
specify { expect(errors).to be_empty }
end
end
context "with variables" do
let(:action) { { scan: "container_scanning", variables: { "FOO" => "BAR" } } }
specify { expect(errors).to be_empty }
context "with invalid key" do
before do
action[:variables]["with spaces"] = action[:variables].delete("FOO")
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match(
"property '/scan_execution_policy/0/actions/0/variables/with spaces' is invalid: error_type=schema")
end
end
end
context "with template" do
let(:action) { { scan: "container_scanning", template: "latest" } }
specify { expect(errors).to be_empty }
context "with invalid value" do
before do
action[:template] = 'regular'
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match(
"property '/scan_execution_policy/0/actions/0/template' is not one of: [\"default\", \"latest\"]")
end
end
end
end
it_behaves_like "policy_scope"
describe 'skip_ci' do
context 'when skip_ci is not provided' do
before do
scan_execution_policy.delete(:skip_ci)
end
it 'is valid' do
expect(errors).to be_empty
end
end
context 'when skip_ci is allowed' do
before do
scan_execution_policy[:skip_ci] = { allowed: true }
end
it 'is valid' do
expect(errors).to be_empty
end
context 'and has allowlist provided' do
before do
scan_execution_policy[:skip_ci] = { allowed: true, allowlist: { users: [{ id: 123 }] } }
end
it 'is valid' do
expect(errors).to be_empty
end
end
end
context 'when skip_ci is disallowed' do
before do
scan_execution_policy[:skip_ci] = { allowed: false }
end
it 'is valid' do
expect(errors).to be_empty
end
context 'and has allowlist provided' do
before do
scan_execution_policy[:skip_ci] = { allowed: false, allowlist: { users: [{ id: 123 }] } }
end
it 'is valid' do
expect(errors).to be_empty
end
end
end
context 'when skip_ci is nil' do
before do
scan_execution_policy[:skip_ci] = nil
end
it 'returns errors' do
expect(errors).to contain_exactly(
"property '/scan_execution_policy/0/skip_ci' is not of type: object"
)
end
end
context 'when skip_ci is empty' do
before do
scan_execution_policy[:skip_ci] = {}
end
it 'returns errors' do
expect(errors).to contain_exactly(
"property '/scan_execution_policy/0/skip_ci' is missing required keys: allowed"
)
end
end
context 'when skip_ci is invalid' do
context 'when allowed is in wrong format' do
before do
scan_execution_policy[:skip_ci] = { allowed: 'invalid' }
end
it 'returns errors' do
expect(errors).to contain_exactly(
"property '/scan_execution_policy/0/skip_ci/allowed' is not of type: boolean"
)
end
end
context 'when users id is in wrong format' do
before do
scan_execution_policy[:skip_ci] = { allowed: false, allowlist: { users: [{ id: 'invalid' }] } }
end
it 'returns errors' do
expect(errors).to contain_exactly(
"property '/scan_execution_policy/0/skip_ci/allowlist/users/0/id' is not of type: integer"
)
end
end
end
end
end
describe "approval policies" do
let(:scan_execution_policy) { nil }
let(:approval_policy) { build(:approval_policy, rules: rules, actions: actions, policy_scope: policy_scope) }
let(:rules) { [rule].compact }
let(:actions) { [action].compact }
let(:action) { nil }
let(:policy_scope) { {} }
let(:policy_yaml) do
{
approval_policy: [approval_policy].compact
}
end
shared_examples "approval policy" do |required_rule_keys|
%i[name enabled rules].each do |key|
context "without #{key}" do
before do
approval_policy.delete(key)
end
specify do
expect(errors).to include("property '/approval_policy/0' is missing required keys: #{key}")
end
end
end
required_rule_keys.each do |key|
context "without #{key}" do
before do
rule.delete(key)
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0' is missing required keys: #{key}")
end
end
end
describe "name" do
context "when too short" do
before do
approval_policy[:name] = ""
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/name' is invalid: error_type=minLength")
end
end
context "when too long" do
before do
approval_policy[:name] = "a" * 256
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/name' is invalid: error_type=maxLength")
end
end
end
describe "rules" do
context "with invalid type" do
before do
rule[:type] = "foobar"
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/approval_policy/0/rules/0/type' is not one of")
end
end
end
it_behaves_like "policy_scope"
describe "approval_settings" do
let(:approval_policy) do
build(:approval_policy, rules: rules, actions: actions, approval_settings: approval_settings)
end
context 'with empty object' do
let(:approval_settings) { {} }
specify { expect(errors).to be_empty }
end
context 'with allowed properties' do
let(:approval_settings) do
{
prevent_approval_by_author: true,
prevent_approval_by_commit_author: false,
remove_approvals_with_new_commit: true,
require_password_to_approve: false,
block_branch_modification: true,
prevent_pushing_and_force_pushing: true,
block_group_branch_modification: true
}
end
specify { expect(errors).to be_empty }
end
context 'with additional property' do
let(:approval_settings) { { additional_key: 'allowed' } }
specify do
expect(errors).to be_empty
end
end
describe "block_group_branch_modification" do
context "in object form" do
let(:approval_settings) { { enabled: true } }
specify do
expect(errors).to be_empty
end
context "with exceptions" do
let(:approval_settings) { { enabled: true, exceptions: %w[foobar] } }
specify do
expect(errors).to be_empty
end
end
end
end
end
describe "actions" do
let(:approvals_required) { 1 }
let(:require_approval_action) do
{
type: "require_approval",
approvals_required: approvals_required
}
end
describe 'require_approval' do
let(:action) { require_approval_action }
context "with invalid required approvals" do
let(:approvals_required) { 101 }
specify do
expect(errors).to include(
"property '/approval_policy/0/actions/0/approvals_required' is invalid: error_type=maximum")
end
end
context "without approvers" do
specify do
expect(errors).not_to be_empty
end
end
context "with user_approvers" do
before do
action[:user_approvers] = %w[foobar]
end
specify { expect(errors).to be_empty }
context "when empty" do
before do
action[:user_approvers] = []
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/actions/0/user_approvers' is invalid: error_type=minItems")
end
end
end
context "with user_approvers_ids" do
before do
action[:user_approvers_ids] = [42]
end
specify { expect(errors).to be_empty }
context "when empty" do
before do
action[:user_approvers_ids] = []
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/actions/0/user_approvers_ids' is invalid: error_type=minItems")
end
end
end
context "with group_approvers" do
before do
action[:group_approvers] = %w[foobar]
end
specify { expect(errors).to be_empty }
context "when empty" do
before do
action[:group_approvers] = []
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/actions/0/group_approvers' is invalid: error_type=minItems")
end
end
end
context "with group_approvers_ids" do
before do
action[:group_approvers_ids] = [42]
end
specify { expect(errors).to be_empty }
context "when empty" do
before do
action[:group_approvers_ids] = []
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/actions/0/group_approvers_ids' is invalid: error_type=minItems")
end
end
end
context "with role_approvers" do
before do
action[:role_approvers] = ['guest', 'reporter', 123]
end
specify do
expect(errors).to be_empty
end
context "with invalid role" do
before do
action[:role_approvers] = %w[foobar]
end
specify do
expect(errors.count).to be(2)
expect(errors.first).to match("property '/approval_policy/0/actions/0/role_approvers/0' is not one of")
expect(errors.last).to match("property '/approval_policy/0/actions/0/role_approvers/0' is not of type: integer")
end
end
end
it_behaves_like "branch_exceptions"
end
describe 'send_bot_message' do
let(:actions) do
[
require_approval_action.tap { |action| action[:user_approvers] = %w[foobar] },
action
]
end
let(:action) do
{
type: "send_bot_message",
enabled: true
}
end
it { expect(errors).to be_empty }
context 'when `enabled` property is missing' do
before do
action.delete(:enabled)
end
it { expect(errors).to be_present }
it { expect(errors.first).to match("property '/approval_policy/0/actions/1' is missing required keys: enabled") }
end
end
end
context "without actions or approval_settings" do
before do
approval_policy.delete(:actions)
approval_policy.delete(:approval_settings)
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0' is missing required keys: actions",
"property '/approval_policy/0' is missing required keys: approval_settings")
end
end
context "with approval_settings" do
let(:approval_settings) do
{
prevent_approval_by_author: true,
prevent_approval_by_commit_author: true,
remove_approvals_with_new_commit: true,
require_password_to_approve: false
}
end
specify { expect(errors).to be_empty }
context "without actions" do
before do
approval_policy.delete(:actions)
end
specify { expect(errors).to be_empty }
end
end
context "with actions" do
let(:action) do
{
type: "require_approval",
approvals_required: 1,
user_approvers_ids: [42]
}
end
specify { expect(errors).to be_empty }
context "without approval_settings" do
before do
approval_policy.delete(:approval_settings)
end
specify { expect(errors).to be_empty }
end
end
end
shared_examples 'rule has branches or branch_type' do
context "with branches" do
before do
rule[:branches] = %w[master]
rule.delete(:branch_type)
end
specify { expect(errors).to be_empty }
context "with branch_type" do
before do
rule[:branch_type] = "protected"
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0' is invalid: error_type=oneOf")
end
end
end
context "with branch_type" do
before do
rule.delete(:branches)
rule[:branch_type] = "protected"
end
specify { expect(errors).to be_empty }
context "with branches" do
before do
rule[:branches] = %w[main]
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0' is invalid: error_type=oneOf")
end
end
end
context "without branches and branch_type" do
before do
rule.delete(:branches)
rule.delete(:branch_type)
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0' is missing required keys: branch_type",
"property '/approval_policy/0/rules/0' is missing required keys: branches")
end
end
end
context "with scan_finding type" do
let(:rule) do
{
type: "scan_finding",
branches: %w[master],
scanners: %w[container_scanning secret_detection],
vulnerabilities_allowed: 0,
severity_levels: %w[critical high],
vulnerability_states: %w[detected]
}
end
specify { expect(errors).to be_empty }
it_behaves_like "approval policy",
%i[scanners vulnerabilities_allowed severity_levels vulnerability_states]
it_behaves_like 'rule has branches or branch_type'
describe "scanners" do
before do
rule[:scanners] = [""]
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/scanners/0' is invalid: error_type=minLength")
end
end
describe "severity_levels" do
before do
rule[:severity_levels] = %w[foobar]
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match("property '/approval_policy/0/rules/0/severity_levels/0' is not one of")
end
end
describe "vulnerability_states" do
before do
rule[:vulnerability_states] = %w[foobar]
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match(
"property '/approval_policy/0/rules/0/vulnerability_states/0' is not one of")
end
end
describe "vulnerabilities_allowed" do
context "when value is below the minimum" do
before do
rule[:vulnerabilities_allowed] = -1
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/vulnerabilities_allowed' is invalid: error_type=minimum")
end
end
context "when value is above the maximum" do
before do
rule[:vulnerabilities_allowed] = 32768
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/vulnerabilities_allowed' is invalid: error_type=maximum")
end
end
end
describe "vulnerability_age" do
before do
rule[:vulnerability_age] = vulnerability_age
end
let(:valid_vulnerability_age) do
{ value: 1, operator: 'greater_than', interval: 'week' }
end
context 'when vulnerability_age is valid' do
let(:vulnerability_age) { valid_vulnerability_age }
specify do
expect(errors).to be_none
end
end
%i[value operator interval].each do |key|
context "when vulnerability_age is missing key #{key}" do
let(:vulnerability_age) { valid_vulnerability_age.except(key) }
specify do
expect(errors.count).to eq(1)
expect(errors.first).to(
match "property '/approval_policy/0/rules/0/vulnerability_age' is missing required keys: #{key}"
)
end
end
end
context "when vulnerability_age contains additional key" do
let(:vulnerability_age) { valid_vulnerability_age.merge(additional: true) }
specify do
expect(errors.count).to eq(1)
expect(errors.first).to(
match "property '/approval_policy/0/rules/0/vulnerability_age/additional' is invalid"
)
end
end
end
end
context "with license_finding type" do
let(:rule) do
{
type: "license_finding",
branches: %w[master],
match_on_inclusion_license: true,
license_types: %w[BSD MIT],
license_states: %w[newly_detected detected]
}
end
specify { expect(errors).to be_empty }
it_behaves_like 'rule has branches or branch_type'
context "without match_on_inclusion_license" do
before do
rule.delete(:match_on_inclusion_license)
end
specify do
expect(errors).to include(
"property '/approval_policy/0/rules/0' is missing required keys: match_on_inclusion_license"
)
end
end
describe "license_types" do
before do
rule[:license_types] = [""]
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/license_types/0' is invalid: error_type=minLength")
end
context "when too long" do
before do
rule[:license_types] = ["a" * 256]
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/license_types/0' is invalid: error_type=maxLength")
end
end
context "with repeated licenses" do
before do
rule[:license_types] = ["a"] * 2
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/license_types' is invalid: error_type=uniqueItems")
end
end
context "with too many licenses" do
before do
licenses = []
1001.times { |i| licenses << "License #{i}" }
rule[:license_types] = licenses
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/license_types' is invalid: error_type=maxItems")
end
end
end
describe "license_states" do
context "without states" do
before do
rule[:license_states] = []
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/license_states' is invalid: error_type=minItems")
end
end
context "with invalid state" do
before do
rule[:license_states] = %w[foobar]
end
specify do
expect(errors.count).to be(1)
expect(errors.first).to match(
"property '/approval_policy/0/rules/0/license_states/0' is not one of")
end
end
end
describe "licenses" do
let(:rule) do
{
type: "license_finding",
branches: %w[master],
license_states: %w[newly_detected detected]
}
end
shared_examples_for "licenses with package exclusions" do
context "without the name key" do
let(:license) { {} }
before do
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0' is missing required keys: name")
end
end
context "with the name key" do
let(:license) { { name: "License" } }
context "when the license name is too long" do
let(:license) do
{ name: "a" * 256 }
end
before do
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/name' is invalid: error_type=maxLength")
end
end
context "when the license name is too short" do
let(:license) do
{ name: "" }
end
before do
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/name' is invalid: error_type=minLength")
end
end
context "when the license list has too many items" do
before do
licenses = []
1001.times { |i| licenses << { name: "License #{i}" } }
rule[:licenses] = { license_list_type.to_sym => licenses }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}' is invalid: error_type=maxItems")
end
end
context "when the license list has duplicated items" do
before do
licenses = [{ name: "License" }, { name: "License" }]
rule[:licenses] = { license_list_type.to_sym => licenses }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}' is invalid: error_type=uniqueItems")
end
end
context "with license_types and match_on_inclusion_license" do
let(:license) do
{ name: "MIT License" }
end
before do
rule[:licenses] = { license_list_type.to_sym => [license] }
rule[:match_on_inclusion_license] = true
rule[:license_types] = %w[BSD MIT]
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0' is invalid: error_type=oneOf")
end
end
context "when the packages key does not contains the excluding key" do
before do
license[:packages] = {}
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages' is missing required keys: excluding")
end
end
context "when the packages key contains the excluding key" do
context "when the excluding key does not contains the purls key" do
before do
license[:packages] = { excluding: {} }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding' is missing required keys: purls")
end
end
context "when the excluding key contains the purls key" do
context "when the purls list is empty" do
before do
license[:packages] = { excluding: { purls: [] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls' is invalid: error_type=minItems")
end
end
context "when the purls list has too many items" do
before do
purls = []
1001.times { |i| purls << "pkg:gem/bundler@#{i}" }
license[:packages] = { excluding: { purls: purls } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls' is invalid: error_type=maxItems")
end
end
context "when the purl is not a string" do
before do
license[:packages] = { excluding: { purls: [1] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls/0' is not of type: string")
end
end
context "when the purl is a string" do
context "when the purl is a valid uri without package version" do
before do
license[:packages] = { excluding: { purls: ["pkg:gem/bundler"] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to be_empty
end
end
context "when the purl is a valid uri with package version" do
before do
license[:packages] = { excluding: { purls: ["pkg:gem/bundler@1.0.0"] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to be_empty
end
end
context "when excluding key contains additional keys" do
before do
license[:packages] = { excluding: { purls: ["pkg:gem/bundler@1.0.0"], additional_key: true } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/additional_key' is invalid: error_type=schema")
end
end
context "when the purl is too short" do
before do
license[:packages] = { excluding: { purls: [""] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls/0' is invalid: error_type=minLength",
"property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls/0' does not match format: uri")
end
end
context "when the purl is too long" do
before do
license[:packages] = { excluding: { purls: ["pkg:gem/bundler@#{'0' * 1025}"] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls/0' is invalid: error_type=maxLength")
end
end
context "when the purl is not a valid uri" do
before do
license[:packages] = { excluding: { purls: ["abc"] } }
rule[:licenses] = { license_list_type.to_sym => [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses/#{license_list_type}/0/packages/excluding/purls/0' does not match format: uri")
end
end
end
end
end
context "with additional key" do
before do
rule[:licenses] = { license_list_type.to_sym => [license], additional_key: true }
end
specify do
expect(errors.count).to eq(1)
expect(errors.first).to(match "property '/approval_policy/0/rules/0/licenses/additional_key' is invalid")
end
end
end
end
context "with allowed licenses" do
let(:license_list_type) { "allowed" }
it_behaves_like "licenses with package exclusions"
end
context "with denied licenses" do
let(:license_list_type) { "denied" }
it_behaves_like "licenses with package exclusions"
end
context "with allowed and denied licenses" do
let(:license) do
{ name: "MIT License" }
end
before do
rule[:licenses] = { allowed: [license], denied: [license] }
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses' is invalid: error_type=oneOf")
end
end
context "without allowed and denied licenses" do
before do
rule[:licenses] = {}
end
specify do
expect(errors).to contain_exactly("property '/approval_policy/0/rules/0/licenses' is missing required keys: allowed",
"property '/approval_policy/0/rules/0/licenses' is missing required keys: denied")
end
end
end
end
context 'with any_merge_request type' do
let(:rule) do
{
type: 'any_merge_request',
branches: %w[master],
commits: 'any'
}
end
specify { expect(errors).to be_empty }
it_behaves_like 'approval policy', %i[commits]
it_behaves_like 'rule has branches or branch_type'
describe 'commits' do
before do
rule[:commits] = 'invalid'
end
specify do
expect(errors).to contain_exactly(
"property '/approval_policy/0/rules/0/commits' is not one of: [\"any\", \"unsigned\"]")
end
end
end
end
shared_examples_for "pipeline_execution_policy_content" do |policy_type|
context 'without content' do
let(:content) { {} }
it do
expect(errors).to contain_exactly(
"property '/#{policy_type}/0/content' is missing required keys: include"
)
end
end
context 'when include is missing required properties' do
let(:content) { { include: [{}] } }
it do
expect(errors).to contain_exactly(
"property '/#{policy_type}/0/content/include/0' is missing required keys: project, file"
)
end
end
context 'when include is an empty array' do
let(:content) { { include: [] } }
it do
expect(errors).to contain_exactly(
"property '/#{policy_type}/0/content/include' is invalid: error_type=minItems"
)
end
end
context 'when include is contains more than 1 item' do
let(:content) do
{
include: [
{ project: '', file: '' }, { project: '', file: '' }
]
}
end
it do
expect(errors).to contain_exactly(
"property '/#{policy_type}/0/content/include' is invalid: error_type=maxItems"
)
end
end
end
describe "pipeline execution policies" do
let(:pipeline_execution_policy) { build(:pipeline_execution_policy, policy_scope: policy_scope) }
let(:policy_scope) { {} }
it { expect(errors).to be_empty }
it_behaves_like "policy_scope"
it_behaves_like "pipeline_execution_policy_content", 'pipeline_execution_policy' do
let(:pipeline_execution_policy) { build(:pipeline_execution_policy, content: content) }
end
describe 'max items' do
let(:policy_yaml) do
{
pipeline_execution_policy: pipeline_execution_policies
}
end
context 'when policies are at the limit' do
let(:pipeline_execution_policies) do
build_list(:pipeline_execution_policy, 5)
end
it { expect(errors).to be_empty }
end
end
describe 'skip_ci' do
let(:policy) { { pipeline_execution_policy: [build(:pipeline_execution_policy)] } }
context 'when skip_ci is not provided' do
it 'does not return any errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0].delete(:skip_ci)
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to be_empty
end
end
context 'when skip_ci is allowed' do
it 'does not return any errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: true }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to be_empty
end
context 'and has allowlist provided' do
it 'does not return any errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: true, allowlist: { users: [{ id: 123 }] } }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to be_empty
end
end
end
context 'when skip_ci is disallowed' do
it 'does not return any errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: false }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to be_empty
end
context 'and has allowlist provided' do
it 'does not return any errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: false, allowlist: { users: [{ id: 123 }] } }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to be_empty
end
end
end
context 'when skip_ci is nil' do
it 'returns errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = nil
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to contain_exactly(
"property '/pipeline_execution_policy/0/skip_ci' is not of type: object"
)
end
end
context 'when skip_ci is empty' do
it 'returns errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = {}
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to contain_exactly(
"property '/pipeline_execution_policy/0/skip_ci' is missing required keys: allowed"
)
end
end
context 'when skip_ci is invalid' do
context 'when allowlist is in wrong format' do
it 'returns errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: 'invalid' }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to contain_exactly(
"property '/pipeline_execution_policy/0/skip_ci/allowed' is not of type: boolean"
)
end
end
context 'when users id is in wrong format' do
it 'returns errors' do
valid_policy = policy.deep_dup
valid_policy[:pipeline_execution_policy][0][:skip_ci] = { allowed: false, allowlist: { users: [{ id: '123' }] } }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to contain_exactly(
"property '/pipeline_execution_policy/0/skip_ci/allowlist/users/0/id' is not of type: integer"
)
end
end
end
end
end
describe "pipeline execution schedule policies" do
let(:pipeline_execution_schedule_policy) { build(:pipeline_execution_schedule_policy, policy_scope: policy_scope) }
let(:policy_scope) { {} }
it { expect(errors).to be_empty }
it_behaves_like "policy_scope"
it_behaves_like "pipeline_execution_policy_content", 'pipeline_execution_schedule_policy' do
let(:pipeline_execution_schedule_policy) { build(:pipeline_execution_schedule_policy, content: content) }
end
describe "schedules" do
context "when empty" do
before do
pipeline_execution_schedule_policy[:schedules] = []
end
specify do
expect(errors).to contain_exactly("property '/pipeline_execution_schedule_policy/0/schedules' is invalid: error_type=minItems")
end
end
context "when exceeding 1" do
before do
pipeline_execution_schedule_policy[:schedules] *= 2
end
specify do
expect(errors).to contain_exactly(
"property '/pipeline_execution_schedule_policy/0/schedules' is invalid: error_type=maxItems",
"property '/pipeline_execution_schedule_policy/0/schedules' is invalid: error_type=uniqueItems")
end
end
end
end
context 'when file is valid' do
it { is_expected.to eq([]) }
end
context 'when policy is passed as argument' do
let_it_be(:policy_yaml) { nil }
let_it_be(:policy) { { scan_execution_policy: [build(:scan_execution_policy, :with_schedule)] } }
context 'when scan type is secret_detection' do
it 'returns false if extra fields are present' do
invalid_policy = policy.deep_dup
invalid_policy[:scan_execution_policy][0][:actions][0][:scan] = 'secret_detection'
invalid_policy[:scan_execution_policy][0][:actions][0][:variables] = { 'SECRET_DETECTION_HISTORIC_SCAN' => 'false' }
invalid_policy[:scan_execution_policy][0][:actions][0][:template] = 'default'
invalid_policy[:scan_execution_policy][0][:actions][0][:scanner_profile] = 'Scanner Profile'
invalid_policy[:scan_execution_policy][0][:actions][0][:site_profile] = 'Site Profile'
invalid_policy[:scan_execution_policy][0][:actions][0][:scan_settings] = { 'ignore_default_before_after_script' => true }
invalid_policy[:scan_execution_policy][0][:rules][0][:cadence] = 'invalid * * * *'
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(invalid_policy)).to contain_exactly(
"property '/scan_execution_policy/0/actions/0' is invalid: error_type=maxProperties",
"property '/scan_execution_policy/0/rules/0/cadence' does not match pattern: (@(yearly|annually|monthly|weekly|daily|midnight|noon|hourly))|(((\\*|(\\-?\\d+\\,?)+)(\\/\\d+)?|last|L|(sun|mon|tue|wed|thu|fri|sat|SUN|MON|TUE|WED|THU|FRI|SAT\\-|\\,)+|(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|\\-|\\,)+)\\s?){5,6}"
)
end
it 'returns true if extra fields are not present' do
valid_policy = policy.deep_dup
valid_policy[:scan_execution_policy][0][:actions][0] = { scan: 'secret_detection' }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to eq([])
end
end
end
end
describe '#active_scan_execution_policies' do
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
let(:expected_active_policies) do
[
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_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_v5', rules: [{ type: 'pipeline', branches: %w[master] }])
]
end
subject(:active_scan_execution_policies) { security_orchestration_policy_configuration.active_scan_execution_policies }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_scan_execution_policies).to eq(expected_active_policies)
end
end
describe '#active_scan_execution_policies_for_pipelines' do
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [policy_pipeline_1, policy_pipeline_2, policy_schedule]) }
let(:policy_pipeline_1) { build(:scan_execution_policy, name: 'Run DAST in every pipeline', rules: [{ type: 'pipeline', branches: %w[production] }]) }
let(:policy_pipeline_2) { build(:scan_execution_policy, name: 'Run DAST in every pipeline_v1', rules: [{ type: 'pipeline', branches: %w[master] }]) }
let(:policy_schedule) { build(:scan_execution_policy, name: 'Run DAST every 20 mins', rules: [{ type: 'schedule', branches: %w[production], cadence: '*/20 * * * *' }]) }
let(:expected_active_scan_execution_policies_for_pipelines) { [policy_pipeline_1, policy_pipeline_2] }
subject(:active_scan_execution_policies_for_pipelines) { security_orchestration_policy_configuration.active_scan_execution_policies_for_pipelines }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only active scan execution policies for pipelines' do
expect(active_scan_execution_policies_for_pipelines).to eq(expected_active_scan_execution_policies_for_pipelines)
end
end
describe '#active_policy_names_with_dast_site_profile' do
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy: [
build(
:scan_execution_policy,
name: 'Run DAST in every pipeline',
actions: [
{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' },
{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile 2' }
])
])
end
it 'returns list of policy names where site profile is referenced' do
expect(security_orchestration_policy_configuration.active_policy_names_with_dast_site_profile('Site Profile')).to contain_exactly('Run DAST in every pipeline')
end
end
describe '#active_policy_names_with_dast_scanner_profile' do
let(:enforce_dast_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy: [
build(
:scan_execution_policy,
name: 'Run DAST in every pipeline',
actions: [
{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' },
{ scan: 'dast', site_profile: 'Site Profile 2', scanner_profile: 'Scanner Profile' }
])
])
end
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(enforce_dast_yaml)
end
it 'returns list of policy names where site profile is referenced' do
expect(security_orchestration_policy_configuration.active_policy_names_with_dast_scanner_profile('Scanner Profile')).to contain_exactly('Run DAST in every pipeline')
end
end
describe '#policy_last_updated_by' do
let(:merged_merge_request) do
create(:merge_request, :merged, author: security_policy_management_project.first_owner)
end
subject(:policy_last_updated_by) { security_orchestration_policy_configuration.policy_last_updated_by }
before do
allow(security_policy_management_project).to receive(:merge_requests).and_return(MergeRequest.where(id: merged_merge_request&.id))
end
context 'when last merged merge request to policy file exists' do
it { is_expected.to eq(security_policy_management_project.first_owner) }
end
context 'when last merge request to policy file does not exist' do
let(:merged_merge_request) {}
it { is_expected.to be_nil }
end
end
describe '#policy_last_updated_at' do
let(:last_commit_updated_at) { Time.zone.now }
let(:commit) { create(:commit) }
subject(:policy_last_updated_at) { security_orchestration_policy_configuration.policy_last_updated_at }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:last_commit_for_path).and_return(commit)
end
context 'when last commit to policy file exists' do
it "returns commit's updated date" do
commit.committed_date = last_commit_updated_at
is_expected.to eq(policy_last_updated_at)
end
end
context 'when last commit to policy file does not exist' do
let(:commit) {}
it { is_expected.to be_nil }
end
it_behaves_like 'captures git errors', :last_commit_for_path
end
describe '#delete_all_schedules' do
let(:rule_schedule) { create(:security_orchestration_policy_rule_schedule, security_orchestration_policy_configuration: security_orchestration_policy_configuration) }
subject(:delete_all_schedules) { security_orchestration_policy_configuration.delete_all_schedules }
it 'deletes all schedules belonging to configuration' do
delete_all_schedules
expect(security_orchestration_policy_configuration.rule_schedules).to be_empty
end
end
describe '#active_scan_result_policies' do
let(:scan_result_yaml) { build(:orchestration_policy_yaml, approval_policy: [build(:approval_policy)]) }
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:active_scan_result_policies) { security_orchestration_policy_configuration.active_scan_result_policies }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_scan_result_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_scan_result_policies.count).to be(5)
end
context 'when policy configuration is configured for namespace' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace, security_policy_management_project: security_policy_management_project)
end
it 'returns only enabled policies' do
expect(active_scan_result_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_scan_result_policies.count).to be(5)
end
end
end
describe '#applicable_scan_result_policies_with_real_index' do
let_it_be(:project) { create(:project) }
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
let(:policy_scope_checker) { instance_double(Security::SecurityOrchestrationPolicies::PolicyScopeChecker) }
before do
allow(Security::SecurityOrchestrationPolicies::PolicyScopeChecker).to receive(:new).with(project: project).and_return(policy_scope_checker)
allow(policy_configuration).to receive(:approval_policies_limit).and_return(3)
end
context 'when there are no policies' do
before do
allow(policy_configuration).to receive(:scan_result_policies).and_return([])
end
it 'does not yield any policies' do
expect { |b| policy_configuration.applicable_scan_result_policies_with_real_index(project, &b) }.not_to yield_control
end
end
context 'when there are policies' do
let(:policies) do
[
{ enabled: true, name: 'Policy 1' },
{ enabled: false, name: 'Policy 2' },
{ enabled: true, name: 'Policy 3' },
{ enabled: true, name: 'Policy 4' },
{ enabled: true, name: 'Policy 5' }
]
end
before do
allow(policy_configuration).to receive(:scan_result_policies).and_return(policies)
allow(policy_scope_checker).to receive(:policy_applicable?).and_return(true)
end
it 'yields applicable policies with correct indices' do
expect { |b| policy_configuration.applicable_scan_result_policies_with_real_index(project, &b) }.to yield_successive_args(
[{ enabled: true, name: 'Policy 1' }, 0, 0],
[{ enabled: true, name: 'Policy 3' }, 2, 1],
[{ enabled: true, name: 'Policy 4' }, 3, 2]
)
end
it 'respects the approval_policies_limit' do
expect { |b| policy_configuration.applicable_scan_result_policies_with_real_index(project, &b) }.to yield_control.exactly(3).times
end
context 'when a policy is not applicable' do
before do
allow(policy_scope_checker).to receive(:policy_applicable?).with(policies[0]).and_return(false)
end
it 'skips non-applicable policies' do
expect { |b| policy_configuration.applicable_scan_result_policies_with_real_index(project, &b) }.to yield_successive_args(
[{ enabled: true, name: 'Policy 3' }, 2, 0],
[{ enabled: true, name: 'Policy 4' }, 3, 1]
)
end
end
end
end
describe '#applicable_scan_result_policies_for_project' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:policy_yaml) do
build(:orchestration_policy_yaml, approval_policy: [
build(:approval_policy, name: 'Active policy'),
build(:approval_policy, name: 'Disabled policy', enabled: false),
build(:approval_policy, name: 'Not applicable policy', policy_scope: {
projects: {
excluding: [{ id: project.id }]
}
})
])
end
subject(:applicable_policies) do
security_orchestration_policy_configuration.applicable_scan_result_policies_for_project(project)
end
it 'returns only active applicable policies' do
expect(applicable_policies).to be_one
expect(applicable_policies.first[:name]).to eq('Active policy')
end
end
describe '#scan_result_policies' do
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:scan_result_policies) { security_orchestration_policy_configuration.scan_result_policies }
it 'returns all scan result policies' do
expect(scan_result_policies.pluck(:enabled)).to contain_exactly(true, true, false, true, true, true, true, true)
end
end
describe '#project?' do
subject { security_orchestration_policy_configuration.project? }
context 'when project is assigned to policy configuration' do
it { is_expected.to eq true }
end
context 'when namespace is assigned to policy configuration' do
let(:security_orchestration_policy_configuration) { create(:security_orchestration_policy_configuration, :namespace) }
it { is_expected.to eq false }
end
end
describe '#namespace?' do
subject { security_orchestration_policy_configuration.namespace? }
context 'when project is assigned to policy configuration' do
it { is_expected.to eq false }
end
context 'when namespace is assigned to policy configuration' do
let(:security_orchestration_policy_configuration) { create(:security_orchestration_policy_configuration, :namespace) }
it { is_expected.to eq true }
end
end
describe '#source' do
subject { security_orchestration_policy_configuration.source }
context 'when project is assigned to policy configuration' do
it { is_expected.to eq security_orchestration_policy_configuration.project }
end
context 'when namespace is assigned to policy configuration' do
let(:security_orchestration_policy_configuration) { create(:security_orchestration_policy_configuration, :namespace) }
it { is_expected.to eq security_orchestration_policy_configuration.namespace }
end
end
describe '#compliance_framework_ids_with_policy_index' do
subject { security_orchestration_policy_configuration.compliance_framework_ids_with_policy_index }
context 'for project level configuration' do
it { is_expected.to eq([]) }
end
context 'for group level configuration' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration,
security_policy_management_project: security_policy_management_project,
namespace: create(:group),
project: nil
)
end
context 'without compliance framework ids' do
it { is_expected.to eq([]) }
end
context 'with compliance framework ids' do
let(:policy_yaml) do
build(:orchestration_policy_yaml,
scan_execution_policy: [build(:scan_execution_policy, policy_scope: { compliance_frameworks: [{ id: 2 }, { id: 3 }] })],
approval_policy: [build(:approval_policy, policy_scope: { compliance_frameworks: [{ id: 1 }, { id: 2 }] })],
pipeline_execution_policy: [build(:pipeline_execution_policy, policy_scope: { compliance_frameworks: [{ id: 1 }, { id: 3 }] })],
vulnerability_management_policy: [build(:vulnerability_management_policy, policy_scope: { compliance_frameworks: [{ id: 2 }, { id: 3 }] })]
)
end
it { is_expected.to match_array([{ framework_ids: [1, 2], policy_index: 0 }, { framework_ids: [2, 3], policy_index: 1 }, { framework_ids: [1, 3], policy_index: 2 }, { framework_ids: [2, 3], policy_index: 3 }]) }
end
end
end
describe 'all_policies_with_type' do
subject(:policies) { security_orchestration_policy_configuration.all_policies_with_type }
context 'with all policy types' do
let(:policy_yaml) do
build(:orchestration_policy_yaml,
scan_execution_policy: [build(:scan_execution_policy)],
approval_policy: [build(:approval_policy)],
pipeline_execution_policy: [build(:pipeline_execution_policy)],
vulnerability_management_policy: [build(:vulnerability_management_policy)],
pipeline_execution_schedule_policy: [build(:pipeline_execution_schedule_policy)]
)
end
it 'has the correct type for each policy' do
policies.each do |policy|
expect(policy[:type]).to be_present
expect(policy[:type]).to be_a(String)
end
end
end
end
describe '#delete_scan_finding_rules' do
subject(:delete_scan_finding_rules) { security_orchestration_policy_configuration.send(:delete_scan_finding_rules) }
let(:project) { security_orchestration_policy_configuration.project }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:security_orchestration_policy_configuration_id) { security_orchestration_policy_configuration.id }
before do
create(:approval_project_rule,
:scan_finding,
project: project,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
create(:report_approver_rule,
:scan_finding,
merge_request: merge_request,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
shared_examples 'approval rules deletion' do
it 'deletes project approval rules' do
expect { delete_scan_finding_rules }.to change(ApprovalProjectRule, :count).from(1).to(0)
end
it 'deletes merge request approval rules' do
expect { delete_scan_finding_rules }.to change(ApprovalMergeRequestRule, :count).from(1).to(0)
end
it_behaves_like 'does not deletes merge request approval rules of merged MR'
end
context 'when associated to a project' do
it_behaves_like 'approval rules deletion'
end
context 'when associated to namespace' do
let(:project) { create(:project) }
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace)
end
it_behaves_like 'approval rules deletion'
end
end
describe '#delete_scan_finding_rules_for_project' do
subject(:delete_scan_finding_rules_for_project) { security_orchestration_policy_configuration.delete_scan_finding_rules_for_project(project.id) }
let(:project) { security_orchestration_policy_configuration.project }
let(:security_orchestration_policy_configuration_id) { security_orchestration_policy_configuration.id }
before do
create(:approval_project_rule,
:scan_finding,
project: project,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
it 'deletes project approval rules' do
expect { delete_scan_finding_rules_for_project }.to change(ApprovalProjectRule, :count).from(1).to(0)
end
context 'with unrelated resources' do
let_it_be(:unrelated_project) { create(:project) }
before do
create(:approval_project_rule,
:scan_finding,
project: unrelated_project,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
it 'does not delete unrelated project approval rules' do
expect { delete_scan_finding_rules_for_project }.to change(ApprovalProjectRule, :count).from(2).to(1)
end
end
end
describe '#delete_merge_request_rules_for_project' do
subject(:delete_merge_request_rules_for_project) { security_orchestration_policy_configuration.delete_merge_request_rules_for_project(project.id) }
let(:project) { security_orchestration_policy_configuration.project }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:security_orchestration_policy_configuration_id) { security_orchestration_policy_configuration.id }
before do
create(:report_approver_rule,
:scan_finding,
merge_request: merge_request,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
it 'deletes merge request approval rules' do
expect { delete_merge_request_rules_for_project }.to change(ApprovalMergeRequestRule, :count).from(1).to(0)
end
context 'with unrelated resources' do
let_it_be(:unrelated_project) { create(:project) }
let(:unrelated_mr) { create(:merge_request, target_project: unrelated_project, source_project: unrelated_project) }
before do
create(:report_approver_rule,
:scan_finding,
merge_request: unrelated_mr,
security_orchestration_policy_configuration_id: security_orchestration_policy_configuration_id)
end
it 'does not delete unrelated merge request approval rules' do
expect { delete_merge_request_rules_for_project }.to change(ApprovalMergeRequestRule, :count).from(2).to(1)
end
it_behaves_like 'does not deletes merge request approval rules of merged MR'
end
end
describe '#delete_software_license_policies' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration) }
let_it_be(:other_read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: other_configuration) }
let_it_be(:policy) { create(:software_license_policy, scan_result_policy_read: read) }
let_it_be(:other_policy) { create(:software_license_policy, scan_result_policy_read: other_read) }
subject(:delete) { configuration.send(:delete_software_license_policies) }
it "deletes software license policies" do
expect { delete }.to change { SoftwareLicensePolicy.exists?(policy.id) }.to(false)
end
it "does not delete other software license policies" do
expect { delete }.not_to change { SoftwareLicensePolicy.exists?(other_policy.id) }.from(true)
end
end
describe '#delete_software_license_policies_for_project' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace) }
let_it_be(:other_project) { create(:project, namespace: namespace) }
let_it_be(:configuration) { create(:security_orchestration_policy_configuration, namespace: namespace, project: nil) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration, project: other_project) }
let_it_be(:scan_result_policy_read) do
create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration, project: project)
end
let_it_be(:scan_result_policy_read_other_project) do
create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration, project: other_project)
end
let_it_be(:scan_result_policy_read_other_configuration) do
create(:scan_result_policy_read, security_orchestration_policy_configuration: other_configuration, project: other_project)
end
let!(:software_license_without_scan_result_policy) do
create(:software_license_policy, project: project)
end
let!(:software_license_with_scan_result_policy) do
create(:software_license_policy, project: project,
scan_result_policy_read: scan_result_policy_read)
end
let!(:software_license_with_scan_result_policy_other_configuration) do
create(:software_license_policy, project: other_project,
scan_result_policy_read: scan_result_policy_read_other_configuration)
end
let!(:software_license_with_scan_result_policy_other_project) do
create(:software_license_policy, project: other_project,
scan_result_policy_read: scan_result_policy_read_other_project)
end
subject(:delete) { configuration.send(:delete_software_license_policies_for_project, project) }
it 'deletes project scan_result_policy_reads' do
delete
software_license_policies = SoftwareLicensePolicy.where(project_id: project.id)
other_project_software_license_policies = SoftwareLicensePolicy.where(project_id: other_project.id)
expect(software_license_policies).to match_array([software_license_without_scan_result_policy])
expect(other_project_software_license_policies).to match_array([software_license_with_scan_result_policy_other_configuration, software_license_with_scan_result_policy_other_project])
end
end
describe '#delete_policy_violations' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration) }
let_it_be(:other_read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: other_configuration) }
let_it_be(:violation) { create(:scan_result_policy_violation, scan_result_policy_read: read) }
let_it_be(:other_violation) { create(:scan_result_policy_violation, scan_result_policy_read: other_read) }
subject(:delete) { configuration.send(:delete_policy_violations) }
it "deletes configuration's scan result policy violations" do
expect { delete }.to change { Security::ScanResultPolicyViolation.exists?(violation.id) }.to(false)
end
it "does not delete other scan result policy violations" do
expect { delete }.not_to change { Security::ScanResultPolicyViolation.exists?(other_violation.id) }.from(true)
end
end
describe '#delete_policy_violations_for_project' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:inherited_configuration) { create(:security_orchestration_policy_configuration, namespace: configuration.project.group) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:project) { configuration.project }
let_it_be(:other_project) { other_configuration.project }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let_it_be(:scan_result_policy_read) do
create(
:scan_result_policy_read,
security_orchestration_policy_configuration: configuration,
project: project)
end
let_it_be(:inherited_scan_result_policy_read) do
create(
:scan_result_policy_read,
security_orchestration_policy_configuration: inherited_configuration,
project: project)
end
let_it_be(:other_scan_result_policy_read) do
create(
:scan_result_policy_read,
security_orchestration_policy_configuration: other_configuration,
project: other_project)
end
let_it_be(:violation) do
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: scan_result_policy_read)
end
let_it_be(:inherited_violation) do
create(
:scan_result_policy_violation,
project: project,
merge_request: merge_request,
scan_result_policy_read: inherited_scan_result_policy_read)
end
let_it_be(:other_violation) do
create(
:scan_result_policy_violation,
project: other_project,
merge_request: other_merge_request,
scan_result_policy_read: other_scan_result_policy_read)
end
it 'deletes scan_result_policy_violations related to the project and configuration' do
configuration.delete_policy_violations_for_project(project)
project_violations = project.scan_result_policy_violations.where(scan_result_policy_id: scan_result_policy_read.id)
inherited_violations = project.scan_result_policy_violations.where(scan_result_policy_id: inherited_scan_result_policy_read.id)
expect(project_violations.count).to be(0)
expect(inherited_violations.count).to be(1)
expect(other_project.scan_result_policy_violations.count).to be(1)
end
it 'changes policy violation count only for the configuration' do
expect { configuration.delete_policy_violations_for_project(project) }.to change { project.scan_result_policy_violations.count }.by(-1)
end
end
describe '#delete_scan_result_policy_reads' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration) }
let_it_be(:other_read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: other_configuration) }
subject(:delete) { configuration.delete_scan_result_policy_reads }
it "deletes scan_result_policy_reads" do
expect { delete }.to change { Security::ScanResultPolicyRead.exists?(read.id) }.to(false)
end
it "does not delete other scan_result_policy_reads" do
expect { delete }.not_to change { Security::ScanResultPolicyRead.exists?(other_read.id) }.from(true)
end
end
describe '#delete_scan_result_policy_reads_for_project' do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:other_configuration) { create(:security_orchestration_policy_configuration) }
let!(:read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration, project: project) }
let_it_be(:other_read) { create(:scan_result_policy_read, security_orchestration_policy_configuration: configuration, project: other_project) }
subject(:delete) { configuration.delete_scan_result_policy_reads_for_project(project) }
it "deletes a project's scan_result_policy_reads" do
expect { delete }.to change { project.scan_result_policy_reads.count }.by(-1)
end
it "does not delete other projects' scan_result_policy_reads" do
expect { delete }.not_to change { other_project.scan_result_policy_reads.count }
end
context "when scan_result_policy_read belongs to other configuration" do
let!(:read) do
create(:scan_result_policy_read, security_orchestration_policy_configuration: other_configuration, project: project)
end
it "does not delete it" do
expect { delete }.not_to change { project.scan_result_policy_reads.count }
end
end
end
shared_context 'for policies with pipeline and scheduled rules' do
before do
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy: scan_execution_policies, approval_policy: approval_policies)
end
let(:scan_execution_policies) { [dast_policy, container_scanning_policy, sast_policy_with_schedule] }
let(:dast_policy) do
build(:scan_execution_policy,
actions: [{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' }])
end
let(:container_scanning_policy) { build(:scan_execution_policy, actions: [{ scan: 'container_scanning' }]) }
let(:sast_policy_with_schedule) { build(:scan_execution_policy, :with_schedule, actions: [{ scan: 'sast' }]) }
let(:approval_policies) { [build(:approval_policy)] }
let_it_be(:project) { create(:project, :repository) }
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, project: project,
security_policy_management_project: security_policy_management_project)
end
end
describe "#active_policies_scan_actions_for_project" do
include_context 'for policies with pipeline and scheduled rules'
subject(:active_scan_actions) { security_orchestration_policy_configuration.active_policies_scan_actions_for_project('refs/heads/master', project) }
context "with matched branches" do
it "returns active scan policies" do
expect(active_scan_actions).to contain_exactly(
*dast_policy[:actions],
*container_scanning_policy[:actions],
*sast_policy_with_schedule[:actions]
)
end
end
context 'with policy scope' do
let(:policy_applicable) { true }
before do
allow_next_instance_of(Security::SecurityOrchestrationPolicies::PolicyBranchesService) do |service|
allow(service).to receive(:scan_execution_branches).and_return(Set[default_branch])
end
allow_next_instance_of(Security::SecurityOrchestrationPolicies::PolicyScopeChecker) do |service|
allow(service).to receive(:policy_applicable?).and_return(policy_applicable)
end
end
it 'returns active scan policies' do
expect(active_scan_actions)
.to contain_exactly(
*dast_policy[:actions],
*container_scanning_policy[:actions],
*sast_policy_with_schedule[:actions]
)
end
context 'when policy is not applicable' do
let(:policy_applicable) { false }
it 'is empty' do
expect(active_scan_actions).to be_empty
end
end
end
context "with disabled scan policies" do
let(:container_scanning_policy) do
build(:scan_execution_policy, actions: [{ scan: 'container_scanning' }], enabled: false)
end
it "filters" do
expect(active_scan_actions).to contain_exactly(*dast_policy[:actions], *sast_policy_with_schedule[:actions])
end
end
context "with scan policies targeting other branch" do
let(:container_scanning_policy) do
build(
:scan_execution_policy,
actions: [{ scan: 'container_scanning' }],
rules: [{ type: 'pipeline', branches: [default_branch.reverse] }]
)
end
it "filters" do
expect(active_scan_actions).to contain_exactly(*dast_policy[:actions], *sast_policy_with_schedule[:actions])
end
end
end
describe '#active_policies_for_project' do
include_context 'for policies with pipeline and scheduled rules'
context 'when pipeline source is not provided' do
subject(:active_policies) { security_orchestration_policy_configuration.active_policies_for_project('refs/heads/master', project) }
it 'includes pipeline and scheduled policies' do
expect(active_policies).to contain_exactly(dast_policy, sast_policy_with_schedule, container_scanning_policy)
end
end
context 'when pipeline source is provided' do
subject(:active_policies) { security_orchestration_policy_configuration.active_policies_for_project('refs/heads/master', project, pipeline_source) }
context 'with scan policies without specifying pipeline source' do
let(:pipeline_source) { 'push' }
it 'includes pipeline and scheduled policies' do
expect(active_policies).to contain_exactly(dast_policy, sast_policy_with_schedule, container_scanning_policy)
end
end
context 'with scan policies targetting specific pipeline source' do
let(:container_scanning_policy) do
build(
:scan_execution_policy,
actions: [{ scan: 'container_scanning' }],
rules: [{ type: 'pipeline', branches: [default_branch], pipeline_sources: { including: ['api'] } }]
)
end
context 'when pipeline source matches source defined in the policy' do
let(:pipeline_source) { 'api' }
it 'includes policies without specified pipeline source and matching one' do
expect(active_policies).to contain_exactly(dast_policy, sast_policy_with_schedule, container_scanning_policy)
end
end
context 'when pipeline source does not match source defined in the policy' do
let(:pipeline_source) { 'web' }
it 'includes only pipelines without defined sources' do
expect(active_policies).to contain_exactly(dast_policy, sast_policy_with_schedule)
end
end
end
end
end
describe 'active_pipeline_policies_for_project' do
include_context 'for policies with pipeline and scheduled rules'
context 'without pipeline source provided' do
subject(:active_scan_policies) { security_orchestration_policy_configuration.active_pipeline_policies_for_project('refs/heads/master', project) }
it 'invokes active_policies_scan_actions_for_project' do
expect(security_orchestration_policy_configuration).to receive(:active_policies_for_project).with('refs/heads/master', project, nil).and_call_original
active_scan_policies
end
it 'excludes the scheduled policies' do
expect(active_scan_policies).to contain_exactly(dast_policy, container_scanning_policy)
end
end
context 'with pipeline source provided' do
subject(:active_scan_policies) { security_orchestration_policy_configuration.active_pipeline_policies_for_project('refs/heads/master', project, 'push') }
it 'invokes active_policies_scan_actions_for_project' do
expect(security_orchestration_policy_configuration).to receive(:active_policies_for_project).with('refs/heads/master', project, 'push').and_call_original
active_scan_policies
end
it 'excludes the scheduled policies' do
expect(active_scan_policies).to contain_exactly(dast_policy, container_scanning_policy)
end
end
end
describe '#enabled_experiments' do
before do
security_orchestration_policy_configuration.experiments = experiments
end
context 'when experiments field is empty' do
let(:experiments) { {} }
it { expect(security_orchestration_policy_configuration.enabled_experiments).to be_empty }
end
context 'when experiments field is nil' do
let(:experiments) { nil }
it { expect(security_orchestration_policy_configuration.enabled_experiments).to be_empty }
end
context 'when feature is disabled' do
let(:experiments) { { 'test_feature' => { 'enabled' => false } } }
it { expect(security_orchestration_policy_configuration.enabled_experiments).to be_empty }
end
context 'when feature is enabled' do
let(:experiments) { { 'test_feature' => { 'enabled' => true } } }
it { expect(security_orchestration_policy_configuration.enabled_experiments).to match_array(['test_feature']) }
end
end
describe '#experiment_enabled?' do
let(:name_of_the_feature) { 'test_feature' }
before do
security_orchestration_policy_configuration.experiments = experiments
end
context 'when experiments field is empty' do
let(:experiments) { {} }
it { expect(security_orchestration_policy_configuration.experiment_enabled?(name_of_the_feature)).to be_falsey }
end
context 'when experiments field is nil' do
let(:experiments) { nil }
it { expect(security_orchestration_policy_configuration.experiment_enabled?(name_of_the_feature)).to be_falsey }
end
context 'when feature is not present in experiments' do
let(:experiments) { { 'other_feature' => { 'enabled' => true } } }
it { expect(security_orchestration_policy_configuration.experiment_enabled?(name_of_the_feature)).to be_falsey }
end
context 'when feature is disabled' do
let(:experiments) { { 'test_feature' => { 'enabled' => false } } }
it { expect(security_orchestration_policy_configuration.experiment_enabled?(name_of_the_feature)).to be_falsey }
end
context 'when feature is enabled' do
let(:experiments) { { 'test_feature' => { 'enabled' => true } } }
it { expect(security_orchestration_policy_configuration.experiment_enabled?(name_of_the_feature)).to be_truthy }
end
end
describe '#experiment_configuration' do
let(:name_of_the_feature) { 'test_feature' }
before do
security_orchestration_policy_configuration.experiments = experiments
end
context 'when experiments field is empty' do
let(:experiments) { {} }
it { expect(security_orchestration_policy_configuration.experiment_configuration(name_of_the_feature)).to eq({}) }
end
context 'when experiments field is nil' do
let(:experiments) { nil }
it { expect(security_orchestration_policy_configuration.experiment_configuration(name_of_the_feature)).to eq({}) }
end
context 'when feature is not present in experiments' do
let(:experiments) { { 'other_feature' => { 'configuration' => { 'option' => 'value' } } } }
it { expect(security_orchestration_policy_configuration.experiment_configuration(name_of_the_feature)).to eq({}) }
end
context 'when feature has no configuration' do
let(:experiments) { { 'test_feature' => { 'enabled' => true } } }
it { expect(security_orchestration_policy_configuration.experiment_configuration(name_of_the_feature)).to eq({}) }
end
context 'when feature has configuration' do
let(:configuration) { { 'option' => 'value', 'another_option' => 123 } }
let(:experiments) { { 'test_feature' => { 'enabled' => true, 'configuration' => configuration } } }
it { expect(security_orchestration_policy_configuration.experiment_configuration(name_of_the_feature)).to eq(configuration) }
end
end
describe '#active_pipeline_execution_policies' do
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:active_pipeline_execution_policies) { security_orchestration_policy_configuration.active_pipeline_execution_policies }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_pipeline_execution_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_pipeline_execution_policies.count).to be(5)
end
it 'uses limits defined based on the project' do
expect(Security::SecurityOrchestrationPolicies::LimitService).to receive(:new).with(container: security_orchestration_policy_configuration.project).and_call_original
active_pipeline_execution_policies
end
context 'when policy configuration is configured for namespace' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace, security_policy_management_project: security_policy_management_project)
end
it 'returns only enabled policies' do
expect(active_pipeline_execution_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_pipeline_execution_policies.count).to be(5)
end
it 'uses limits defined based on the namespace' do
expect(Security::SecurityOrchestrationPolicies::LimitService).to receive(:new).with(container: security_orchestration_policy_configuration.namespace).and_call_original
active_pipeline_execution_policies
end
describe 'limits' do
let(:namespace) { security_orchestration_policy_configuration.namespace }
it 'uses limits defined based on the namespace' do
expect(Security::SecurityOrchestrationPolicies::LimitService).to receive(:new).with(container: namespace).and_call_original
active_pipeline_execution_policies
end
context 'when the limit is defined in the namespace settings' do
let(:setting) { build(:namespace_settings, pipeline_execution_policies_per_configuration_limit: 1) }
before do
namespace.update!(namespace_settings: setting)
end
it 'returns only 1 active policy' do
expect(active_pipeline_execution_policies.count).to be(1)
end
end
end
end
end
describe '#active_pipeline_execution_schedule_policies' do
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:active_pipeline_execution_schedule_policies) { security_orchestration_policy_configuration.active_pipeline_execution_schedule_policies }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_pipeline_execution_schedule_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 1 from all active policies' do
expect(active_pipeline_execution_schedule_policies.count).to be(1)
end
context 'when policy configuration is configured for namespace' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace, security_policy_management_project: security_policy_management_project)
end
it 'returns only enabled policies' do
expect(active_pipeline_execution_schedule_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 1 from all active policies' do
expect(active_pipeline_execution_schedule_policies.count).to be(1)
end
end
end
describe '#active_ci_component_publishing_policies' do
let(:ci_component_publishing_yaml) do
build(:orchestration_policy_yaml, ci_component_publishing_policy: [build(:ci_component_publishing_policy)])
end
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:active_ci_component_publishing_policies) do
security_orchestration_policy_configuration.active_ci_component_publishing_policies
end
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_ci_component_publishing_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only the limit (5) from all active policies' do
expect(active_ci_component_publishing_policies.count).to be(5)
end
context 'when policy configuration is configured for namespace' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace, security_policy_management_project: security_policy_management_project)
end
it 'returns only enabled policies' do
expect(active_ci_component_publishing_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_ci_component_publishing_policies.count).to be(5)
end
end
end
describe '#active_vulnerability_management_policies' do
let(:vulnerability_management_yaml) do
build(:orchestration_policy_yaml, vulnerability_management_policy: [build(:vulnerability_management_policy)])
end
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
subject(:active_vulnerability_management_policies) do
security_orchestration_policy_configuration.active_vulnerability_management_policies
end
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:blob_data_at).with(default_branch, Security::OrchestrationPolicyConfiguration::POLICY_PATH).and_return(policy_yaml)
end
it 'returns only enabled policies' do
expect(active_vulnerability_management_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only the limit (5) from all active policies' do
expect(active_vulnerability_management_policies.count).to be(5)
end
context 'when policy configuration is configured for namespace' do
let(:security_orchestration_policy_configuration) do
create(:security_orchestration_policy_configuration, :namespace, security_policy_management_project: security_policy_management_project)
end
it 'returns only enabled policies' do
expect(active_vulnerability_management_policies.pluck(:enabled).uniq).to contain_exactly(true)
end
it 'returns only 5 from all active policies' do
expect(active_vulnerability_management_policies.count).to be(5)
end
end
end
describe '#policy_changes' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
let_it_be(:db_policy1) do
create(:security_policy,
security_orchestration_policy_configuration: configuration,
name: 'Policy 1', checksum: 'abc123', policy_index: 0
)
end
let_it_be(:db_policy2) do
create(:security_policy,
security_orchestration_policy_configuration: configuration,
name: 'Policy 2', checksum: 'def456', policy_index: 1
)
end
let_it_be(:db_policy3) do
create(:security_policy,
security_orchestration_policy_configuration: configuration,
name: 'Policy 3', checksum: 'ghi789', policy_index: 2
)
end
let(:yaml_policy1) { { name: 'Policy 1', rules: ['Rule 1'] } }
let(:yaml_policy2) { { name: 'Policy 2', rules: ['Rule 2'] } }
let(:yaml_policy3) { { name: 'Policy 3', rules: ['Rule 3 Updated'] } }
let(:yaml_policy4) { { name: 'Policy 4', rules: ['Rule 4'] } }
let(:db_policies) { [] }
let(:yaml_policies) { [] }
let(:policy_changes) { configuration.policy_changes(db_policies, yaml_policies) }
before do
allow(Security::Policy).to receive(:checksum).and_return('abc123', 'def456', 'xyz789', 'jkl012')
end
context 'when new policies are introduced' do
let(:db_policies) { [db_policy1, db_policy2] }
let(:yaml_policies) { [yaml_policy1, yaml_policy2, yaml_policy4] }
it 'identifies new policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to match_array([[yaml_policy4, 2]])
expect(deleted_policies).to be_empty
expect(changed_policies).to be_empty
expect(rearranged_policies).to be_empty
end
end
context 'when policies are deleted' do
let(:db_policies) { [db_policy1, db_policy2, db_policy3] }
let(:yaml_policies) { [yaml_policy1, yaml_policy2] }
it 'identifies deleted policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to be_empty
expect(deleted_policies).to match_array([db_policy3])
expect(changed_policies).to be_empty
expect(rearranged_policies).to be_empty
end
end
context 'when policies are updated' do
let(:db_policies) { [db_policy1, db_policy2, db_policy3] }
let(:yaml_policies) { [yaml_policy1, yaml_policy2, yaml_policy3] }
it 'identifies changed policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to be_empty
expect(deleted_policies).to be_empty
expect(changed_policies.size).to eq(1)
expect(changed_policies.first).to be_a(Security::SecurityOrchestrationPolicies::PolicyComparer)
expect(changed_policies.first.db_policy).to eq(db_policy3)
expect(changed_policies.first.yaml_policy).to eq(yaml_policy3)
expect(changed_policies.first.policy_index).to eq(2)
expect(rearranged_policies).to be_empty
end
end
context 'when policies are rearranged' do
let(:db_policies) { [db_policy1, db_policy2] }
let(:yaml_policies) { [yaml_policy2, yaml_policy1] }
before do
allow(Security::Policy).to receive(:checksum).and_return('def456', 'abc123', 'xyz789', 'jkl012')
end
it 'identifies rearranged policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to be_empty
expect(deleted_policies).to be_empty
expect(changed_policies).to be_empty
expect(rearranged_policies).to match_array([[db_policy2, 0], [db_policy1, 1]])
end
end
context 'when db policies are empty' do
let(:db_policies) { [] }
let(:yaml_policies) { [yaml_policy1, yaml_policy2] }
it 'handles empty db_policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to match_array([[yaml_policy1, 0], [yaml_policy2, 1]])
expect(deleted_policies).to be_empty
expect(changed_policies).to be_empty
expect(rearranged_policies).to be_empty
end
end
context 'when yaml_policies are empty' do
let(:db_policies) { [db_policy1, db_policy2] }
let(:yaml_policies) { [] }
it 'handles empty yaml_policies', :aggregate_failures do
new_policies, deleted_policies, changed_policies, rearranged_policies = policy_changes
expect(new_policies).to be_empty
expect(deleted_policies).to match_array([db_policy1, db_policy2])
expect(changed_policies).to be_empty
expect(rearranged_policies).to be_empty
end
end
end
describe '#policies_changed?' do
let_it_be(:configuration) { create(:security_orchestration_policy_configuration) }
subject(:policies_changed?) { configuration.policies_changed? }
context 'when approval policies have changed' do
before do
create(:security_policy, :require_approval, security_orchestration_policy_configuration: configuration)
end
it { is_expected.to be_truthy }
end
context 'when scan execution policies have changed' do
before do
create(:security_policy, :scan_execution_policy, security_orchestration_policy_configuration: configuration)
end
it { is_expected.to be_truthy }
end
context 'when pipeline execution policy has changed' do
before do
create(:security_policy, :pipeline_execution_policy, security_orchestration_policy_configuration: configuration)
end
it { is_expected.to be_truthy }
end
context 'when pipeline execution schedule policy has changed' do
before do
create(:security_policy, :pipeline_execution_schedule_policy, security_orchestration_policy_configuration: configuration)
end
it { is_expected.to be_truthy }
end
context 'when vulnerability management policy has changed' do
before do
create(:security_policy,
:vulnerability_management_policy,
security_orchestration_policy_configuration: configuration)
end
it { is_expected.to be_truthy }
end
context 'when no policies have changed' do
it { is_expected.to be_falsey }
end
end
describe '#all_project_ids' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project) }
context 'when configuration is at namespace-level' do
let_it_be(:configuration) do
create(:security_orchestration_policy_configuration, namespace: namespace, project: nil)
end
let_it_be(:project1) { create(:project, namespace: namespace) }
let_it_be(:project2) { create(:project, namespace: namespace) }
let_it_be(:project3) { create(:project, namespace: namespace) }
it 'returns all project IDs under the namespace' do
expect(configuration.all_project_ids).to match_array([project1.id, project2.id, project3.id])
end
it 'uses batch processing' do
expect(Gitlab::Database::NamespaceEachBatch)
.to receive(:new)
.with(namespace_class: Namespace, cursor: { current_id: namespace.id, depth: [namespace.id] })
.and_call_original
configuration.all_project_ids
end
end
context 'when configuration is at project-level' do
let_it_be(:configuration) do
create(:security_orchestration_policy_configuration, project: project, namespace: nil)
end
it 'returns single project id' do
expect(configuration.all_project_ids).to contain_exactly(project.id)
end
end
end
describe '#self_and_ancestor_configuration_ids' do
subject(:self_and_ancestor_configuration_ids) { configuration.self_and_ancestor_configuration_ids }
let_it_be(:top_level_group) { create(:group) }
let_it_be(:direct_subgroup) { create(:group, parent: top_level_group) }
let_it_be(:nested_subgroup) { create(:group, parent: direct_subgroup) }
let_it_be(:direct_subgroup_project) { create(:project, group: direct_subgroup) }
let_it_be(:configuration_top_level_group) do
create(:security_orchestration_policy_configuration, :namespace, namespace: top_level_group,
security_policy_management_project: security_policy_management_project)
end
let_it_be(:configuration_direct_subgroup) do
create(:security_orchestration_policy_configuration, :namespace, namespace: direct_subgroup,
security_policy_management_project: security_policy_management_project)
end
let_it_be(:configuration_nested_subgroup) do
create(:security_orchestration_policy_configuration, :namespace, namespace: nested_subgroup,
security_policy_management_project: security_policy_management_project)
end
let_it_be(:configuration_project) do
create(:security_orchestration_policy_configuration, project: direct_subgroup_project,
security_policy_management_project: security_policy_management_project)
end
context 'with project configuration' do
let(:configuration) { configuration_project }
it 'returns project and ancestor configuration ids and excludes nested_subgroup configuration' do
expect(self_and_ancestor_configuration_ids)
.to contain_exactly(configuration_project.id, configuration_direct_subgroup.id, configuration_top_level_group.id)
end
end
context 'with nested_subgroup configuration' do
let(:configuration) { configuration_nested_subgroup }
it 'returns nested_subgroup, direct_subgroup and top-level group configuration ids' do
expect(self_and_ancestor_configuration_ids)
.to contain_exactly(configuration_nested_subgroup.id, configuration_direct_subgroup.id,
configuration_top_level_group.id)
end
end
context 'with direct_subgroup configuration' do
let(:configuration) { configuration_direct_subgroup }
it 'returns direct_subgroup and top-level group configuration ids' do
expect(self_and_ancestor_configuration_ids)
.to contain_exactly(configuration_direct_subgroup.id, configuration_top_level_group.id)
end
end
context 'with top-level group configuration' do
let(:configuration) { configuration_top_level_group }
it 'returns top-level group configuration id' do
expect(self_and_ancestor_configuration_ids).to contain_exactly(configuration_top_level_group.id)
end
end
end
end