Skip to content

Security bot says policy violate but not accurate

Summary

Customer reported all merge requests received a comment from the GitLab Security Bot stating a policy violation. However, the policies currently do not block or require approval right now. While it doesn't seem to be blocking anyone, its behavior is confusing.

Steps to reproduce

What is the current bug behavior?

Security bot stating that there's policy violation when there isn't any

What is the expected correct behavior?

Security bot should only prompted when there's an actual policy violation

Relevant logs and/or screenshots

Screenshot 2023-08-03 at 4.40.03 PM.png

Output of checks

Results of GitLab environment info

Expand for output related to GitLab environment info

(For installations with omnibus-gitlab package run and paste the output of: \\\`sudo gitlab-rake gitlab:env:info\\\`) (For installations from source run and paste the output of: \\\`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production\\\`)

Results of GitLab application Check

Expand for output related to the GitLab application check

(For installations with omnibus-gitlab package run and paste the output of: \`sudo gitlab-rake gitlab:check SANITIZE=true\`) (For installations from source run and paste the output of: \`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true\`) (we will only investigate if the tests are passing)

Possible fixes

We should show a different message when the approvals are optional.

diff --git a/ee/app/services/security/scan_result_policies/generate_policy_violation_comment_service.rb b/ee/app/services/security/scan_result_policies/generate_policy_violation_comment_service.rb
index 7d82c2e51825..9614e16b0a40 100644
--- a/ee/app/services/security/scan_result_policies/generate_policy_violation_comment_service.rb
+++ b/ee/app/services/security/scan_result_policies/generate_policy_violation_comment_service.rb
@@ -7,13 +7,14 @@ class GeneratePolicyViolationCommentService

       LOCK_SLEEP_SEC = 0.5.seconds

-      attr_reader :merge_request, :project, :report_type, :violated_policy
+      attr_reader :merge_request, :project, :report_type, :violated_policy, :requires_approval

-      def initialize(merge_request, report_type, violated_policy)
+      def initialize(merge_request, params = {})
         @merge_request = merge_request
         @project = merge_request.project
-        @report_type = report_type
-        @violated_policy = violated_policy
+        @report_type = params['report_type']
+        @violated_policy = params['violated_policy']
+        @requires_approval = params['requires_approval']
       end

       def execute
@@ -45,7 +46,7 @@ def exclusive_lock_key
       def comment
         @comment ||= PolicyViolationComment.new(existing_comment).tap do |violation_comment|
           if violated_policy
-            violation_comment.add_report_type(report_type)
+            violation_comment.add_report_type(report_type, requires_approval)
           else
             violation_comment.remove_report_type(report_type)
           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 3df6bb977155..0f697a4ece94 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
@@ -26,7 +26,7 @@ def execute
         violated_rules, unviolated_rules = partition_rules(approval_rules)

         ApprovalMergeRequestRule.remove_required_approved(unviolated_rules) if unviolated_rules.any?
-        generate_policy_bot_comment(violated_rules.any?)
+        generate_policy_bot_comment(violated_rules)
       end

       private
@@ -94,16 +94,21 @@ def target_pipeline_security_scan_types
       end
       strong_memoize_attr :target_pipeline_security_scan_types

-      def generate_policy_bot_comment(violated_policy)
+      def generate_policy_bot_comment(violated_rules)
         return if Feature.disabled?(:security_policy_approval_notification, pipeline.project)

         Security::GeneratePolicyViolationCommentWorker.perform_async(
           merge_request.id,
           { 'report_type' => Security::ScanResultPolicies::PolicyViolationComment::REPORT_TYPES[:scan_finding],
-            'violated_policy' => violated_policy }
+            'violated_policy' => violated_rules.any?,
+            'requires_approval' => rules_requiring_approval?(violated_rules) }
         )
       end

+      def rules_requiring_approval?(approval_rules)
+        approval_rules.any? { |rule| rule.approvals_required > 0 }
+      end
+
       def target_pipeline
         merge_request.latest_finished_target_branch_pipeline_for_scan_result_policy
       end
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 f2b8bb2fe8ea..7a00222c8e4c 100644
--- a/ee/app/services/security/sync_license_scanning_rules_service.rb
+++ b/ee/app/services/security/sync_license_scanning_rules_service.rb
@@ -47,7 +47,7 @@ def remove_required_license_finding_approval(merge_request)
         violates_policy?(merge_request, rule)
       end
       ApprovalMergeRequestRule.remove_required_approved(unviolated_rules)
-      generate_policy_bot_comment(merge_request, violated_rules.any?)
+      generate_policy_bot_comment(merge_request, violated_rules)
     end

     ## Checks if a policy rule violates the following conditions:
@@ -139,14 +139,19 @@ def license_names_from_report
     end
     strong_memoize_attr :license_names_from_report

-    def generate_policy_bot_comment(merge_request, violated_policy)
+    def generate_policy_bot_comment(merge_request, violated_rules)
       return if Feature.disabled?(:security_policy_approval_notification, pipeline.project)

       Security::GeneratePolicyViolationCommentWorker.perform_async(
         merge_request.id,
         { 'report_type' => Security::ScanResultPolicies::PolicyViolationComment::REPORT_TYPES[:license_scanning],
-          'violated_policy' => violated_policy }
+          'violated_policy' => violated_rules.any?,
+          'requires_approval' => rules_requiring_approval?(violated_rules) }
       )
     end
+
+    def rules_requiring_approval?(approval_rules)
+      approval_rules.any? { |rule| rule.approvals_required > 0 }
+    end
   end
 end
diff --git a/ee/app/workers/security/generate_policy_violation_comment_worker.rb b/ee/app/workers/security/generate_policy_violation_comment_worker.rb
index 1dcaea0c58c3..06a562356833 100644
--- a/ee/app/workers/security/generate_policy_violation_comment_worker.rb
+++ b/ee/app/workers/security/generate_policy_violation_comment_worker.rb
@@ -20,8 +20,7 @@ def perform(merge_request_id, params = {})

       result = Security::ScanResultPolicies::GeneratePolicyViolationCommentService.new(
         merge_request,
-        params['report_type'],
-        params['violated_policy']
+        params
       ).execute

       return unless result.error?
@@ -37,6 +36,7 @@ def log_message(errors, merge_request_id, params)
           merge_request_id: merge_request_id,
           violated_policy: params['violated_policy'],
           report_type: params['report_type'],
+          requires_approval: params['requires_approval'],
           message: errors
         ))
     end
diff --git a/ee/lib/security/scan_result_policies/policy_violation_comment.rb b/ee/lib/security/scan_result_policies/policy_violation_comment.rb
index 8523ba5e0814..0624717ea987 100644
--- a/ee/lib/security/scan_result_policies/policy_violation_comment.rb
+++ b/ee/lib/security/scan_result_policies/policy_violation_comment.rb
@@ -5,29 +5,49 @@ module ScanResultPolicies
     class PolicyViolationComment
       MESSAGE_HEADER = '<!-- policy_violation_comment -->'
       VIOLATED_REPORTS_HEADER_PATTERN = /<!-- violated_reports: ([a-z_,]+)/
+      APPROVALS_HEADER_PATTERN = /<!-- optional_approvals: ([a-z_,]+)/
       REPORT_TYPES = {
         license_scanning: 'license_scanning',
         scan_finding: 'scan_finding'
       }.freeze

-      attr_reader :reports, :existing_comment
+      MESSAGE_REQUIRES_APPROVAL = <<~TEXT.squish
+        Security and compliance scanners enforced by your organization have completed and identified that approvals
+        are required due to one or more policy violations.
+        Review the policy's rules in the MR widget and assign reviewers to proceed.
+      TEXT
+
+      MESSAGE_REQUIRES_NO_APPROVAL = <<~TEXT.squish
+        Security and compliance scanners enforced by your organization have completed and identified one or more
+        policy violations.
+        Consider including optional reviewers based on the policy rules in the MR widget.
+      TEXT
+
+      attr_reader :reports, :optional_approval_reports, :existing_comment

       def initialize(existing_comment)
         @existing_comment = existing_comment
         @reports = Set.new
+        @optional_approval_reports = Set.new

         return unless existing_comment

-        match = existing_comment.note.match(VIOLATED_REPORTS_HEADER_PATTERN)
-        match[1].split(',').each { |report_type| add_report_type(report_type) } if match
+        parse_reports
       end

-      def add_report_type(report_type)
+      def add_report_type(report_type, requires_approval)
         @reports = (reports + [report_type]) & REPORT_TYPES.values
+
+        add_optional_approval_report(report_type) unless requires_approval
+      end
+
+      def add_optional_approval_report(report_type)
+        @optional_approval_reports = (optional_approval_reports + [report_type]) & REPORT_TYPES.values
       end

       def remove_report_type(report_type)
         @reports -= [report_type]
+        @optional_approval_reports -= [report_type]
       end

       def body
@@ -38,6 +58,18 @@ def body

       private

+      def parse_reports
+        match = existing_comment.note.match(VIOLATED_REPORTS_HEADER_PATTERN)
+        match[1].split(',').each { |report_type| add_report_type(report_type, true) } if match
+
+        parse_optional_approval_reports
+      end
+
+      def parse_optional_approval_reports
+        match = existing_comment.note.match(APPROVALS_HEADER_PATTERN)
+        match[1].split(',').each { |report_type| add_optional_approval_report(report_type) } if match
+      end
+
       def fixed_note_body
         'Security policy violations have been resolved.'
       end
@@ -45,13 +77,10 @@ def fixed_note_body
       def body_message
         return fixed_note_body if reports.empty?

-        message = <<~TEXT.squish
-          Security and compliance scanners enforced by your organization have completed and identified that approvals
-          are required due to one or more policy violations.
-          Review the policy's rules in the MR widget and assign reviewers to proceed.
-        TEXT
+        message = reports == optional_approval_reports ? MESSAGE_REQUIRES_NO_APPROVAL : MESSAGE_REQUIRES_APPROVAL
         <<~MARKDOWN
           <!-- violated_reports: #{reports.join(',')} -->
+          #{"<!-- optional_approvals: #{optional_approval_reports.join(',')} -->" if optional_approval_reports.any?}
           | :warning: **Policy violation(s) detected**|
           | ----------------------------------------- |
           | #{message}                                |
Edited by Martin Čavoj