ai_root_shield 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,601 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'syslog'
7
+ require 'time'
8
+
9
+ module AiRootShield
10
+ module Enterprise
11
+ # Advanced reporting and alert system with multiple notification channels
12
+ class AlertSystem
13
+ ALERT_LEVELS = %w[info warning critical emergency].freeze
14
+ SUPPORTED_CHANNELS = %w[syslog webhook email slack teams].freeze
15
+
16
+ attr_reader :channels, :alert_history, :status
17
+
18
+ def initialize(config = {})
19
+ @config = default_config.merge(config)
20
+ @channels = {}
21
+ @alert_history = []
22
+ @status = { active: false, alerts_sent: 0 }
23
+ @rate_limiter = {}
24
+
25
+ initialize_channels
26
+ end
27
+
28
+ # Start alert system
29
+ def start
30
+ @status[:active] = true
31
+ @status[:started_at] = Time.now.utc.iso8601
32
+
33
+ # Initialize syslog if configured
34
+ if @config[:syslog][:enabled]
35
+ Syslog.open("ai_root_shield", Syslog::LOG_PID, Syslog::LOG_LOCAL0)
36
+ end
37
+
38
+ log_system_event("Alert system started")
39
+ true
40
+ end
41
+
42
+ # Stop alert system
43
+ def stop
44
+ @status[:active] = false
45
+ @status[:stopped_at] = Time.now.utc.iso8601
46
+
47
+ Syslog.close if @config[:syslog][:enabled] && Syslog.opened?
48
+ log_system_event("Alert system stopped")
49
+ true
50
+ end
51
+
52
+ # Send alert through configured channels
53
+ def send_alert(alert_data)
54
+ return false unless @status[:active]
55
+ return false if rate_limited?(alert_data)
56
+
57
+ alert = prepare_alert(alert_data)
58
+ results = {}
59
+
60
+ @config[:channels].each do |channel_name, channel_config|
61
+ next unless channel_config[:enabled]
62
+
63
+ begin
64
+ result = send_to_channel(channel_name, alert, channel_config)
65
+ results[channel_name] = result
66
+
67
+ log_channel_result(channel_name, result, alert[:id])
68
+ rescue StandardError => e
69
+ results[channel_name] = { success: false, error: e.message }
70
+ log_system_event("Alert channel error", { channel: channel_name, error: e.message })
71
+ end
72
+ end
73
+
74
+ # Store alert in history
75
+ store_alert_history(alert, results)
76
+
77
+ @status[:alerts_sent] += 1
78
+ results
79
+ end
80
+
81
+ # Send security incident alert
82
+ def send_security_alert(analysis_result, severity = 'warning')
83
+ alert_data = {
84
+ type: 'security_incident',
85
+ severity: severity,
86
+ title: "Security Threat Detected",
87
+ message: format_security_message(analysis_result),
88
+ data: analysis_result,
89
+ source: 'ai_root_shield',
90
+ timestamp: Time.now.utc.iso8601
91
+ }
92
+
93
+ send_alert(alert_data)
94
+ end
95
+
96
+ # Send compliance violation alert
97
+ def send_compliance_alert(compliance_result, policy_type)
98
+ severity = compliance_result[:compliant] ? 'info' : 'critical'
99
+
100
+ alert_data = {
101
+ type: 'compliance_violation',
102
+ severity: severity,
103
+ title: "Compliance Status Update",
104
+ message: format_compliance_message(compliance_result, policy_type),
105
+ data: compliance_result,
106
+ source: 'ai_root_shield_compliance',
107
+ timestamp: Time.now.utc.iso8601
108
+ }
109
+
110
+ send_alert(alert_data)
111
+ end
112
+
113
+ # Send system health alert
114
+ def send_health_alert(health_data, severity = 'info')
115
+ alert_data = {
116
+ type: 'system_health',
117
+ severity: severity,
118
+ title: "System Health Update",
119
+ message: format_health_message(health_data),
120
+ data: health_data,
121
+ source: 'ai_root_shield_system',
122
+ timestamp: Time.now.utc.iso8601
123
+ }
124
+
125
+ send_alert(alert_data)
126
+ end
127
+
128
+ # Get alert statistics
129
+ def get_statistics
130
+ {
131
+ total_alerts: @alert_history.length,
132
+ alerts_by_severity: count_by_severity,
133
+ alerts_by_type: count_by_type,
134
+ alerts_by_channel: count_by_channel,
135
+ success_rate: calculate_success_rate,
136
+ recent_alerts: @alert_history.last(10)
137
+ }
138
+ end
139
+
140
+ # Configure alert channel
141
+ def configure_channel(channel_name, config)
142
+ @config[:channels][channel_name.to_sym] = config
143
+ initialize_channel(channel_name, config)
144
+ end
145
+
146
+ private
147
+
148
+ # Default configuration
149
+ def default_config
150
+ {
151
+ rate_limit: {
152
+ enabled: true,
153
+ max_alerts_per_minute: 10,
154
+ max_alerts_per_hour: 100
155
+ },
156
+ syslog: {
157
+ enabled: true,
158
+ facility: Syslog::LOG_LOCAL0,
159
+ level: Syslog::LOG_INFO
160
+ },
161
+ channels: {
162
+ syslog: {
163
+ enabled: true,
164
+ format: 'json',
165
+ include_metadata: true
166
+ },
167
+ webhook: {
168
+ enabled: false,
169
+ url: nil,
170
+ headers: {},
171
+ timeout: 30,
172
+ retry_count: 3
173
+ }
174
+ }
175
+ }
176
+ end
177
+
178
+ # Initialize all configured channels
179
+ def initialize_channels
180
+ @config[:channels].each do |channel_name, channel_config|
181
+ initialize_channel(channel_name, channel_config) if channel_config[:enabled]
182
+ end
183
+ end
184
+
185
+ # Initialize specific channel
186
+ def initialize_channel(channel_name, config)
187
+ case channel_name.to_sym
188
+ when :syslog
189
+ @channels[:syslog] = SyslogChannel.new(config)
190
+ when :webhook
191
+ @channels[:webhook] = WebhookChannel.new(config)
192
+ when :slack
193
+ @channels[:slack] = SlackChannel.new(config)
194
+ when :teams
195
+ @channels[:teams] = TeamsChannel.new(config)
196
+ end
197
+ end
198
+
199
+ # Prepare alert for sending
200
+ def prepare_alert(alert_data)
201
+ {
202
+ id: generate_alert_id,
203
+ timestamp: alert_data[:timestamp] || Time.now.utc.iso8601,
204
+ type: alert_data[:type],
205
+ severity: alert_data[:severity],
206
+ title: alert_data[:title],
207
+ message: alert_data[:message],
208
+ data: alert_data[:data],
209
+ source: alert_data[:source],
210
+ metadata: {
211
+ version: AiRootShield::VERSION,
212
+ hostname: Socket.gethostname,
213
+ process_id: Process.pid
214
+ }
215
+ }
216
+ end
217
+
218
+ # Send alert to specific channel
219
+ def send_to_channel(channel_name, alert, config)
220
+ channel = @channels[channel_name.to_sym]
221
+ return { success: false, error: "Channel not initialized" } unless channel
222
+
223
+ channel.send_alert(alert)
224
+ end
225
+
226
+ # Check if alert is rate limited
227
+ def rate_limited?(alert_data)
228
+ return false unless @config[:rate_limit][:enabled]
229
+
230
+ now = Time.now
231
+ key = "#{alert_data[:type]}_#{alert_data[:severity]}"
232
+
233
+ @rate_limiter[key] ||= { minute: [], hour: [] }
234
+
235
+ # Clean old entries
236
+ @rate_limiter[key][:minute].reject! { |time| now - time > 60 }
237
+ @rate_limiter[key][:hour].reject! { |time| now - time > 3600 }
238
+
239
+ # Check limits
240
+ minute_limit = @config[:rate_limit][:max_alerts_per_minute]
241
+ hour_limit = @config[:rate_limit][:max_alerts_per_hour]
242
+
243
+ if @rate_limiter[key][:minute].length >= minute_limit ||
244
+ @rate_limiter[key][:hour].length >= hour_limit
245
+ return true
246
+ end
247
+
248
+ # Add current time
249
+ @rate_limiter[key][:minute] << now
250
+ @rate_limiter[key][:hour] << now
251
+
252
+ false
253
+ end
254
+
255
+ # Format security alert message
256
+ def format_security_message(analysis_result)
257
+ risk_score = analysis_result[:risk_score] || 0
258
+ factors = analysis_result[:factors] || []
259
+
260
+ message = "Security analysis completed with risk score: #{risk_score}/100"
261
+ message += "\nDetected factors: #{factors.join(', ')}" if factors.any?
262
+ message += "\nRecommendations: #{analysis_result[:recommendations].join(', ')}" if analysis_result[:recommendations]
263
+
264
+ message
265
+ end
266
+
267
+ # Format compliance alert message
268
+ def format_compliance_message(compliance_result, policy_type)
269
+ status = compliance_result[:compliant] ? "COMPLIANT" : "NON-COMPLIANT"
270
+ score = compliance_result[:compliance_score] || 0
271
+
272
+ message = "Compliance check for #{policy_type} policy: #{status} (Score: #{score}/100)"
273
+
274
+ if compliance_result[:violations]&.any?
275
+ message += "\nViolations found:"
276
+ compliance_result[:violations].each do |violation|
277
+ message += "\n- #{violation[:message]} (#{violation[:severity]})"
278
+ end
279
+ end
280
+
281
+ message
282
+ end
283
+
284
+ # Format health alert message
285
+ def format_health_message(health_data)
286
+ "System health check: #{health_data[:status] || 'Unknown'}"
287
+ end
288
+
289
+ # Store alert in history
290
+ def store_alert_history(alert, results)
291
+ history_entry = {
292
+ alert: alert,
293
+ results: results,
294
+ sent_at: Time.now.utc.iso8601,
295
+ success: results.values.all? { |r| r[:success] }
296
+ }
297
+
298
+ @alert_history << history_entry
299
+
300
+ # Keep only last 1000 alerts
301
+ @alert_history = @alert_history.last(1000) if @alert_history.length > 1000
302
+ end
303
+
304
+ # Count alerts by severity
305
+ def count_by_severity
306
+ @alert_history.group_by { |entry| entry[:alert][:severity] }
307
+ .transform_values(&:count)
308
+ end
309
+
310
+ # Count alerts by type
311
+ def count_by_type
312
+ @alert_history.group_by { |entry| entry[:alert][:type] }
313
+ .transform_values(&:count)
314
+ end
315
+
316
+ # Count alerts by channel
317
+ def count_by_channel
318
+ channel_counts = {}
319
+
320
+ @alert_history.each do |entry|
321
+ entry[:results].each do |channel, result|
322
+ channel_counts[channel] ||= 0
323
+ channel_counts[channel] += 1 if result[:success]
324
+ end
325
+ end
326
+
327
+ channel_counts
328
+ end
329
+
330
+ # Calculate success rate
331
+ def calculate_success_rate
332
+ return 100.0 if @alert_history.empty?
333
+
334
+ successful = @alert_history.count { |entry| entry[:success] }
335
+ (successful.to_f / @alert_history.length * 100).round(2)
336
+ end
337
+
338
+ # Generate unique alert ID
339
+ def generate_alert_id
340
+ "ARS-ALERT-#{Time.now.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4).upcase}"
341
+ end
342
+
343
+ # Log system events
344
+ def log_system_event(message, details = {})
345
+ puts "[#{Time.now.utc.iso8601}] AlertSystem: #{message} #{details.to_json}" if @config[:debug]
346
+ end
347
+
348
+ # Log channel results
349
+ def log_channel_result(channel_name, result, alert_id)
350
+ status = result[:success] ? "SUCCESS" : "FAILED"
351
+ log_system_event("Alert sent", {
352
+ channel: channel_name,
353
+ status: status,
354
+ alert_id: alert_id,
355
+ error: result[:error]
356
+ })
357
+ end
358
+ end
359
+
360
+ # Syslog channel implementation
361
+ class SyslogChannel
362
+ def initialize(config)
363
+ @config = config
364
+ end
365
+
366
+ def send_alert(alert)
367
+ return { success: false, error: "Syslog not available" } unless Syslog.opened?
368
+
369
+ priority = map_severity_to_syslog(alert[:severity])
370
+ message = format_syslog_message(alert)
371
+
372
+ Syslog.log(priority, message)
373
+
374
+ { success: true, channel: 'syslog', timestamp: Time.now.utc.iso8601 }
375
+ rescue StandardError => e
376
+ { success: false, error: e.message, channel: 'syslog' }
377
+ end
378
+
379
+ private
380
+
381
+ def map_severity_to_syslog(severity)
382
+ case severity
383
+ when 'emergency'
384
+ Syslog::LOG_EMERG
385
+ when 'critical'
386
+ Syslog::LOG_CRIT
387
+ when 'warning'
388
+ Syslog::LOG_WARNING
389
+ when 'info'
390
+ Syslog::LOG_INFO
391
+ else
392
+ Syslog::LOG_INFO
393
+ end
394
+ end
395
+
396
+ def format_syslog_message(alert)
397
+ if @config[:format] == 'json'
398
+ alert.to_json
399
+ else
400
+ "#{alert[:title]}: #{alert[:message]}"
401
+ end
402
+ end
403
+ end
404
+
405
+ # Webhook channel implementation
406
+ class WebhookChannel
407
+ def initialize(config)
408
+ @config = config
409
+ end
410
+
411
+ def send_alert(alert)
412
+ return { success: false, error: "No webhook URL configured" } unless @config[:url]
413
+
414
+ uri = URI(@config[:url])
415
+ http = Net::HTTP.new(uri.host, uri.port)
416
+ http.use_ssl = uri.scheme == 'https'
417
+ http.read_timeout = @config[:timeout] || 30
418
+
419
+ request = Net::HTTP::Post.new(uri)
420
+ request['Content-Type'] = 'application/json'
421
+
422
+ # Add custom headers
423
+ @config[:headers]&.each do |key, value|
424
+ request[key] = value
425
+ end
426
+
427
+ request.body = format_webhook_payload(alert)
428
+
429
+ response = http.request(request)
430
+
431
+ if response.code.to_i.between?(200, 299)
432
+ { success: true, channel: 'webhook', response_code: response.code }
433
+ else
434
+ { success: false, error: "HTTP #{response.code}: #{response.body}", channel: 'webhook' }
435
+ end
436
+ rescue StandardError => e
437
+ { success: false, error: e.message, channel: 'webhook' }
438
+ end
439
+
440
+ private
441
+
442
+ def format_webhook_payload(alert)
443
+ {
444
+ alert_id: alert[:id],
445
+ timestamp: alert[:timestamp],
446
+ type: alert[:type],
447
+ severity: alert[:severity],
448
+ title: alert[:title],
449
+ message: alert[:message],
450
+ source: alert[:source],
451
+ data: alert[:data],
452
+ metadata: alert[:metadata]
453
+ }.to_json
454
+ end
455
+ end
456
+
457
+ # Slack channel implementation
458
+ class SlackChannel
459
+ def initialize(config)
460
+ @config = config
461
+ end
462
+
463
+ def send_alert(alert)
464
+ return { success: false, error: "No Slack webhook URL configured" } unless @config[:webhook_url]
465
+
466
+ payload = format_slack_payload(alert)
467
+
468
+ uri = URI(@config[:webhook_url])
469
+ http = Net::HTTP.new(uri.host, uri.port)
470
+ http.use_ssl = true
471
+
472
+ request = Net::HTTP::Post.new(uri)
473
+ request['Content-Type'] = 'application/json'
474
+ request.body = payload.to_json
475
+
476
+ response = http.request(request)
477
+
478
+ if response.code == '200'
479
+ { success: true, channel: 'slack' }
480
+ else
481
+ { success: false, error: "Slack API error: #{response.body}", channel: 'slack' }
482
+ end
483
+ rescue StandardError => e
484
+ { success: false, error: e.message, channel: 'slack' }
485
+ end
486
+
487
+ private
488
+
489
+ def format_slack_payload(alert)
490
+ color = case alert[:severity]
491
+ when 'emergency', 'critical'
492
+ 'danger'
493
+ when 'warning'
494
+ 'warning'
495
+ else
496
+ 'good'
497
+ end
498
+
499
+ {
500
+ text: alert[:title],
501
+ attachments: [
502
+ {
503
+ color: color,
504
+ fields: [
505
+ {
506
+ title: "Severity",
507
+ value: alert[:severity].upcase,
508
+ short: true
509
+ },
510
+ {
511
+ title: "Type",
512
+ value: alert[:type],
513
+ short: true
514
+ },
515
+ {
516
+ title: "Message",
517
+ value: alert[:message],
518
+ short: false
519
+ }
520
+ ],
521
+ footer: "AI Root Shield",
522
+ ts: Time.parse(alert[:timestamp]).to_i
523
+ }
524
+ ]
525
+ }
526
+ end
527
+ end
528
+
529
+ # Microsoft Teams channel implementation
530
+ class TeamsChannel
531
+ def initialize(config)
532
+ @config = config
533
+ end
534
+
535
+ def send_alert(alert)
536
+ return { success: false, error: "No Teams webhook URL configured" } unless @config[:webhook_url]
537
+
538
+ payload = format_teams_payload(alert)
539
+
540
+ uri = URI(@config[:webhook_url])
541
+ http = Net::HTTP.new(uri.host, uri.port)
542
+ http.use_ssl = true
543
+
544
+ request = Net::HTTP::Post.new(uri)
545
+ request['Content-Type'] = 'application/json'
546
+ request.body = payload.to_json
547
+
548
+ response = http.request(request)
549
+
550
+ if response.code == '200'
551
+ { success: true, channel: 'teams' }
552
+ else
553
+ { success: false, error: "Teams API error: #{response.body}", channel: 'teams' }
554
+ end
555
+ rescue StandardError => e
556
+ { success: false, error: e.message, channel: 'teams' }
557
+ end
558
+
559
+ private
560
+
561
+ def format_teams_payload(alert)
562
+ theme_color = case alert[:severity]
563
+ when 'emergency', 'critical'
564
+ 'FF0000'
565
+ when 'warning'
566
+ 'FFA500'
567
+ else
568
+ '00FF00'
569
+ end
570
+
571
+ {
572
+ "@type": "MessageCard",
573
+ "@context": "http://schema.org/extensions",
574
+ themeColor: theme_color,
575
+ summary: alert[:title],
576
+ sections: [
577
+ {
578
+ activityTitle: alert[:title],
579
+ activitySubtitle: "AI Root Shield Alert",
580
+ facts: [
581
+ {
582
+ name: "Severity",
583
+ value: alert[:severity].upcase
584
+ },
585
+ {
586
+ name: "Type",
587
+ value: alert[:type]
588
+ },
589
+ {
590
+ name: "Timestamp",
591
+ value: alert[:timestamp]
592
+ }
593
+ ],
594
+ text: alert[:message]
595
+ }
596
+ ]
597
+ }
598
+ end
599
+ end
600
+ end
601
+ end