diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 73292268..cc8cadc1 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -151,6 +151,57 @@ export type Database = { } Relationships: [] } + alert_correlation_rules: { + Row: { + alert_patterns: Json + auto_create_incident: boolean + created_at: string + created_by: string | null + description: string | null + enabled: boolean + id: string + incident_description_template: string | null + incident_severity: string + incident_title_template: string + min_alerts_required: number + rule_name: string + time_window_minutes: number + updated_at: string + } + Insert: { + alert_patterns: Json + auto_create_incident?: boolean + created_at?: string + created_by?: string | null + description?: string | null + enabled?: boolean + id?: string + incident_description_template?: string | null + incident_severity: string + incident_title_template: string + min_alerts_required?: number + rule_name: string + time_window_minutes?: number + updated_at?: string + } + Update: { + alert_patterns?: Json + auto_create_incident?: boolean + created_at?: string + created_by?: string | null + description?: string | null + enabled?: boolean + id?: string + incident_description_template?: string | null + incident_severity?: string + incident_title_template?: string + min_alerts_required?: number + rule_name?: string + time_window_minutes?: number + updated_at?: string + } + Relationships: [] + } approval_transaction_metrics: { Row: { created_at: string | null @@ -1551,6 +1602,110 @@ export type Database = { }, ] } + incident_alerts: { + Row: { + added_at: string + alert_id: string + alert_source: string + id: string + incident_id: string + } + Insert: { + added_at?: string + alert_id: string + alert_source: string + id?: string + incident_id: string + } + Update: { + added_at?: string + alert_id?: string + alert_source?: string + id?: string + incident_id?: string + } + Relationships: [ + { + foreignKeyName: "incident_alerts_incident_id_fkey" + columns: ["incident_id"] + isOneToOne: false + referencedRelation: "incidents" + referencedColumns: ["id"] + }, + ] + } + incidents: { + Row: { + acknowledged_at: string | null + acknowledged_by: string | null + alert_count: number + correlation_rule_id: string | null + created_at: string + description: string | null + detected_at: string + id: string + incident_number: string + resolution_notes: string | null + resolved_at: string | null + resolved_by: string | null + severity: string + status: string + title: string + updated_at: string + } + Insert: { + acknowledged_at?: string | null + acknowledged_by?: string | null + alert_count?: number + correlation_rule_id?: string | null + created_at?: string + description?: string | null + detected_at?: string + id?: string + incident_number: string + resolution_notes?: string | null + resolved_at?: string | null + resolved_by?: string | null + severity: string + status?: string + title: string + updated_at?: string + } + Update: { + acknowledged_at?: string | null + acknowledged_by?: string | null + alert_count?: number + correlation_rule_id?: string | null + created_at?: string + description?: string | null + detected_at?: string + id?: string + incident_number?: string + resolution_notes?: string | null + resolved_at?: string | null + resolved_by?: string | null + severity?: string + status?: string + title?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "incidents_correlation_rule_id_fkey" + columns: ["correlation_rule_id"] + isOneToOne: false + referencedRelation: "alert_correlation_rules" + referencedColumns: ["id"] + }, + { + foreignKeyName: "incidents_correlation_rule_id_fkey" + columns: ["correlation_rule_id"] + isOneToOne: false + referencedRelation: "alert_correlations_view" + referencedColumns: ["rule_id"] + }, + ] + } item_change_fields: { Row: { created_at: string | null @@ -5838,6 +5993,25 @@ export type Database = { } } Views: { + alert_correlations_view: { + Row: { + alert_ids: string[] | null + alert_messages: string[] | null + alert_sources: string[] | null + can_create_incident: boolean | null + first_alert_at: string | null + incident_severity: string | null + incident_title_template: string | null + last_alert_at: string | null + matching_alerts_count: number | null + min_alerts_required: number | null + rule_description: string | null + rule_id: string | null + rule_name: string | null + time_window_minutes: number | null + } + Relationships: [] + } error_summary: { Row: { affected_users: number | null @@ -6246,6 +6420,7 @@ export type Database = { } extract_cf_image_id: { Args: { url: string }; Returns: string } generate_deletion_confirmation_code: { Args: never; Returns: string } + generate_incident_number: { Args: never; Returns: string } generate_notification_idempotency_key: { Args: { p_entity_id: string diff --git a/supabase/migrations/20251111015842_46eb673e-b422-4e76-9ac3-b59c90538c01.sql b/supabase/migrations/20251111015842_46eb673e-b422-4e76-9ac3-b59c90538c01.sql new file mode 100644 index 00000000..f0a949f5 --- /dev/null +++ b/supabase/migrations/20251111015842_46eb673e-b422-4e76-9ac3-b59c90538c01.sql @@ -0,0 +1,282 @@ +-- Alert Correlation System: Detect related issues and create incidents + +-- Table: Alert correlation rules (defines which alert patterns indicate incidents) +CREATE TABLE alert_correlation_rules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + rule_name TEXT NOT NULL, + description TEXT, + alert_patterns JSONB NOT NULL, + time_window_minutes INTEGER NOT NULL DEFAULT 15, + min_alerts_required INTEGER NOT NULL DEFAULT 2, + incident_severity TEXT NOT NULL CHECK (incident_severity IN ('critical', 'high', 'medium', 'low')), + incident_title_template TEXT NOT NULL, + incident_description_template TEXT, + auto_create_incident BOOLEAN NOT NULL DEFAULT true, + enabled BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Table: Incidents (auto-generated or manual from correlated alerts) +CREATE TABLE incidents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incident_number TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + description TEXT, + severity TEXT NOT NULL CHECK (severity IN ('critical', 'high', 'medium', 'low')), + status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'investigating', 'resolved', 'closed')), + correlation_rule_id UUID REFERENCES alert_correlation_rules(id), + detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + acknowledged_at TIMESTAMPTZ, + acknowledged_by UUID REFERENCES auth.users(id), + resolved_at TIMESTAMPTZ, + resolved_by UUID REFERENCES auth.users(id), + resolution_notes TEXT, + alert_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Table: Links alerts to incidents +CREATE TABLE incident_alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, + alert_source TEXT NOT NULL CHECK (alert_source IN ('system', 'rate_limit')), + alert_id UUID NOT NULL, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(incident_id, alert_source, alert_id) +); + +-- Generate incident numbers sequentially +CREATE SEQUENCE incident_number_seq START 1000; + +-- Function to generate incident numbers +CREATE OR REPLACE FUNCTION generate_incident_number() +RETURNS TEXT AS $$ +BEGIN + RETURN 'INC-' || LPAD(nextval('incident_number_seq')::TEXT, 6, '0'); +END; +$$ LANGUAGE plpgsql; + +-- Trigger to auto-generate incident numbers +CREATE OR REPLACE FUNCTION set_incident_number() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.incident_number IS NULL THEN + NEW.incident_number := generate_incident_number(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_set_incident_number + BEFORE INSERT ON incidents + FOR EACH ROW + EXECUTE FUNCTION set_incident_number(); + +-- Trigger to update incident alert count +CREATE OR REPLACE FUNCTION update_incident_alert_count() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE incidents + SET alert_count = alert_count + 1, + updated_at = NOW() + WHERE id = NEW.incident_id; + ELSIF TG_OP = 'DELETE' THEN + UPDATE incidents + SET alert_count = alert_count - 1, + updated_at = NOW() + WHERE id = OLD.incident_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_update_incident_alert_count + AFTER INSERT OR DELETE ON incident_alerts + FOR EACH ROW + EXECUTE FUNCTION update_incident_alert_count(); + +-- View: Active correlations (alerts matching correlation patterns) +CREATE OR REPLACE VIEW alert_correlations_view +WITH (security_invoker=on) +AS +WITH recent_alerts AS ( + SELECT + 'system'::TEXT AS source, + id::TEXT AS alert_id, + alert_type, + NULL::TEXT AS metric_type, + severity, + created_at, + message AS alert_message + FROM system_alerts + WHERE created_at > NOW() - INTERVAL '1 hour' + AND resolved_at IS NULL + UNION ALL + SELECT + 'rate_limit'::TEXT AS source, + id::TEXT AS alert_id, + NULL::TEXT AS alert_type, + metric_type, + 'high'::TEXT AS severity, + created_at, + alert_message + FROM rate_limit_alerts + WHERE created_at > NOW() - INTERVAL '1 hour' + AND resolved_at IS NULL +), +matched_alerts AS ( + SELECT + acr.id AS rule_id, + acr.rule_name, + acr.description AS rule_description, + acr.incident_severity, + acr.incident_title_template, + acr.time_window_minutes, + acr.min_alerts_required, + ra.alert_id, + ra.source, + ra.alert_message, + ra.created_at + FROM alert_correlation_rules acr + CROSS JOIN LATERAL ( + SELECT ra.* + FROM recent_alerts ra, + jsonb_array_elements(acr.alert_patterns) AS pattern + WHERE ra.created_at > NOW() - (acr.time_window_minutes || ' minutes')::INTERVAL + AND ( + (pattern->>'source' = ra.source) + AND ( + (pattern->>'alert_type' IS NULL OR pattern->>'alert_type' = ra.alert_type) + OR (pattern->>'metric_type' IS NULL OR pattern->>'metric_type' = ra.metric_type) + ) + AND (pattern->>'severity' IS NULL OR pattern->>'severity' = ra.severity) + ) + ) ra + WHERE acr.enabled = true +) +SELECT + rule_id, + rule_name, + rule_description, + incident_severity, + incident_title_template, + time_window_minutes, + min_alerts_required, + COUNT(DISTINCT alert_id) AS matching_alerts_count, + ARRAY_AGG(DISTINCT alert_id) AS alert_ids, + ARRAY_AGG(DISTINCT source) AS alert_sources, + (ARRAY_AGG(alert_message ORDER BY created_at DESC))[1:5] AS alert_messages, + MIN(created_at) AS first_alert_at, + MAX(created_at) AS last_alert_at, + NOT EXISTS ( + SELECT 1 FROM incidents i + WHERE i.correlation_rule_id = matched_alerts.rule_id + AND i.status IN ('open', 'investigating') + AND i.detected_at > NOW() - (matched_alerts.time_window_minutes || ' minutes')::INTERVAL + ) AS can_create_incident +FROM matched_alerts +GROUP BY rule_id, rule_name, rule_description, incident_severity, + incident_title_template, time_window_minutes, min_alerts_required +HAVING COUNT(DISTINCT alert_id) >= min_alerts_required; + +-- Grant permissions +GRANT SELECT ON alert_correlation_rules TO authenticated; +GRANT SELECT ON incidents TO authenticated; +GRANT SELECT ON incident_alerts TO authenticated; +GRANT SELECT ON alert_correlations_view TO authenticated; + +-- RLS Policies +ALTER TABLE alert_correlation_rules ENABLE ROW LEVEL SECURITY; +ALTER TABLE incidents ENABLE ROW LEVEL SECURITY; +ALTER TABLE incident_alerts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY moderators_manage_correlation_rules ON alert_correlation_rules + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM user_roles + WHERE user_id = auth.uid() + AND role IN ('moderator', 'admin', 'superuser') + ) + ); + +CREATE POLICY moderators_view_incidents ON incidents + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM user_roles + WHERE user_id = auth.uid() + AND role IN ('moderator', 'admin', 'superuser') + ) + ); + +CREATE POLICY moderators_manage_incidents ON incidents + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM user_roles + WHERE user_id = auth.uid() + AND role IN ('moderator', 'admin', 'superuser') + ) + ); + +CREATE POLICY moderators_view_incident_alerts ON incident_alerts + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM user_roles + WHERE user_id = auth.uid() + AND role IN ('moderator', 'admin', 'superuser') + ) + ); + +-- Insert default correlation rules +INSERT INTO alert_correlation_rules (rule_name, description, alert_patterns, time_window_minutes, min_alerts_required, incident_severity, incident_title_template, incident_description_template, enabled) VALUES +('Database Performance Degradation', 'Database issues causing application errors', + '[ + {"source": "system", "alert_type": "database_connection_pool_exhausted"}, + {"source": "system", "alert_type": "high_error_rate"}, + {"source": "rate_limit", "metric_type": "rate_limit_violation"} + ]'::jsonb, + 15, 2, 'critical', + 'Database Performance Incident', + 'Multiple database-related alerts detected indicating potential database performance degradation affecting application availability.', + true), + +('Authentication System Issues', 'Auth failures causing user lockouts', + '[ + {"source": "system", "alert_type": "auth_failure_spike"}, + {"source": "system", "alert_type": "high_error_rate"}, + {"source": "rate_limit", "metric_type": "rate_limit_violation"} + ]'::jsonb, + 10, 2, 'high', + 'Authentication System Incident', + 'Authentication-related alerts detected indicating potential issues with user login and authentication services.', + true), + +('Rate Limiting Cascade', 'Rate limit violations causing service degradation', + '[ + {"source": "rate_limit", "metric_type": "rate_limit_violation"}, + {"source": "system", "alert_type": "high_error_rate"} + ]'::jsonb, + 20, 2, 'high', + 'Rate Limiting Cascade Incident', + 'Multiple rate limit violations detected potentially causing cascading service issues.', + true), + +('Storage System Issues', 'File upload/storage problems', + '[ + {"source": "system", "alert_type": "upload_timeout"}, + {"source": "system", "alert_type": "storage_quota_exceeded"}, + {"source": "system", "alert_type": "high_error_rate"} + ]'::jsonb, + 15, 2, 'medium', + 'Storage System Incident', + 'Storage-related alerts detected indicating potential issues with file uploads or storage capacity.', + true); + +-- Create indexes for performance +CREATE INDEX idx_incidents_status ON incidents(status) WHERE status IN ('open', 'investigating'); +CREATE INDEX idx_incidents_severity ON incidents(severity, detected_at DESC); +CREATE INDEX idx_incident_alerts_incident_id ON incident_alerts(incident_id); +CREATE INDEX idx_alert_correlation_rules_enabled ON alert_correlation_rules(enabled) WHERE enabled = true; \ No newline at end of file