@@ -6,6 +6,7 @@ use anyhow::{Context, Result};
66use lettre:: message:: { Mailbox , MultiPart , SinglePart } ;
77use lettre:: transport:: smtp:: authentication:: Credentials ;
88use lettre:: { AsyncSmtpTransport , AsyncTransport , Message , Tokio1Executor } ;
9+ use std:: env;
910
1011use crate :: alerting:: alert:: { Alert , AlertSeverity } ;
1112
@@ -35,6 +36,30 @@ impl NotificationConfig {
3536 }
3637 }
3738
39+ /// Build notification config from environment variables.
40+ pub fn from_env ( ) -> Self {
41+ Self {
42+ slack_webhook : env:: var ( "STACKDOG_SLACK_WEBHOOK_URL" ) . ok ( ) ,
43+ smtp_host : env:: var ( "STACKDOG_SMTP_HOST" ) . ok ( ) ,
44+ smtp_port : env:: var ( "STACKDOG_SMTP_PORT" )
45+ . ok ( )
46+ . and_then ( |value| value. parse ( ) . ok ( ) ) ,
47+ smtp_user : env:: var ( "STACKDOG_SMTP_USER" ) . ok ( ) ,
48+ smtp_password : env:: var ( "STACKDOG_SMTP_PASSWORD" ) . ok ( ) ,
49+ webhook_url : env:: var ( "STACKDOG_WEBHOOK_URL" ) . ok ( ) ,
50+ email_recipients : env:: var ( "STACKDOG_EMAIL_RECIPIENTS" )
51+ . ok ( )
52+ . map ( |recipients| {
53+ recipients
54+ . split ( ',' )
55+ . map ( |recipient| recipient. trim ( ) . to_string ( ) )
56+ . filter ( |recipient| !recipient. is_empty ( ) )
57+ . collect ( )
58+ } )
59+ . unwrap_or_default ( ) ,
60+ }
61+ }
62+
3863 /// Set Slack webhook
3964 pub fn with_slack_webhook ( mut self , url : String ) -> Self {
4065 self . slack_webhook = Some ( url) ;
@@ -139,6 +164,17 @@ impl NotificationConfig {
139164 }
140165}
141166
167+ pub fn env_flag_enabled ( name : & str , default : bool ) -> bool {
168+ env:: var ( name)
169+ . ok ( )
170+ . and_then ( |value| match value. trim ( ) . to_ascii_lowercase ( ) . as_str ( ) {
171+ "1" | "true" | "yes" | "on" => Some ( true ) ,
172+ "0" | "false" | "no" | "off" => Some ( false ) ,
173+ _ => None ,
174+ } )
175+ . unwrap_or ( default)
176+ }
177+
142178/// Notification channel
143179#[ derive( Debug , Clone , PartialEq , Eq ) ]
144180pub enum NotificationChannel {
@@ -329,6 +365,41 @@ impl NotificationChannel {
329365 }
330366}
331367
368+ pub async fn dispatch_stored_alert (
369+ alert : & crate :: database:: models:: Alert ,
370+ config : & NotificationConfig ,
371+ ) -> Result < usize > {
372+ let mut runtime_alert = Alert :: new ( alert. alert_type , alert. severity , alert. message . clone ( ) ) ;
373+
374+ if let Some ( metadata) = & alert. metadata {
375+ if let Some ( container_id) = & metadata. container_id {
376+ runtime_alert. add_metadata ( "container_id" . into ( ) , container_id. clone ( ) ) ;
377+ }
378+ if let Some ( source) = & metadata. source {
379+ runtime_alert. add_metadata ( "source" . into ( ) , source. clone ( ) ) ;
380+ }
381+ if let Some ( reason) = & metadata. reason {
382+ runtime_alert. add_metadata ( "reason" . into ( ) , reason. clone ( ) ) ;
383+ }
384+ for ( key, value) in & metadata. extra {
385+ runtime_alert. add_metadata ( key. clone ( ) , value. clone ( ) ) ;
386+ }
387+ }
388+
389+ let channels = config. configured_channels_for_severity ( alert. severity ) ;
390+ let mut sent = 0 ;
391+ for channel in & channels {
392+ match channel. send ( & runtime_alert, config) . await ? {
393+ NotificationResult :: Success ( _) => sent += 1 ,
394+ NotificationResult :: Failure ( message) => {
395+ log:: warn!( "Action notification channel reported failure: {}" , message)
396+ }
397+ }
398+ }
399+
400+ Ok ( sent)
401+ }
402+
332403/// Notification result
333404#[ derive( Debug , Clone ) ]
334405pub enum NotificationResult {
@@ -443,6 +514,21 @@ fn build_email_html(alert: &Alert) -> String {
443514#[ cfg( test) ]
444515mod tests {
445516 use super :: * ;
517+ use std:: sync:: Mutex ;
518+
519+ static ENV_MUTEX : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
520+
521+ fn clear_notification_env ( ) {
522+ env:: remove_var ( "STACKDOG_SLACK_WEBHOOK_URL" ) ;
523+ env:: remove_var ( "STACKDOG_WEBHOOK_URL" ) ;
524+ env:: remove_var ( "STACKDOG_SMTP_HOST" ) ;
525+ env:: remove_var ( "STACKDOG_SMTP_PORT" ) ;
526+ env:: remove_var ( "STACKDOG_SMTP_USER" ) ;
527+ env:: remove_var ( "STACKDOG_SMTP_PASSWORD" ) ;
528+ env:: remove_var ( "STACKDOG_EMAIL_RECIPIENTS" ) ;
529+ env:: remove_var ( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" ) ;
530+ env:: remove_var ( "STACKDOG_NOTIFY_QUARANTINE_ACTIONS" ) ;
531+ }
446532
447533 #[ tokio:: test]
448534 async fn test_console_notification ( ) {
@@ -457,6 +543,61 @@ mod tests {
457543 assert ! ( result. is_ok( ) ) ;
458544 }
459545
546+ #[ test]
547+ fn test_notification_config_from_env_reads_channels ( ) {
548+ let _guard = ENV_MUTEX . lock ( ) . unwrap ( ) ;
549+ clear_notification_env ( ) ;
550+
551+ env:: set_var (
552+ "STACKDOG_SLACK_WEBHOOK_URL" ,
553+ "https://hooks.slack.test/services/1" ,
554+ ) ;
555+ env:: set_var ( "STACKDOG_WEBHOOK_URL" , "https://example.test/webhook" ) ;
556+ env:: set_var ( "STACKDOG_SMTP_HOST" , "smtp.example.com" ) ;
557+ env:: set_var ( "STACKDOG_SMTP_PORT" , "2525" ) ;
558+ env:: set_var ( "STACKDOG_SMTP_USER" , "alerts@example.com" ) ;
559+ env:: set_var ( "STACKDOG_SMTP_PASSWORD" , "secret" ) ;
560+ env:: set_var (
561+ "STACKDOG_EMAIL_RECIPIENTS" ,
562+ "soc@example.com,oncall@example.com" ,
563+ ) ;
564+
565+ let config = NotificationConfig :: from_env ( ) ;
566+
567+ assert_eq ! (
568+ config. slack_webhook( ) ,
569+ Some ( "https://hooks.slack.test/services/1" )
570+ ) ;
571+ assert_eq ! ( config. webhook_url( ) , Some ( "https://example.test/webhook" ) ) ;
572+ assert_eq ! ( config. smtp_host( ) , Some ( "smtp.example.com" ) ) ;
573+ assert_eq ! ( config. smtp_port( ) , Some ( 2525 ) ) ;
574+ assert_eq ! ( config. smtp_user( ) , Some ( "alerts@example.com" ) ) ;
575+ assert_eq ! ( config. smtp_password( ) , Some ( "secret" ) ) ;
576+ assert_eq ! (
577+ config. email_recipients( ) ,
578+ & [
579+ "soc@example.com" . to_string( ) ,
580+ "oncall@example.com" . to_string( )
581+ ]
582+ ) ;
583+
584+ clear_notification_env ( ) ;
585+ }
586+
587+ #[ test]
588+ fn test_env_flag_enabled_honors_boolean_values ( ) {
589+ let _guard = ENV_MUTEX . lock ( ) . unwrap ( ) ;
590+ clear_notification_env ( ) ;
591+
592+ assert ! ( env_flag_enabled( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" , true ) ) ;
593+ env:: set_var ( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" , "false" ) ;
594+ assert ! ( !env_flag_enabled( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" , true ) ) ;
595+ env:: set_var ( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" , "yes" ) ;
596+ assert ! ( env_flag_enabled( "STACKDOG_NOTIFY_IP_BAN_ACTIONS" , false ) ) ;
597+
598+ clear_notification_env ( ) ;
599+ }
600+
460601 #[ test]
461602 fn test_severity_to_slack_color ( ) {
462603 assert_eq ! ( severity_to_slack_color( AlertSeverity :: Critical ) , "#FF0000" ) ;
0 commit comments