From 4b3f6a0657b1979cd6534e0e87f168eab72d6c3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20C=CC=8Cavoj?= <mcavoj@gitlab.com>
Date: Thu, 15 Feb 2024 12:05:14 +0100
Subject: [PATCH] Prevent policy bot message on non-applicable branches

Changelog: fixed
EE: true
---
 config/sidekiq_queues.yml                     |   2 +
 .../ee/merge_requests/update_service.rb       |   2 +-
 .../update_approvals_service.rb               |   6 +-
 .../sync_license_scanning_rules_service.rb    |   5 +-
 ee/app/workers/all_queues.yml                 |   9 ++
 .../sync_policy_violation_comment_worker.rb   |  35 ++++++
 .../ee/merge_requests/update_service_spec.rb  |   8 +-
 .../update_approvals_service_spec.rb          |  31 ++++-
 ...ync_license_scanning_rules_service_spec.rb |  21 +++-
 ...nc_policy_violation_comment_worker_spec.rb | 110 ++++++++++++++++++
 10 files changed, 218 insertions(+), 11 deletions(-)
 create mode 100644 ee/app/workers/security/sync_policy_violation_comment_worker.rb
 create mode 100644 ee/spec/workers/security/sync_policy_violation_comment_worker_spec.rb

diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2e5fa7923b32e4..69e82ea6738b33 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -711,6 +711,8 @@
   - 2
 - - security_scans_purge_by_job_id
   - 1
+- - security_sync_policy_violation_comment
+  - 1
 - - security_sync_scan_policies
   - 1
 - - security_unenforceable_policy_rules_notification
diff --git a/ee/app/services/ee/merge_requests/update_service.rb b/ee/app/services/ee/merge_requests/update_service.rb
index 523c49d83dd7e0..5cd14293c5659a 100644
--- a/ee/app/services/ee/merge_requests/update_service.rb
+++ b/ee/app/services/ee/merge_requests/update_service.rb
@@ -58,7 +58,7 @@ def sync_any_merge_request_approval_rules(merge_request)
       end
 
       def notify_for_policy_violations(merge_request)
-        ::Security::UnenforceablePolicyRulesNotificationWorker.perform_async(merge_request.id)
+        ::Security::SyncPolicyViolationCommentWorker.perform_async(merge_request.id)
       end
     end
   end
diff --git a/ee/app/services/security/scan_result_policies/update_approvals_service.rb b/ee/app/services/security/scan_result_policies/update_approvals_service.rb
index a38a5e23d910e5..30f22a8998a630 100644
--- a/ee/app/services/security/scan_result_policies/update_approvals_service.rb
+++ b/ee/app/services/security/scan_result_policies/update_approvals_service.rb
@@ -37,7 +37,11 @@ def execute
         update_required_approvals(violated_rules, unviolated_rules)
         violations.add(violated_rules.pluck(:scan_result_policy_id), unviolated_rules.pluck(:scan_result_policy_id)) # rubocop:disable CodeReuse/ActiveRecord
         violations.execute
-        generate_policy_bot_comment(merge_request, all_scan_finding_rules, :scan_finding)
+        generate_policy_bot_comment(
+          merge_request,
+          all_scan_finding_rules.applicable_to_branch(merge_request.target_branch),
+          :scan_finding
+        )
       end
 
       private
diff --git a/ee/app/services/security/sync_license_scanning_rules_service.rb b/ee/app/services/security/sync_license_scanning_rules_service.rb
index c025afa6dd4b27..c98da6a2636412 100644
--- a/ee/app/services/security/sync_license_scanning_rules_service.rb
+++ b/ee/app/services/security/sync_license_scanning_rules_service.rb
@@ -53,7 +53,10 @@ def remove_required_license_finding_approval(merge_request)
       log_violated_rules(merge_request, violated_rules)
       violations.add(violated_rules.pluck(:scan_result_policy_id), unviolated_rules.pluck(:scan_result_policy_id)) # rubocop:disable CodeReuse/ActiveRecord
       violations.execute
-      generate_policy_bot_comment(merge_request, license_approval_rules, :license_scanning)
+      generate_policy_bot_comment(
+        merge_request,
+        license_approval_rules.applicable_to_branch(merge_request.target_branch),
+        :license_scanning)
     end
 
     def update_required_approvals(merge_request, violated_rules, unviolated_rules)
diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml
index 750f9c80ffcc5a..e036f778be7230 100644
--- a/ee/app/workers/all_queues.yml
+++ b/ee/app/workers/all_queues.yml
@@ -2046,6 +2046,15 @@
   :weight: 1
   :idempotent: true
   :tags: []
