Skip to content

BE: Prepare implementation plan for Policies Warn Mode features related to Vulnerability Report

The Policies Warn Mode feature aims to help security engineers validate policy impact before enforcement by:

  • Introducing "Warn" vs "Enforce" modes for MR approval policies
  • Generating bot comments for violations without blocking MRs
  • Creating optional approval rules for developer guidance
  • Enabling dismissal of findings with audit tracking
  • Integrating with vulnerability reporting for policy violation history

Proposed Issue Description

Objective: Create a comprehensive backend implementation plan for Policies Warn Mode features to ensure they are feasible and performant from the Vulnerability Report perspective.

Scope: This implementation plan should cover the technical architecture, database changes, API modifications, and performance considerations for:

  1. Policy Mode Configuration
    • Schema changes for enforcement mode (warn/enforce)
    • YAML policy validation updates
    • Backward compatibility considerations
➡️ implementation plan
diff --git a/ee/app/models/security/policy.rb b/ee/app/models/security/policy.rb
index 604842faca36..a0c90d18effd 100644
--- a/ee/app/models/security/policy.rb
+++ b/ee/app/models/security/policy.rb
@@ -10,7 +10,7 @@ class Policy < ApplicationRecord
     self.inheritance_column = :_type_disabled
 
     POLICY_CONTENT_FIELDS = {
-      approval_policy: %i[actions approval_settings fallback_behavior policy_tuning bypass_settings],
+      approval_policy: %i[actions approval_settings fallback_behavior policy_tuning bypass_settings enforcement_type],
       scan_execution_policy: %i[actions skip_ci],
       pipeline_execution_policy: %i[content pipeline_config_strategy suffix skip_ci variables_override],
       vulnerability_management_policy: %i[actions],
@@ -295,7 +295,16 @@ def policy_content
     end
     strong_memoize_attr :policy_content
 
+    def enforcement_type
+      content&.dig('enforcement_type') || 'enforce'
+    end
+
     def warn_mode?
+      # Check new enforcement_type field first
+      enforcement_type_value = content&.dig('enforcement_type')
+      return enforcement_type_value == 'warn' if enforcement_type_value.present?
+
+      # Fall back to legacy approvals_required: 0 pattern for backwards compatibility
       actions = content&.dig('actions')
       return false unless actions
 
diff --git a/ee/app/validators/json_schemas/approval_policy_content.json b/ee/app/validators/json_schemas/approval_policy_content.json
index bf30c2bfd8d3..1451f8b7fe86 100644
--- a/ee/app/validators/json_schemas/approval_policy_content.json
+++ b/ee/app/validators/json_schemas/approval_policy_content.json
@@ -317,6 +317,12 @@
           }
         }
       }
+    },
+    "enforcement_type": {
+      "description": "Defines how this policy should be enforced. 'enforce' (default) blocks merge requests when violations are detected. 'warn' allows merge requests to proceed but shows warnings.",
+      "type": "string",
+      "enum": ["warn", "enforce"],
+      "default": "enforce"
     }
   },
   "additionalProperties": false
diff --git a/ee/app/validators/json_schemas/security_orchestration_policy.json b/ee/app/validators/json_schemas/security_orchestration_policy.json
index 488cc29ad554..cb18aa500f6e 100644
--- a/ee/app/validators/json_schemas/security_orchestration_policy.json
+++ b/ee/app/validators/json_schemas/security_orchestration_policy.json
@@ -1073,6 +1073,12 @@
             "description": "Whether to enforce this policy or not.",
             "type": "boolean"
           },
