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