diff --git a/ee/spec/features/merge_requests/policy_dismissal_audit_integration_spec.rb b/ee/spec/features/merge_requests/policy_dismissal_audit_integration_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..91f0b1bfd99b0f30e0f72bf0fc49bf84d1d0ea22 --- /dev/null +++ b/ee/spec/features/merge_requests/policy_dismissal_audit_integration_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Policy dismissal audit event integration', :js, feature_category: :security_policy_management do + include_context 'with security orchestration policy configuration' + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:policy_management_project) { create(:project, :repository) } + let_it_be(:policy_configuration) do + create(:security_orchestration_policy_configuration, + project: project, + security_policy_management_project: policy_management_project) + end + + let(:approval_policy) do + { + name: 'MR - Security Scan', + description: 'Security Scan', + enabled: true, + enforcement_type: 'warn', + rules: [ + { + type: 'scan_finding', + scanners: ['secret_detection'], + vulnerabilities_allowed: 0, + severity_levels: [], + vulnerability_states: [], + branch_type: 'protected' + } + ], + actions: [ + { + type: 'require_approval', + approvals_required: 1, + role_approvers: ['developer', 'maintainer', 'owner'] + }, + { + type: 'send_bot_message', + enabled: true + } + ], + approval_settings: { + block_branch_modification: false, + prevent_pushing_and_force_pushing: false, + prevent_approval_by_author: false, + prevent_approval_by_commit_author: false, + remove_approvals_with_new_commit: false, + require_password_to_approve: false + }, + fallback_behavior: { + fail: 'open' + } + } + end + + let(:policy_yaml) do + { + approval_policy: [approval_policy] + }.to_yaml + end + + before_all do + project.add_maintainer(user) + policy_management_project.add_maintainer(user) + end + + before do + stub_licensed_features(security_orchestration_policies: true) + stub_feature_flags(security_policy_approval_warn_mode: true) + sign_in(user) + + # Create the security policy + create_policy_commit(policy_yaml) + end + + context 'when merge request has dismissed security policy violations' do + let!(:merge_request) { create_test_merge_request } + let!(:security_policy) { create_security_policy } + let!(:policy_dismissal) { create_policy_dismissal(merge_request, security_policy) } + + it 'creates audit event when MR is merged with dismissed policy' do + # Verify initial state + expect(merge_request.policy_dismissals).to include(policy_dismissal) + expect(policy_dismissal.status).to eq('open') + expect(AuditEvent.where(entity: project)).to be_empty + + # Merge the MR + merge_request.mark_as_mergeable + MergeRequests::MergeService.new(project: project, current_user: user).execute(merge_request) + + # Wait for background jobs to complete + perform_enqueued_jobs + + # Verify policy dismissal was preserved + expect(policy_dismissal.reload.status).to eq('preserved') + + # Verify audit event was created + audit_events = AuditEvent.where(entity: project) + expect(audit_events.count).to eq(1) + + audit_event = audit_events.first + expect(audit_event.details[:custom_message]).to include( + "Merge request !#{merge_request.iid} was merged with violated security policy." + ) + expect(audit_event.author).to eq(policy_dismissal.user) + expect(audit_event.target_type).to eq('Security::Policy') + expect(audit_event.target_id).to eq(security_policy.id) + end + + context 'when policy dismissal is not applicable for all violations' do + let!(:additional_violation) { create_additional_violation(merge_request, security_policy) } + + it 'destroys non-applicable dismissal and does not create audit event' do + # Make dismissal non-applicable by not covering all violation UUIDs + policy_dismissal.update!(security_findings_uuids: ['partial-uuid']) + + expect(policy_dismissal.applicable_for_all_violations?).to be_falsey + + # Merge the MR + merge_request.mark_as_mergeable + MergeRequests::MergeService.new(project: project, current_user: user).execute(merge_request) + + # Wait for background jobs to complete + perform_enqueued_jobs + + # Verify dismissal was destroyed + expect { policy_dismissal.reload }.to raise_error(ActiveRecord::RecordNotFound) + + # Verify no audit event was created + expect(AuditEvent.where(entity: project)).to be_empty + end + end + end + + context 'when merge request has no policy dismissals' do + let!(:merge_request) { create_test_merge_request } + + it 'does not create audit events' do + # Merge the MR + merge_request.mark_as_mergeable + MergeRequests::MergeService.new(project: project, current_user: user).execute(merge_request) + + # Wait for background jobs to complete + perform_enqueued_jobs + + # Verify no audit event was created + expect(AuditEvent.where(entity: project)).to be_empty + end + end + + private + + def create_policy_commit(policy_content) + policy_management_project.repository.create_file( + user, + '.gitlab/security-policies/policy.yml', + policy_content, + message: 'Add security policy', + branch_name: policy_management_project.default_branch + ) + end + + def create_test_merge_request + # Create a simple test MR without actual vulnerabilities + # The policy violations will be mocked in the test setup + branch_name = 'test-branch' + project.repository.create_branch(branch_name, project.default_branch) + + # Add a simple test file + project.repository.create_file( + user, + 'test_file.txt', + "This is a test file for policy dismissal integration test\n", + message: 'Add test file', + branch_name: branch_name + ) + + create(:merge_request, + source_project: project, + target_project: project, + source_branch: branch_name, + target_branch: project.default_branch, + author: user, + title: 'Test MR for policy dismissal') + end + + def create_security_policy + create(:security_policy, + security_orchestration_policy_configuration: policy_configuration, + name: 'MR - Security Scan', + policy_index: 0) + end + + def create_policy_dismissal(merge_request, security_policy) + create(:policy_dismissal, + project: project, + merge_request: merge_request, + security_policy: security_policy, + user: user, + security_findings_uuids: ['test-uuid-1', 'test-uuid-2'], + dismissal_types: [Security::PolicyDismissal::DISMISSAL_TYPES[:policy_false_positive]], + comment: 'Dismissed due to false positive') + end + + def create_additional_violation(merge_request, security_policy) + approval_policy_rule = create(:approval_policy_rule, security_policy: security_policy) + + create(:scan_result_policy_violation, :new_scan_finding, + project: project, + merge_request: merge_request, + approval_policy_rule: approval_policy_rule, + uuids: ['additional-uuid']) + end +end