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:
-
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
-
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
-
Vulnerability Report Integration
- Policy violation metadata in vulnerability records
- Filtering capabilities for policy-violating vulnerabilities
- Performance impact on vulnerability queries
-
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:
- Performance Requirements: What are the expected query performance requirements for filtering vulnerabilities by policy violations? Are there specific SLA targets?
- Data Retention: How long should policy violation audit data be retained? This affects storage and query optimization strategies.
-
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
- Integration Points: Which specific vulnerability report features need to integrate with policy data? (filtering, sorting, export, etc.)
- Backward Compatibility: Are there existing vulnerability report queries or APIs that must remain unchanged?
- Dependencies: What are the hard dependencies on Security Insights team deliverables vs. what can be implemented independently?
Edited by Andy Schoenen