+- :name: security_sync_policy_violation_comment
+  :worker_name: Security::SyncPolicyViolationCommentWorker
+  :feature_category: :security_policy_management
+  :has_external_dependencies: false
+  :urgency: :low
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
 - :name: security_sync_scan_policies
   :worker_name: Security::SyncScanPoliciesWorker
   :feature_category: :security_policy_management
diff --git a/ee/app/workers/security/sync_policy_violation_comment_worker.rb b/ee/app/workers/security/sync_policy_violation_comment_worker.rb
new file mode 100644
index 00000000000000..183cb9a49b8939
--- /dev/null
+++ b/ee/app/workers/security/sync_policy_violation_comment_worker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Security
+  class SyncPolicyViolationCommentWorker
+    include ApplicationWorker
+    include ::Security::ScanResultPolicies::PolicyViolationCommentGenerator
+
+    idempotent!
+    data_consistency :sticky
+    feature_category :security_policy_management
+
+    def perform(merge_request_id)
+      merge_request = MergeRequest.find_by_id(merge_request_id)
+
+      unless merge_request
+        logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
+        return
+      end
+
+      return unless merge_request.project.licensed_feature_available?(:security_orchestration_policies)
+
+      approval_rules = merge_request.approval_rules
+      generate_comment(merge_request, approval_rules.scan_finding, :scan_finding)
+      generate_comment(merge_request, approval_rules.license_scanning, :license_scanning)
+    end
+
+    private
+
+    def generate_comment(merge_request, rules, report_type)
+      return if rules.blank?
+
+      generate_policy_bot_comment(merge_request, rules.applicable_to_branch(merge_request.target_branch), report_type)
+    end
+  end
+end
diff --git a/ee/spec/services/ee/merge_requests/update_service_spec.rb b/ee/spec/services/ee/merge_requests/update_service_spec.rb
index 8157011de20f33..74fbe02e4f269a 100644
--- a/ee/spec/services/ee/merge_requests/update_service_spec.rb
+++ b/ee/spec/services/ee/merge_requests/update_service_spec.rb
@@ -462,8 +462,8 @@ def update_merge_request(opts)
 
       subject(:execute) { update_merge_request(opts) }
 
-      it 'enqueues Security::UnenforceablePolicyRulesNotificationWorker' do
-        expect(Security::UnenforceablePolicyRulesNotificationWorker).to receive(:perform_async).with(merge_request.id)
+      it 'enqueues Security::SyncPolicyViolationCommentWorker' do
+        expect(Security::SyncPolicyViolationCommentWorker).to receive(:perform_async).with(merge_request.id)
 
         execute
       end
@@ -471,8 +471,8 @@ def update_merge_request(opts)
       context 'when target_branch is not changing' do
         let(:opts) { {} }
 
-        it 'does not enqueue Security::UnenforceablePolicyRulesNotificationWorker' do
-          expect(Security::UnenforceablePolicyRulesNotificationWorker).not_to receive(:perform_async)
+        it 'does not enqueue Security::SyncPolicyViolationCommentWorker' do
+          expect(Security::SyncPolicyViolationCommentWorker).not_to receive(:perform_async)
 
           execute
         end
diff --git a/ee/spec/services/security/scan_result_policies/update_approvals_service_spec.rb b/ee/spec/services/security/scan_result_policies/update_approvals_service_spec.rb
index 19fe8976df4ef9..e93f96f6265f33 100644
--- a/ee/spec/services/security/scan_result_policies/update_approvals_service_spec.rb
+++ b/ee/spec/services/security/scan_result_policies/update_approvals_service_spec.rb
@@ -42,6 +42,8 @@
       end
     end
 
+    let_it_be(:scan_result_policy_read) { create(:scan_result_policy_read) }
+
     let!(:report_approver_rule) do
       create(:report_approver_rule, :scan_finding,
         merge_request: merge_request,
@@ -50,7 +52,7 @@
         vulnerabilities_allowed: vulnerabilities_allowed,
         severity_levels: severity_levels,
         vulnerability_states: vulnerability_states,
-        scan_result_policy_id: create(:scan_result_policy_read).id
+        scan_result_policy_read: scan_result_policy_read
       )
     end
 
@@ -170,10 +172,12 @@
       it_behaves_like 'triggers policy bot comment', :scan_finding, false
 
       context 'when there are other scan_finding violations' do
