ai_root_shield 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -4
- data/README.md +33 -2
- data/bindings/python/README.md +304 -0
- data/bindings/python/ai_root_shield.py +438 -0
- data/bindings/python/setup.py +65 -0
- data/examples/device_logs/android_safetynet_device.json +148 -0
- data/examples/device_logs/ios_jailbroken_device.json +172 -0
- data/lib/ai_root_shield/ci_cd/security_test_module.rb +743 -0
- data/lib/ai_root_shield/dashboard/web_dashboard.rb +441 -0
- data/lib/ai_root_shield/enterprise/alert_system.rb +601 -0
- data/lib/ai_root_shield/enterprise/hybrid_detection_engine.rb +650 -0
- data/lib/ai_root_shield/enterprise/performance_optimizer.rb +613 -0
- data/lib/ai_root_shield/enterprise/policy_manager.rb +637 -0
- data/lib/ai_root_shield/integrations/siem_connector.rb +695 -0
- data/lib/ai_root_shield/platform/android_security_module.rb +263 -0
- data/lib/ai_root_shield/platform/hardware_security_analyzer.rb +452 -0
- data/lib/ai_root_shield/platform/ios_security_module.rb +513 -0
- data/lib/ai_root_shield/platform/unified_report_generator.rb +613 -0
- data/lib/ai_root_shield/version.rb +1 -1
- data/security_test_artifacts/security_report.json +124 -0
- data/security_test_artifacts/security_results.sarif +16 -0
- data/security_test_artifacts/security_tests.xml +3 -0
- metadata +20 -1
@@ -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
|