+          "enforcement_type": {
+            "description": "Defines how this policy should be enforced. 'enforce' (default) blocks merge requests when violations are detected. 'warn' allows merge requests to proceed but shows warnings.",
+            "type": "string",
+            "enum": ["warn", "enforce"],
+            "default": "enforce"
+          },
           "fallback_behavior": {
             "type": "object",
             "properties": {

See: master...549766-add-enforcement-type-to-approval-policy-schema

  1. Violation Tracking & Dismissal
    • Database schema for tracking dismissed findings
    • Audit event generation for policy violations
    • Integration with existing vulnerability data
➡️ Implementation plan
diff --git a/db/migrate/20250129000001_create_security_finding_policy_dismissals.rb b/db/migrate/20250129000001_create_security_finding_policy_dismissals.rb
new file mode 100644
index 000000000000..ae52ccb2c44c
--- /dev/null
+++ b/db/migrate/20250129000001_create_security_finding_policy_dismissals.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class CreateSecurityFindingPolicyDismissals < Gitlab::Database::Migration[2.2]
+  milestone '17.8'
+
+  def change
+    create_table :security_finding_policy_dismissals do |t|
+      t.references :security_finding, null: false, type: :bigint
+      t.references :security_policy, null: false, foreign_key: { to_table: :security_policies }, type: :bigint
+      t.references :merge_request, null: false, foreign_key: true, type: :bigint
+      t.references :dismissed_by, null: false, foreign_key: { to_table: :users }, type: :bigint
+      t.references :project, null: false, foreign_key: true, type: :bigint
+
+      t.text :finding_uuid, null: false, limit: 36
+      t.text :dismissal_reason, limit: 255
+      t.jsonb :dismissal_context, default: {}
+      t.integer :status, default: 0, null: false
+
+      t.timestamps_with_timezone null: false
+
+      t.index [:finding_uuid, :security_policy_id], unique: true,
+              name: 'idx_security_finding_policy_dismissals_unique'
+      t.index [:security_policy_id, :merge_request_id],
+              name: 'idx_finding_dismissals_policy_mr'
+      t.index [:project_id, :created_at],
+              name: 'idx_finding_dismissals_project_created'
+      t.index [:dismissed_by_id, :created_at],
+              name: 'idx_finding_dismissals_user_created'
+    end
+
+    add_check_constraint :security_finding_policy_dismissals,
+                        "char_length(finding_uuid) <= 36",
+                        'check_finding_uuid_length'
+    add_check_constraint :security_finding_policy_dismissals,
+                        "char_length(dismissal_reason) <= 255",
+                        'check_dismissal_reason_length'
+  end
+end
diff --git a/db/post_migrate/20250129000002_add_foreign_keys_to_security_finding_policy_dismissals.rb b/db/post_migrate/20250129000002_add_foreign_keys_to_security_finding_policy_dismissals.rb
new file mode 100644
index 000000000000..2fea6a0f0c71
--- /dev/null
+++ b/db/post_migrate/20250129000002_add_foreign_keys_to_security_finding_policy_dismissals.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class AddForeignKeysToSecurityFindingPolicyDismissals < Gitlab::Database::Migration[2.2]
+  milestone '18.3'
+
+  disable_ddl_transaction!
+
+  def up
+    # Note: We can't add a direct FK to security_findings due to partitioning
+    # Instead, we'll rely on application-level constraints and cleanup jobs
+
+    add_concurrent_foreign_key :security_finding_policy_dismissals, :security_policies,
+                              column: :security_policy_id,
+                              on_delete: :cascade
+
+    add_concurrent_foreign_key :security_finding_policy_dismissals, :merge_requests,
+                              column: :merge_request_id,
+                              on_delete: :cascade
+
+    add_concurrent_foreign_key :security_finding_policy_dismissals, :users,
+                              column: :dismissed_by_id,
+                              on_delete: :nullify
+  end
+
+  def down
+    remove_foreign_key_if_exists :security_finding_policy_dismissals, column: :security_policy_id
+    remove_foreign_key_if_exists :security_finding_policy_dismissals, column: :merge_request_id
+    remove_foreign_key_if_exists :security_finding_policy_dismissals, column: :dismissed_by_id
+  end
+end
diff --git a/ee/app/graphql/mutations/security/dismiss_findings.rb b/ee/app/graphql/mutations/security/dismiss_findings.rb
new file mode 100644
index 000000000000..01d35028b1f2
--- /dev/null
+++ b/ee/app/graphql/mutations/security/dismiss_findings.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Mutations
+  module Security
+    class DismissFindings < BaseMutation
+      graphql_name 'SecurityDismissFindings'
+      description 'Dismiss security findings for a specific policy'
+
+      authorize :admin_security_policy
+
+      argument :project_path, GraphQL::Types::ID,
+               required: true,
+               description: 'Full path of the project'
+
+      argument :finding_uuids, [GraphQL::Types::String],
+               required: true,
+               description: 'UUIDs of findings to dismiss'
+
+      argument :policy_id, GraphQL::Types::ID,
+               required: true,
+               description: 'ID of the security policy'
+
+      argument :merge_request_id, GraphQL::Types::ID,
+               required: true,
+               description: 'ID of the merge request'
+
+      argument :dismissal_reason, GraphQL::Types::String,
+               required: false,
+               description: 'Reason for dismissing the findings'
+
+      argument :additional_context, GraphQL::Types::JSON,
+               required: false,
+               description: 'Additional context for the dismissal'
+
+      field :dismissed_findings, [Types::Security::FindingType],
+            null: true,
+            description: 'Dismissed security findings'
+
+      field :policy, Types::Security::PolicyType,
+            null: true,
+            description: 'Security policy used for dismissal'
+
+      field :errors, [GraphQL::Types::String],
+            null: false,
+            description: 'Errors encountered during dismissal'
+
+      def resolve(project_path:, finding_uuids:, policy_id:, merge_request_id:, **args)
+        project = authorized_find!(project_path)
+
+        result = ::Security::Findings::DismissService.new(
+          project: project,
+          current_user: current_user,
+          params: {
+            finding_uuids: finding_uuids,
+            policy_id: policy_id,
+            merge_request_id: merge_request_id,
+            dismissal_reason: args[:dismissal_reason],
+            additional_context: args[:additional_context],
+            client_info: 'graphql'
+          }
+        ).execute
+
+        if result.success?
+          {
+            dismissed_findings: result.payload[:dismissed_findings],
+            policy: result.payload[:policy],
+            errors: []
+          }
+        else
+          {
+            dismissed_findings: nil,
+            policy: nil,
+            errors: [result.message]
+          }
+        end
+      end
+
+      private
+
+      def authorized_find!(project_path)
+        project = Project.find_by_full_path(project_path)
+        raise_resource_not_available_error! unless project
+
+        project
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/ee/app/models/merge_request_extensions.rb b/ee/app/models/merge_request_extensions.rb
new file mode 100644
index 000000000000..a2e85f78ad8a
--- /dev/null
+++ b/ee/app/models/merge_request_extensions.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# Extension for MergeRequest model to add dismissal relationships
+module MergeRequestExtensions
+  extend ActiveSupport::Concern
+
+  included do
+    has_many :security_finding_policy_dismissals,
+             class_name: 'Security::FindingPolicyDismissal',
+             inverse_of: :merge_request,
+             dependent: :destroy
+
+    has_many :active_security_finding_dismissals,
+             -> { status_active },
+             class_name: 'Security::FindingPolicyDismissal',
+             inverse_of: :merge_request
+  end
+
+  # Get all findings dismissed in this MR
+  def dismissed_security_findings
+    finding_uuids = active_security_finding_dismissals.pluck(:finding_uuid)
+    return Security::Finding.none if finding_uuids.empty?
+
+    Security::Finding.by_uuid(finding_uuids)
+  end
+
+  # Check if this MR introduced any dismissed findings to main branch
+  def introduced_dismissed_findings_to_main?
+    return false unless merged?
+    
+    security_finding_policy_dismissals.any?
+  end
+
+  # Get dismissal summary for this MR
+  def security_dismissal_summary
+    dismissals = security_finding_policy_dismissals.includes(:security_policy, :dismissed_by)
+    
+    {
+      total_dismissals: dismissals.count,
+      policies_with_dismissals: dismissals.joins(:security_policy).distinct.count('security_policies.id'),
+      dismissal_reasons: dismissals.group(:dismissal_reason).count,
+      dismissed_by_users: dismissals.joins(:dismissed_by).distinct.count('users.id')
+    }
+  end
+end
\ No newline at end of file
diff --git a/ee/app/models/security/finding_policy_dismissal.rb b/ee/app/models/security/finding_policy_dismissal.rb
new file mode 100644
index 000000000000..9eb6002fd5b3
--- /dev/null
+++ b/ee/app/models/security/finding_policy_dismissal.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Security
+  class FindingPolicyDismissal < ApplicationRecord
+    include EachBatch
+    include Gitlab::Utils::StrongMemoize
+
+    self.table_name = 'security_finding_policy_dismissals'
+
+    belongs_to :security_policy, class_name: 'Security::Policy', inverse_of: :finding_dismissals
+    belongs_to :merge_request, inverse_of: :security_finding_policy_dismissals
+    belongs_to :dismissed_by, class_name: 'User', inverse_of: :dismissed_security_findings
+
+    validates :finding_uuid, presence: true, length: { maximum: 36 }
+    validates :finding_uuid, uniqueness: { scope: :security_policy_id }
+
+    scope :for_finding_uuids, ->(uuids) { where(finding_uuid: uuids) }
+    scope :for_policy, ->(policy) { where(security_policy: policy) }
+    scope :for_merge_request, ->(mr) { where(merge_request: mr) }
+
+    # Get the associated security finding (may be across partitions)
+    def security_finding
+      strong_memoize(:security_finding) do
+        Security::Finding.by_uuid(finding_uuid).first
+      end
+    end
+
+    # Class methods for bulk operations
+    class << self
+      def dismiss_findings_for_policy!(findings, policy, merge_request, user, reason: nil, context: {})
+        dismissals = findings.map do |finding|
+          {
+            finding_uuid: finding.uuid,
+            security_policy_id: policy.id,
+            merge_request_id: merge_request.id,
+            dismissed_by_id: user.id,
+            project_id: merge_request.project_id,
+            dismissal_reason: reason,
+            dismissal_context: context.merge(
+              'finding_severity' => finding.severity,
+              'finding_scanner' => finding.scanner&.name,
+              'dismissed_at' => Time.current.iso8601
+            ),
+            created_at: Time.current,
+            updated_at: Time.current
+          }
+        end
+
+        insert_all(dismissals, unique_by: [:finding_uuid, :security_policy_id])
+      end
+
+      def revoke_dismissals_for_policy!(policy, reason: nil)
+        for_policy(policy).status_active.find_each do |dismissal|
+          dismissal.revoke!(reason: reason)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/models/security/policy_extensions.rb b/ee/app/models/security/policy_extensions.rb
new file mode 100644
index 000000000000..a3b77fc7b6ee
--- /dev/null
+++ b/ee/app/models/security/policy_extensions.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# Extension for Security::Policy model to add dismissal relationships
+module Security
+  module PolicyExtensions
+    extend ActiveSupport::Concern
+
+    included do
+      has_many :finding_dismissals,
+               class_name: 'Security::FindingPolicyDismissal',
+               foreign_key: 'security_policy_id',
+               inverse_of: :security_policy,
+               dependent: :destroy
+
+      has_many :active_finding_dismissals,
+               -> { status_active },
+               class_name: 'Security::FindingPolicyDismissal',
+               foreign_key: 'security_policy_id',
+               inverse_of: :security_policy
+    end
+
+    # Get all findings dismissed by this policy
+    def dismissed_findings
+      finding_uuids = active_finding_dismissals.pluck(:finding_uuid)
+      return Security::Finding.none if finding_uuids.empty?
+
+      Security::Finding.by_uuid(finding_uuids)
+    end
+  end
+end
diff --git a/ee/app/services/security/findings/dismiss_service.rb b/ee/app/services/security/findings/dismiss_service.rb
index 039c4ecfe19c..bb37840dad19 100644
--- a/ee/app/services/security/findings/dismiss_service.rb
+++ b/ee/app/services/security/findings/dismiss_service.rb
@@ -2,105 +2,151 @@
 
 module Security
   module Findings
-    class DismissService < BaseProjectService
+    class DismissService < BaseService
       include Gitlab::Allowable
 
-      def initialize(user:, security_finding:, comment: nil, dismissal_reason: nil)
-        super(project: security_finding.project, current_user: user)
-        @security_finding = security_finding
-        @comment = comment
-        @dismissal_reason = dismissal_reason
+      def initialize(project:, current_user:, params: {})
+        @project = project
+        @current_user = current_user
+        @params = params
       end
 
       def execute
-        return ServiceResponse.error(message: "Access denied", http_status: :forbidden) unless authorized?
+        return error('Unauthorized') unless can?(current_user, :admin_security_policy, project)
+        return error('Invalid parameters') unless valid_params?
 
-        dismiss_finding
+        findings = find_security_findings
+        return error('No findings found') if findings.empty?
+
+        policy = find_security_policy
+        return error('Policy not found') unless policy
+        return error('Policy not applicable to project') unless policy.scope_applicable?(project)
+
+        merge_request = find_merge_request
+        return error('Merge request not found') unless merge_request
+
+        dismiss_findings!(findings, policy, merge_request)
+
+        success(dismissed_findings: findings, policy: policy, merge_request: merge_request)
       end
 
       private
 
-      def authorized?
-        can?(@current_user, :admin_vulnerability, @project)
+      attr_reader :project, :current_user, :params
+
+      def valid_params?
+        params[:finding_uuids].present? &&
+          params[:policy_id].present? &&
+          params[:merge_request_id].present?
+      end
+
+      def find_security_findings
+        Security::Finding.by_uuid(params[:finding_uuids])
+                        .joins(:scan)
+                        .merge(Security::Scan.by_project(project))
+      end
+
+      def find_security_policy
+        Security::Policy.find_by(id: params[:policy_id])
       end
 
-      def dismiss_finding
-        @error_message = nil
+      def find_merge_request
+        project.merge_requests.find_by(id: params[:merge_request_id])
+      end
 
-        SecApplicationRecord.transaction do
-          create_or_update_feedback
-          create_and_dismiss_vulnerability
+      def dismiss_findings!(findings, policy, merge_request)
+        ApplicationRecord.transaction do
+          # Create dismissal records
+          Security::FindingPolicyDismissal.dismiss_findings_for_policy!(
+            findings,
+            policy,
+            merge_request,
+            current_user,
+            reason: params[:dismissal_reason],
+            context: dismissal_context
+          )
+
+          # Update scan result policy violations if needed
+          update_policy_violations(findings, policy, merge_request)
+
+          # Log audit event
+          log_dismissal_audit_event(findings, policy, merge_request)
+
+          # Send notifications
+          notify_stakeholders(findings, policy, merge_request)
         end
+      end
 
-        if @error_message
-          error_string = format(_("failed to dismiss security finding: %{message}"), message: @error_message)
-          ServiceResponse.error(message: error_string, http_status: :unprocessable_entity)
-        else
-          ServiceResponse.success(payload: { security_finding: @security_finding })
+      def dismissal_context
+        {
+          'dismissed_via' => 'api',
+          'client_info' => params[:client_info],
+          'additional_context' => params[:additional_context] || {}
+        }.compact
+      end
+
+      def update_policy_violations(findings, policy, merge_request)
+        # Update existing scan result policy violations to reflect dismissals
+        violations = merge_request.scan_result_policy_violations
+                                 .joins(:security_policy)
+                                 .where(security_policies: { id: policy.id })
+
+        violations.each do |violation|
+          update_violation_data_with_dismissals(violation, findings)
         end
       end
 
-      # This method will be removed after the deprecation of Vulnerability Feedbacks is declared successful
-      # It is a temporary measure to permit revert to Feedbacks if necessary.
-      def create_or_update_feedback
-        feedback = @project
-          .vulnerability_feedback
-          .with_feedback_type('dismissal')
-          .by_finding_uuid([@security_finding.uuid]).first
-
-        if feedback
-          # We want to update existing feedback only for comment
-          feedback.update!(vulnerability_feedback_attributes)
-        else
-          result = ::VulnerabilityFeedback::CreateService.new(
-            @project,
-            @current_user,
-            feedback_params
-          ).execute
-
-          return if result[:status] == :success
-
-          @error_message = result[:message].full_messages.join(",")
-          raise ActiveRecord::Rollback
+      def update_violation_data_with_dismissals(violation, findings)
+        violation_data = violation.violation_data || {}
+        violation_findings = violation_data['findings'] || []
+
+        findings.each do |finding|
+          finding_data = violation_findings.find { |f| f['uuid'] == finding.uuid }
+          next unless finding_data
+
+          finding_data['dismissed'] = true
+          finding_data['dismissed_at'] = Time.current.iso8601
+          finding_data['dismissed_by'] = current_user.id
         end
+
+        violation.update!(violation_data: violation_data.merge('findings' => violation_findings))
       end
 
-      def create_and_dismiss_vulnerability
-        security_finding_params = {
-          security_finding_uuid: @security_finding.uuid,
-          comment: @comment,
-          dismissal_reason: @dismissal_reason
+      def log_dismissal_audit_event(findings, policy, merge_request)
+        audit_context = {
+          name: 'security_finding_dismissed',
+          author: current_user,
+          scope: project,
+          target: policy,
+          message: "Dismissed #{findings.count} security findings for policy '#{policy.name}' in MR !#{merge_request.iid}",
+          additional_details: {
+            finding_uuids: findings.pluck(:uuid),
+            policy_id: policy.id,
+            merge_request_id: merge_request.id,
+            dismissal_reason: params[:dismissal_reason]
+          }
         }
 
-        ::Vulnerabilities::FindOrCreateFromSecurityFindingService.new(
-          project: @project,
-          current_user: @current_user,
-          params: security_finding_params,
-          state: :dismissed,
-          present_on_default_branch: false
-        ).execute
+        ::Gitlab::Audit::Auditor.audit(audit_context)
       end
 
-      def feedback_params
-        {
-          category: @security_finding.scan_type,
-          feedback_type: 'dismissal',
-          comment: @comment,
-          dismissal_reason: @dismissal_reason,
-          pipeline: @security_finding.pipeline,
-          finding_uuid: @security_finding.uuid,
-          dismiss_vulnerability: false,
-          migrated_to_state_transition: true
-        }
+      def notify_stakeholders(findings, policy, merge_request)
+        # Notify security team about dismissals
+        Security::FindingDismissalNotificationWorker.perform_async(
+          findings.pluck(:uuid),
+          policy.id,
+          merge_request.id,
+          current_user.id
+        )
       end
 
-      def vulnerability_feedback_attributes
-        if @comment.present?
-          { comment: @comment, comment_timestamp: Time.zone.now, comment_author: @current_user }
-        else
-          { comment: nil, comment_timestamp: nil, comment_author: nil }
-        end
+      def success(payload)
+        ServiceResponse.success(payload: payload)
+      end
+
+      def error(message)
+        ServiceResponse.error(message: message)
       end
     end
   end
-end
+end
\ No newline at end of file
diff --git a/ee/app/workers/security/finding_dismissal_cleanup_worker.rb b/ee/app/workers/security/finding_dismissal_cleanup_worker.rb
new file mode 100644
index 000000000000..aeff16c462d4
--- /dev/null
+++ b/ee/app/workers/security/finding_dismissal_cleanup_worker.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Security
+  class FindingDismissalCleanupWorker
+    include ApplicationWorker
+    include CronjobQueue
+
+    data_consistency :always
+    feature_category :security_policy_management
+    urgency :low
+
+    def perform
+      cleanup_orphaned_dismissals
+      cleanup_expired_dismissals
+    end
+
+    private
+
+    def cleanup_orphaned_dismissals
+      Security::FindingPolicyDismissal.cleanup_orphaned_dismissals!
+    end
+
+    def cleanup_expired_dismissals
+      # Mark dismissals as expired if the associated policy is disabled
+      # or if the merge request was closed without merging
+      Security::FindingPolicyDismissal
+        .status_active
+        .joins(:security_policy, :merge_request)
+        .where(
+          security_policies: { enabled: false }
+        )
+        .or(
+          Security::FindingPolicyDismissal
+            .status_active
+            .joins(:merge_request)
+            .where(merge_requests: { state_id: MergeRequest.available_states[:closed] })
+        )
+        .find_each(batch_size: 1000) do |dismissal|
+          dismissal.update!(
+            status: :expired,
+            dismissal_context: dismissal.dismissal_context.merge(
+              'expired_at' => Time.current.iso8601,
+              'expiration_reason' => 'policy_disabled_or_mr_closed'
+            )
+          )
+        end
+    end
+  end
+end
\ No newline at end of file
  1. Vulnerability Report Integration
    • Policy violation metadata in vulnerability records
    • Filtering capabilities for policy-violating vulnerabilities
    • Performance impact on vulnerability queries
  2. Bot Comment Enhancements
    • Updated messaging for warn mode
    • Notification mechanisms for policy owners

Clarifying Questions

To create a more detailed implementation plan, I need some clarification:

  1. Performance Requirements: What are the expected query performance requirements for filtering vulnerabilities by policy violations? Are there specific SLA targets?
  2. Data Retention: How long should policy violation audit data be retained? This affects storage and query optimization strategies.
  3. Scale Considerations: What's the expected volume of:
    • Policies in warn mode per organization
    • MRs with policy violations per day
    • Vulnerability records that need policy metadata
  4. Integration Points: Which specific vulnerability report features need to integrate with policy data? (filtering, sorting, export, etc.)
  5. Backward Compatibility: Are there existing vulnerability report queries or APIs that must remain unchanged?
  6. Dependencies: What are the hard dependencies on Security Insights team deliverables vs. what can be implemented independently?
Edited by Andy Schoenen