+        let_it_be(:protected_branch) { create(:protected_branch, project: project, name: 'master') }
         let_it_be(:scan_result_policy_read_other_scan_finding) { create(:scan_result_policy_read, project: project) }
         let_it_be(:approval_project_rule_other) do
           create(:approval_project_rule, :scan_finding, project: project, approvals_required: 1,
-            scan_result_policy_read: scan_result_policy_read_other_scan_finding)
+            scan_result_policy_read: scan_result_policy_read_other_scan_finding,
+            protected_branches: [protected_branch])
         end
 
         let_it_be(:approver_rule_other) do
@@ -198,6 +202,27 @@
       it_behaves_like 'triggers policy bot comment', :scan_finding, true, requires_approval: false
     end
 
+    context 'when targeting an unprotected branch' do
+      let_it_be(:protected_branch) { create(:protected_branch, project: project, name: 'master') }
+      let!(:report_approver_project_rule) do
+        create(:approval_project_rule, :scan_finding, project: project,
+          approvals_required: approvals_required, scan_result_policy_read: scan_result_policy_read,
+          protected_branches: [protected_branch])
+      end
+
+      let!(:report_approver_rule) do
+        create(:report_approver_rule, :scan_finding, merge_request: merge_request,
+          approval_project_rule: report_approver_project_rule,
+          approvals_required: approvals_required, scan_result_policy_read: scan_result_policy_read)
+      end
+
+      before do
+        merge_request.update!(target_branch: 'non-protected')
+      end
+
+      it_behaves_like 'triggers policy bot comment', :scan_finding, false, requires_approval: false
+    end
+
     context 'when target pipeline is nil' do
       let_it_be(:merge_request) do
         create(:merge_request, source_branch: 'feature', target_branch: 'target-branch')
@@ -431,7 +456,7 @@
               vulnerabilities_allowed: vulnerabilities_allowed,
               severity_levels: severity_levels,
               vulnerability_states: vulnerability_states,
-              scan_result_policy_id: create(:scan_result_policy_read).id
+              scan_result_policy_read: scan_result_policy_read
             )
           end
 
diff --git a/ee/spec/services/security/sync_license_scanning_rules_service_spec.rb b/ee/spec/services/security/sync_license_scanning_rules_service_spec.rb
index 887ce8b7e94f9d..35f2944b94fbb4 100644
--- a/ee/spec/services/security/sync_license_scanning_rules_service_spec.rb
+++ b/ee/spec/services/security/sync_license_scanning_rules_service_spec.rb
@@ -10,7 +10,7 @@
     create(:merge_request, :with_merge_request_pipeline, source_project: project)
   end
 
-  let_it_be(:pipeline) do
+  let_it_be_with_reload(:pipeline) do
     create(
       :ee_ci_pipeline,
       :success,
@@ -64,14 +64,24 @@
       let(:license_states) { ['newly_detected'] }
       let(:match_on_inclusion_license) { true }
       let(:approvals_required) { 1 }
+      let_it_be(:protected_branch) do
+        create(:protected_branch, name: merge_request.target_branch, project: project)
+      end
 
       let(:scan_result_policy_read) do
         create(:scan_result_policy_read, license_states: license_states,
           match_on_inclusion_license: match_on_inclusion_license)
       end
 
+      let!(:license_finding_project_rule) do
+        create(:approval_project_rule, :license_scanning, project: project,
+          approvals_required: approvals_required, scan_result_policy_read: scan_result_policy_read,
+          protected_branches: [protected_branch])
+      end
+
       let!(:license_finding_rule) do
         create(:report_approver_rule, :license_scanning, merge_request: merge_request,
+          approval_project_rule: license_finding_project_rule,
           approvals_required: approvals_required, scan_result_policy_read: scan_result_policy_read)
       end
 
@@ -118,6 +128,15 @@
           it_behaves_like 'triggers policy bot comment', :license_scanning, true, requires_approval: false
         end
 
+        context 'when targeting an unprotected branch' do
+          before do
+            merge_request.update!(target_branch: 'non-protected')
+            pipeline.update!(ref: 'non-protected')
+          end
+
+          it_behaves_like 'triggers policy bot comment', :license_scanning, false, requires_approval: false
+        end
+
         context 'when most recent base pipeline lacks SBOM report' do
           let(:pipeline_without_sbom) do
             create(
diff --git a/ee/spec/workers/security/sync_policy_violation_comment_worker_spec.rb b/ee/spec/workers/security/sync_policy_violation_comment_worker_spec.rb
new file mode 100644
index 00000000000000..9124206eb8165c
--- /dev/null
+++ b/ee/spec/workers/security/sync_policy_violation_comment_worker_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::SyncPolicyViolationCommentWorker, feature_category: :security_policy_management do
+  describe '#perform' do
+    let_it_be(:project) { create(:project) }
+    let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+    let(:merge_request_id) { merge_request.id }
+    let(:licensed_feature) { true }
+    let(:approvals_required) { 1 }
+    let_it_be(:protected_branch) { create(:protected_branch, project: project, name: merge_request.target_branch) }
+    let_it_be(:scan_result_policy_read) { create(:scan_result_policy_read) }
+    let_it_be(:scan_finding_project_rule) do
+      create(:approval_project_rule, :scan_finding, project: project, protected_branches: [protected_branch],
+        scan_result_policy_read: scan_result_policy_read)
+    end
+
+    let!(:scan_finding_rule) do
+      create(:report_approver_rule, :scan_finding, merge_request: merge_request,
+        approval_project_rule: scan_finding_project_rule, approvals_required: approvals_required,
+        scan_result_policy_read: scan_result_policy_read)
+    end
+
+    let_it_be(:license_scanning_project_rule) do
+      create(:approval_project_rule, :license_scanning, project: project, protected_branches: [protected_branch],
+        scan_result_policy_read: scan_result_policy_read)
+    end
+
+    let!(:license_scanning_rule) do
+      create(:report_approver_rule, :license_scanning, merge_request: merge_request,
+        approval_project_rule: scan_finding_project_rule, approvals_required: approvals_required,
+        scan_result_policy_read: scan_result_policy_read)
+    end
+
+    before do
+      stub_licensed_features(security_orchestration_policies: licensed_feature)
+    end
+
+    subject(:perform) { described_class.new.perform(merge_request_id) }
+
+    it_behaves_like 'an idempotent worker' do
+      let(:job_args) { [merge_request_id] }
+    end
+
+    it 'enqueues Security::GeneratePolicyViolationCommentWorker' do
+      expect(Security::GeneratePolicyViolationCommentWorker)
+        .to receive(:perform_async).with(merge_request.id,
+          { 'report_type' => 'scan_finding', 'violated_policy' => false, 'requires_approval' => true })
+      expect(Security::GeneratePolicyViolationCommentWorker)
+        .to receive(:perform_async).with(merge_request.id,
+          { 'report_type' => 'license_scanning', 'violated_policy' => false, 'requires_approval' => true })
+
+      perform
+    end
+
+    context 'when there are violations' do
+      before do
+        create(:scan_result_policy_violation, scan_result_policy_read: scan_result_policy_read,
+          merge_request: merge_request, project: project)
+      end
+
+      it 'enqueues Security::GeneratePolicyViolationCommentWorker with correct params' do
+        expect(Security::GeneratePolicyViolationCommentWorker)
+          .to receive(:perform_async).with(merge_request.id,
+            { 'report_type' => 'scan_finding', 'violated_policy' => true, 'requires_approval' => true })
+        expect(Security::GeneratePolicyViolationCommentWorker)
+          .to receive(:perform_async).with(merge_request.id,
+            { 'report_type' => 'license_scanning', 'violated_policy' => true, 'requires_approval' => true })
+
+        perform
+      end
+
+      context 'when approvals are optional' do
+        let(:approvals_required) { 0 }
+
+        it 'enqueues Security::GeneratePolicyViolationCommentWorker with correct params' do
+          expect(Security::GeneratePolicyViolationCommentWorker)
+            .to receive(:perform_async).with(merge_request.id,
+              { 'report_type' => 'scan_finding', 'violated_policy' => true, 'requires_approval' => false })
+          expect(Security::GeneratePolicyViolationCommentWorker)
+            .to receive(:perform_async).with(merge_request.id,
+              { 'report_type' => 'license_scanning', 'violated_policy' => true, 'requires_approval' => false })
+
+          perform
+        end
+      end
+    end
+
+    context 'with a non-existing merge request' do
+      let(:merge_request_id) { non_existing_record_id }
+
+      it 'does not enqueue the worker' do
+        expect(Security::GeneratePolicyViolationCommentWorker).not_to receive(:perform_async)
+
+        perform
+      end
+    end
+
+    context 'when feature is not licensed' do
+      let(:licensed_feature) { false }
+
+      it 'does not enqueue the worker' do
+        expect(Security::GeneratePolicyViolationCommentWorker).not_to receive(:perform_async)
+
+        perform
+      end
+    end
+  end
+end
-- 
GitLab