From 0338734fe782216c128ee745a8bdea90eff187cd Mon Sep 17 00:00:00 2001 From: Neil Lathwood Date: Sat, 13 May 2017 12:46:08 +0100 Subject: [PATCH] feature: Added script (scripts/test-template.php) to test alert templates (#6631) * feature: Added script (scripts/test-template.php) to test alert templates * moved remaining functions, fixed php 5.3 and include dir * added docs on use for test-template script --- alerts.php | 553 ------------------------ doc/Extensions/Alerting.md | 39 +- html/includes/print-alert-templates.php | 4 +- includes/alerts.inc.php | 548 +++++++++++++++++++++++ scripts/test-template.php | 41 ++ 5 files changed, 618 insertions(+), 567 deletions(-) create mode 100755 scripts/test-template.php diff --git a/alerts.php b/alerts.php index 97da4d6e50..453671ffc1 100755 --- a/alerts.php +++ b/alerts.php @@ -61,556 +61,3 @@ if (!defined('TEST') && $config['alert']['disable'] != 'true') { } release_lock('alerts'); - -function ClearStaleAlerts() -{ - $sql = "SELECT `alerts`.`id` AS `alert_id`, `devices`.`hostname` AS `hostname` FROM `alerts` LEFT JOIN `devices` ON `alerts`.`device_id`=`devices`.`device_id` RIGHT JOIN `alert_rules` ON `alerts`.`rule_id`=`alert_rules`.`id` WHERE `alerts`.`state`!=0 AND `devices`.`hostname` IS NULL"; - foreach (dbFetchRows($sql) as $alert) { - if (empty($alert['hostname']) && isset($alert['alert_id'])) { - dbDelete('alerts', '`id` = ?', array($alert['alert_id'])); - echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL; - } - } -} - -/** - * Re-Validate Rule-Mappings - * @param integer $device Device-ID - * @param integer $rule Rule-ID - * @return boolean - */ -function IsRuleValid($device, $rule) -{ - global $rulescache; - if (empty($rulescache[$device]) || !isset($rulescache[$device])) { - foreach (GetRules($device) as $chk) { - $rulescache[$device][$chk['id']] = true; - } - } - - if ($rulescache[$device][$rule] === true) { - return true; - } - - return false; -}//end IsRuleValid() - - -/** - * Issue Alert-Object - * @param array $alert - * @return boolean - */ -function IssueAlert($alert) -{ - global $config; - if (dbFetchCell('SELECT attrib_value FROM devices_attribs WHERE attrib_type = "disable_notify" && device_id = ?', array($alert['device_id'])) == '1') { - return true; - } - - if ($config['alert']['fixed-contacts'] == false) { - if (empty($alert['query'])) { - $alert['query'] = GenSQL($alert['rule']); - } - $sql = $alert['query']; - $qry = dbFetchRows($sql, array($alert['device_id'])); - $alert['details']['contacts'] = GetContacts($qry); - } - - $obj = DescribeAlert($alert); - if (is_array($obj)) { - echo 'Issuing Alert-UID #'.$alert['id'].'/'.$alert['state'].': '; - if (!empty($config['alert']['transports'])) { - ExtTransports($obj); - } - - echo "\r\n"; - } - - return true; -}//end IssueAlert() - - -/** - * Issue ACK notification - * @return void - */ -function RunAcks() -{ - foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state = 2 && alerts.open = 1') as $alert) { - $tmp = array( - $alert['rule_id'], - $alert['device_id'], - ); - $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); - if (empty($alert['rule']) || !IsRuleValid($tmp[1], $tmp[0])) { - // Alert-Rule does not exist anymore, let's remove the alert-state. - echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; - dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); - continue; - } - - $alert['details'] = json_decode(gzuncompress($alert['details']), true); - $alert['state'] = 2; - IssueAlert($alert); - dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } -}//end RunAcks() - - -/** - * Run Follow-Up alerts - * @return void - */ -function RunFollowUp() -{ - global $config; - foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state != 2 && alerts.state > 0 && alerts.open = 0') as $alert) { - $tmp = array( - $alert['rule_id'], - $alert['device_id'], - ); - $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule, alert_rules.query,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); - if (empty($alert['rule']) || !IsRuleValid($tmp[1], $tmp[0])) { - // Alert-Rule does not exist anymore, let's remove the alert-state. - echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; - dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); - continue; - } - - $alert['details'] = json_decode(gzuncompress($alert['details']), true); - $rextra = json_decode($alert['extra'], true); - if ($rextra['invert']) { - continue; - } - - if (empty($alert['query'])) { - $alert['query'] = GenSQL($alert['rule']); - } - $chk = dbFetchRows($alert['query'], array($alert['device_id'])); - //make sure we can json_encode all the datas later - $cnt = count($chk); - for ($i = 0; $i < $cnt; $i++) { - if (isset($chk[$i]['ip'])) { - $chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']); - } - } - $o = sizeof($alert['details']['rule']); - $n = sizeof($chk); - $ret = 'Alert #'.$alert['id']; - $state = 0; - if ($n > $o) { - $ret .= ' Worsens'; - $state = 3; - $alert['details']['diff'] = array_diff($chk, $alert['details']['rule']); - } elseif ($n < $o) { - $ret .= ' Betters'; - $state = 4; - $alert['details']['diff'] = array_diff($alert['details']['rule'], $chk); - } - - if ($state > 0 && $n > 0) { - $alert['details']['rule'] = $chk; - if (dbInsert(array('state' => $state, 'device_id' => $alert['device_id'], 'rule_id' => $alert['rule_id'], 'details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log')) { - dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - - echo $ret.' ('.$o.'/'.$n.")\r\n"; - } - }//end foreach -}//end RunFollowUp() - - -/** - * Run all alerts - * @return void - */ -function RunAlerts() -{ - global $config; - foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state != 2 && alerts.open = 1') as $alert) { - $tmp = array( - $alert['rule_id'], - $alert['device_id'], - ); - $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); - if (empty($alert['rule_id']) || !IsRuleValid($tmp[1], $tmp[0])) { - echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; - // Alert-Rule does not exist anymore, let's remove the alert-state. - dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); - continue; - } - - $alert['details'] = json_decode(gzuncompress($alert['details']), true); - $noiss = false; - $noacc = false; - $updet = false; - $rextra = json_decode($alert['extra'], true); - $chk = dbFetchRow('SELECT alerts.alerted,devices.ignore,devices.disabled FROM alerts,devices WHERE alerts.device_id = ? && devices.device_id = alerts.device_id && alerts.rule_id = ?', array($alert['device_id'], $alert['rule_id'])); - if ($chk['alerted'] == $alert['state']) { - $noiss = true; - } - - if (!empty($rextra['count']) && empty($rextra['interval'])) { - // This check below is for compat-reasons - if (!empty($rextra['delay'])) { - if ((time() - strtotime($alert['time_logged']) + $config['alert']['tolerance_window']) < $rextra['delay'] || (!empty($alert['details']['delay']) && (time() - $alert['details']['delay'] + $config['alert']['tolerance_window']) < $rextra['delay'])) { - continue; - } else { - $alert['details']['delay'] = time(); - $updet = true; - } - } - - if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { - if ($alert['details']['count'] < $rextra['count']) { - $noacc = true; - } - - $updet = true; - $noiss = false; - } - } else { - // This is the new way - if (!empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + $config['alert']['tolerance_window']) < $rextra['delay']) { - continue; - } - - if (!empty($rextra['interval'])) { - if (!empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + $config['alert']['tolerance_window']) < $rextra['interval']) { - continue; - } else { - $alert['details']['interval'] = time(); - $updet = true; - } - } - - if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { - if ($alert['details']['count'] < $rextra['count']) { - $noacc = true; - } - - $updet = true; - $noiss = false; - } - }//end if - if ($chk['ignore'] == 1 || $chk['disabled'] == 1) { - $noiss = true; - $updet = false; - $noacc = false; - } - - if (IsMaintenance($alert['device_id']) > 0) { - $noiss = true; - $noacc = true; - } - - if ($updet) { - dbUpdate(array('details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); - } - - if (!empty($rextra['mute'])) { - echo 'Muted Alert-UID #'.$alert['id']."\r\n"; - $noiss = true; - } - - if (!$noiss) { - IssueAlert($alert); - dbUpdate(array('alerted' => $alert['state']), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - - if (!$noacc) { - dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - }//end foreach -}//end RunAlerts() - - -/** - * Run external transports - * @param array $obj Alert-Array - * @return void - */ -function ExtTransports($obj) -{ - global $config; - $tmp = false; - // To keep scrutinizer from naging because it doesnt understand eval - foreach ($config['alert']['transports'] as $transport => $opts) { - if (is_array($opts)) { - $opts = array_filter($opts); - } - if (($opts === true || !empty($opts)) && $opts != false && file_exists($config['install_dir'].'/includes/alerts/transport.'.$transport.'.php')) { - $obj['transport'] = $transport; - $msg = FormatAlertTpl($obj); - $obj['msg'] = $msg; - echo $transport.' => '; - eval('$tmp = function($obj,$opts) { global $config; '.file_get_contents($config['install_dir'].'/includes/alerts/transport.'.$transport.'.php').' return false; };'); - $tmp = $tmp($obj,$opts); - $prefix = array( 0=>"recovery", 1=>$obj['severity']." alert", 2=>"acknowledgment" ); - $prefix[3] = &$prefix[0]; - $prefix[4] = &$prefix[0]; - if ($tmp === true) { - echo 'OK'; - log_event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 1); - } elseif ($tmp === false) { - echo 'ERROR'; - log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 5); - } else { - echo 'ERROR: '.$tmp."\r\n"; - log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $tmp, $obj['device_id'], null, 5); - } - } - - echo '; '; - } -}//end ExtTransports() - - -/** - * Format Alert - * @param array $obj Alert-Array - * @return string - */ -function FormatAlertTpl($obj) -{ - $tpl = $obj["template"]; - $msg = '$ret .= "'.str_replace(array('{else}', '{/if}', '{/foreach}'), array('"; } else { $ret .= "', '"; } $ret .= "', '"; } $ret .= "'), addslashes($tpl)).'";'; - $parsed = $msg; - $s = strlen($msg); - $x = $pos = -1; - $buff = ''; - $if = $for = $calc = false; - while (++$x < $s) { - if ($msg[$x] == '{' && $buff == '') { - $buff .= $msg[$x]; - } elseif ($buff == '{ ') { - $buff = ''; - } elseif ($buff != '') { - $buff .= $msg[$x]; - } - - if ($buff == '{if') { - $pos = $x; - $if = true; - } elseif ($buff == '{foreach') { - $pos = $x; - $for = true; - } elseif ($buff == '{calc') { - $pos = $x; - $calc = true; - } - - if ($pos != -1 && $msg[$x] == '}') { - $orig = $buff; - $buff = ''; - $pos = -1; - if ($if) { - $if = false; - $o = 3; - $native = array( - '"; if( ', - ' ) { $ret .= "', - ); - } elseif ($for) { - $for = false; - $o = 8; - $native = array( - '"; foreach( ', - ' as $key=>$value) { $ret .= "', - ); - } elseif ($calc) { - $calc = false; - $o = 5; - $native = array( - '"; $ret .= (float) (0+(', - ')); $ret .= "', - ); - } else { - continue; - } - - $cond = trim(populate(substr($orig, $o, -1), false)); - $native = $native[0].$cond.$native[1]; - $parsed = str_replace($orig, $native, $parsed); - unset($cond, $o, $orig, $native); - }//end if - }//end while - - $parsed = populate($parsed); - return RunJail($parsed, $obj); -}//end FormatAlertTpl() - - -/** - * Describe Alert - * @param array $alert Alert-Result from DB - * @return array - */ -function DescribeAlert($alert) -{ - $obj = array(); - $i = 0; - $device = dbFetchRow('SELECT hostname, sysName, location, purpose, notes, uptime FROM devices WHERE device_id = ?', array($alert['device_id'])); - $tpl = dbFetchRow('SELECT `template`,`title`,`title_rec` FROM `alert_templates` JOIN `alert_template_map` ON `alert_template_map`.`alert_templates_id`=`alert_templates`.`id` WHERE `alert_template_map`.`alert_rule_id`=?', array($alert['rule_id'])); - $default_tpl = "%title\r\nSeverity: %severity\r\n{if %state == 0}Time elapsed: %elapsed\r\n{/if}Timestamp: %timestamp\r\nUnique-ID: %uid\r\nRule: {if %name}%name{else}%rule{/if}\r\n{if %faults}Faults:\r\n{foreach %faults} #%key: %value.string\r\n{/foreach}{/if}Alert sent to: {foreach %contacts}%value <%key> {/foreach}"; - $obj['hostname'] = $device['hostname']; - $obj['sysName'] = $device['sysName']; - $obj['location'] = $device['location']; - $obj['uptime'] = $device['uptime']; - $obj['uptime_short'] = formatUptime($device['uptime'], 'short'); - $obj['uptime_long'] = formatUptime($device['uptime']); - $obj['description'] = $device['purpose']; - $obj['notes'] = $device['notes']; - $obj['device_id'] = $alert['device_id']; - $extra = $alert['details']; - if (!isset($tpl['template'])) { - $obj['template'] = $default_tpl; - } else { - $obj['template'] = $tpl['template']; - } - if ($alert['state'] >= 1) { - if (!empty($tpl['title'])) { - $obj['title'] = $tpl['title']; - } else { - $obj['title'] = 'Alert for device '.$device['hostname'].' - '.($alert['name'] ? $alert['name'] : $alert['rule']); - } - if ($alert['state'] == 2) { - $obj['title'] .= ' got acknowledged'; - } elseif ($alert['state'] == 3) { - $obj['title'] .= ' got worse'; - } elseif ($alert['state'] == 4) { - $obj['title'] .= ' got better'; - } - - foreach ($extra['rule'] as $incident) { - $i++; - $obj['faults'][$i] = $incident; - foreach ($incident as $k => $v) { - if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { - $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; - } - } - } - $obj['elapsed'] = TimeFormat(time() - strtotime($alert['time_logged'])); - if (!empty($extra['diff'])) { - $obj['diff'] = $extra['diff']; - } - } elseif ($alert['state'] == 0) { - $id = dbFetchRow('SELECT alert_log.id,alert_log.time_logged,alert_log.details FROM alert_log WHERE alert_log.state != 2 && alert_log.state != 0 && alert_log.rule_id = ? && alert_log.device_id = ? && alert_log.id < ? ORDER BY id DESC LIMIT 1', array($alert['rule_id'], $alert['device_id'], $alert['id'])); - if (empty($id['id'])) { - return false; - } - - $extra = json_decode(gzuncompress($id['details']), true); - if (!empty($tpl['title_rec'])) { - $obj['title'] = $tpl['title_rec']; - } else { - $obj['title'] = 'Device '.$device['hostname'].' recovered from '.($alert['name'] ? $alert['name'] : $alert['rule']); - } - $obj['elapsed'] = TimeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged'])); - $obj['id'] = $id['id']; - foreach ($extra['rule'] as $incident) { - $i++; - $obj['faults'][$i] = $incident; - foreach ($incident as $k => $v) { - if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { - $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; - } - } - } - } else { - return 'Unknown State'; - }//end if - $obj['uid'] = $alert['id']; - $obj['severity'] = $alert['severity']; - $obj['rule'] = $alert['rule']; - $obj['name'] = $alert['name']; - $obj['timestamp'] = $alert['time_logged']; - $obj['contacts'] = $extra['contacts']; - $obj['state'] = $alert['state']; - if (strstr($obj['title'], '%')) { - $obj['title'] = RunJail('$ret = "'.populate(addslashes($obj['title'])).'";', $obj); - } - return $obj; -}//end DescribeAlert() - - -/** - * Format Elapsed Time - * @param integer $secs Seconds elapsed - * @return string - */ -function TimeFormat($secs) -{ - $bit = array( - 'y' => $secs / 31556926 % 12, - 'w' => $secs / 604800 % 52, - 'd' => $secs / 86400 % 7, - 'h' => $secs / 3600 % 24, - 'm' => $secs / 60 % 60, - 's' => $secs % 60, - ); - $ret = array(); - foreach ($bit as $k => $v) { - if ($v > 0) { - $ret[] = $v.$k; - } - } - - if (empty($ret)) { - return 'none'; - } - - return join(' ', $ret); -}//end TimeFormat() - - -/** - * "Safely" run eval - * @param string $code Code to run - * @param array $obj Object with variables - * @return string|mixed - */ -function RunJail($code, $obj) -{ - $ret = ''; - eval($code); - return $ret; -}//end RunJail() - - -/** - * Populate variables - * @param string $txt Text with variables - * @param boolean $wrap Wrap variable for text-usage (default: true) - * @return string - */ -function populate($txt, $wrap = true) -{ - preg_match_all('/%([\w\.]+)/', $txt, $m); - foreach ($m[1] as $tmp) { - $orig = $tmp; - $rep = false; - if ($tmp == 'key' || $tmp == 'value') { - $rep = '$'.$tmp; - } else { - if (strstr($tmp, '.')) { - $tmp = explode('.', $tmp, 2); - $pre = '$'.$tmp[0]; - $tmp = $tmp[1]; - } else { - $pre = '$obj'; - } - - $rep = $pre."['".str_replace('.', "']['", $tmp)."']"; - if ($wrap) { - $rep = '{'.$rep.'}'; - } - } - - $txt = str_replace('%'.$orig, $rep, $txt); - }//end foreach - - return $txt; -}//end populate() diff --git a/doc/Extensions/Alerting.md b/doc/Extensions/Alerting.md index b56bcea438..78fe95c13b 100644 --- a/doc/Extensions/Alerting.md +++ b/doc/Extensions/Alerting.md @@ -4,10 +4,12 @@ Table of Content: - [About](#about) - [Rules](#rules) - [Syntax](#rules-syntax) + - [Options](#extra) - [Examples](#rules-examples) - [Procedure](#rules-procedure) - [Templates](#templates) - [Syntax](#templates-syntax) + - [Testing](#templates-testing) - [Examples](#templates-examples) - [Included](#templates-included) - [Transports](#transports) @@ -46,7 +48,6 @@ Table of Content: - [Time](#macros-time) - [Sensors](#macros-sensors) - [Misc](#macros-misc) -- [Additional Options](#extra) # About @@ -86,6 +87,18 @@ __Glues__ can be either `&&` for `AND` or `||` for `OR`. __Note__: The difference between `Equals` and `Like` (and its negation) is that `Equals` does a strict comparison and `Like` allows the usage of RegExp. Arithmetics are allowed as well. +# Options + +Here are some of the other options available when adding an alerting rule: + +- Rule name: The name associated with the rule. +- Severity: How "important" the rule is. +- Max alerts: The maximum number of alerts sent for the event. `-1` means unlimited. +- Delay: The amount of time in seconds to wait after a rule is matched before sending an alert. +- Interval: The interval of time in seconds between alerts for an event until Max is reached. +- Mute alerts: Disable sending alerts for this rule. +- Invert match: Invert the matching rule (ie. alert on items that _don't_ match the rule). + ## Examples Alert when: @@ -159,6 +172,18 @@ Limit: %value.sensor_limit_low / %value.sensor_limit The Default Template is a 'one-size-fit-all'. We highly recommend defining own templates for your rules to include more specific information. Templates can be matched against several rules. +## Testing + +It's possible to test your new template before assigning it to a rule. To do so you can run `./scripts/est-template.php`. The script will provide the help +info when ran without any paramaters. + +As an example, if you wanted to test template ID 10 against localhost running rule ID 2 then you would run: + +`./scripts/test-template.php -t 10 -d localhost -r 2` + +If the rule is currently alerting for localhost then you will get the full template as expected to see on email, if it's not then you will just see the +template without any fault information. + ## Examples Default Template: @@ -919,15 +944,3 @@ Entity: `%sensors.sensor_class = "current" && %sensors.sensor_descr = "Bank Tota Description: APC PDU over amperage Example: `%macros.pdu_over_amperage_apc = "1"` - -# Additional Options - -Here are some of the other options available when adding an alerting rule: - -- Rule name: The name associated with the rule. -- Severity: How "important" the rule is. -- Max alerts: The maximum number of alerts sent for the event. `-1` means unlimited. -- Delay: The amount of time in seconds to wait after a rule is matched before sending an alert. -- Interval: The interval of time in seconds between alerts for an event until Max is reached. -- Mute alerts: Disable sending alerts for this rule. -- Invert match: Invert the matching rule (ie. alert on items that _don't_ match the rule). diff --git a/html/includes/print-alert-templates.php b/html/includes/print-alert-templates.php index 35f0fe6241..80578e86cf 100644 --- a/html/includes/print-alert-templates.php +++ b/html/includes/print-alert-templates.php @@ -26,11 +26,12 @@ if (isset($_POST['results_amount']) && $_POST['results_amount'] > 0) { echo '
+ - +
# Name Action
'; + '; if ($_SESSION['userlevel'] >= '10') { echo ''; @@ -76,6 +77,7 @@ $full_query = $full_query.$query." LIMIT $start,$results"; foreach (dbFetchRows($full_query, $param) as $template) { echo '
#' . $template['id'] . ' '.$template['name'].' '; if ($_SESSION['userlevel'] >= '10') { diff --git a/includes/alerts.inc.php b/includes/alerts.inc.php index 19fc521cee..12b10a2473 100644 --- a/includes/alerts.inc.php +++ b/includes/alerts.inc.php @@ -312,3 +312,551 @@ function GetContacts($results) return $tmp_contacts; } + + +/** + * Format Alert + * @param array $obj Alert-Array + * @return string + */ +function FormatAlertTpl($obj) +{ + $tpl = $obj["template"]; + $msg = '$ret .= "'.str_replace(array('{else}', '{/if}', '{/foreach}'), array('"; } else { $ret .= "', '"; } $ret .= "', '"; } $ret .= "'), str_replace("'", "\'", $tpl)).'";'; + $parsed = $msg; + $s = strlen($msg); + $x = $pos = -1; + $buff = ''; + $if = $for = $calc = false; + while (++$x < $s) { + if ($msg[$x] == '{' && $buff == '') { + $buff .= $msg[$x]; + } elseif ($buff == '{ ') { + $buff = ''; + } elseif ($buff != '') { + $buff .= $msg[$x]; + } + + if ($buff == '{if') { + $pos = $x; + $if = true; + } elseif ($buff == '{foreach') { + $pos = $x; + $for = true; + } elseif ($buff == '{calc') { + $pos = $x; + $calc = true; + } + + if ($pos != -1 && $msg[$x] == '}') { + $orig = $buff; + $buff = ''; + $pos = -1; + if ($if) { + $if = false; + $o = 3; + $native = array( + '"; if( ', + ' ) { $ret .= "', + ); + } elseif ($for) { + $for = false; + $o = 8; + $native = array( + '"; foreach( ', + ' as $key=>$value) { $ret .= "', + ); + } elseif ($calc) { + $calc = false; + $o = 5; + $native = array( + '"; $ret .= (float) (0+(', + ')); $ret .= "', + ); + } else { + continue; + } + + $cond = trim(populate(substr($orig, $o, -1), false)); + $native = $native[0].$cond.$native[1]; + $parsed = str_replace($orig, $native, $parsed); + unset($cond, $o, $orig, $native); + }//end if + }//end while + $parsed = populate($parsed); + return RunJail($parsed, $obj); +}//end FormatAlertTpl() + +/** + * Populate variables + * @param string $txt Text with variables + * @param boolean $wrap Wrap variable for text-usage (default: true) + * @return string + */ +function populate($txt, $wrap = true) +{ + preg_match_all('/%([\w\.]+)/', $txt, $m); + foreach ($m[1] as $tmp) { + $orig = $tmp; + $rep = false; + if ($tmp == 'key' || $tmp == 'value') { + $rep = '$'.$tmp; + } else { + if (strstr($tmp, '.')) { + $tmp = explode('.', $tmp, 2); + $pre = '$'.$tmp[0]; + $tmp = $tmp[1]; + } else { + $pre = '$obj'; + } + + $rep = $pre."['".str_replace('.', "']['", $tmp)."']"; + if ($wrap) { + $rep = '{'.$rep.'}'; + } + } + + $txt = str_replace('%'.$orig, $rep, $txt); + }//end foreach + return $txt; +}//end populate() + +/** + * "Safely" run eval + * @param string $code Code to run + * @param array $obj Object with variables + * @return string|mixed + */ +function RunJail($code, $obj) +{ + $ret = ''; + eval($code); + return $ret; +}//end RunJail() + + +/** + * Describe Alert + * @param array $alert Alert-Result from DB + * @return array|boolean + */ +function DescribeAlert($alert) +{ + $obj = array(); + $i = 0; + $device = dbFetchRow('SELECT hostname, sysName, location, purpose, notes, uptime FROM devices WHERE device_id = ?', array($alert['device_id'])); + $tpl = dbFetchRow('SELECT `template`,`title`,`title_rec` FROM `alert_templates` JOIN `alert_template_map` ON `alert_template_map`.`alert_templates_id`=`alert_templates`.`id` WHERE `alert_template_map`.`alert_rule_id`=?', array($alert['rule_id'])); + $default_tpl = "%title\r\nSeverity: %severity\r\n{if %state == 0}Time elapsed: %elapsed\r\n{/if}Timestamp: %timestamp\r\nUnique-ID: %uid\r\nRule: {if %name}%name{else}%rule{/if}\r\n{if %faults}Faults:\r\n{foreach %faults} #%key: %value.string\r\n{/foreach}{/if}Alert sent to: {foreach %contacts}%value <%key> {/foreach}"; + $obj['hostname'] = $device['hostname']; + $obj['sysName'] = $device['sysName']; + $obj['location'] = $device['location']; + $obj['uptime'] = $device['uptime']; + $obj['uptime_short'] = formatUptime($device['uptime'], 'short'); + $obj['uptime_long'] = formatUptime($device['uptime']); + $obj['description'] = $device['purpose']; + $obj['notes'] = $device['notes']; + $obj['device_id'] = $alert['device_id']; + $extra = $alert['details']; + if (!isset($tpl['template'])) { + $obj['template'] = $default_tpl; + } else { + $obj['template'] = $tpl['template']; + } + if ($alert['state'] >= 1) { + if (!empty($tpl['title'])) { + $obj['title'] = $tpl['title']; + } else { + $obj['title'] = 'Alert for device '.$device['hostname'].' - '.($alert['name'] ? $alert['name'] : $alert['rule']); + } + if ($alert['state'] == 2) { + $obj['title'] .= ' got acknowledged'; + } elseif ($alert['state'] == 3) { + $obj['title'] .= ' got worse'; + } elseif ($alert['state'] == 4) { + $obj['title'] .= ' got better'; + } + + foreach ($extra['rule'] as $incident) { + $i++; + $obj['faults'][$i] = $incident; + foreach ($incident as $k => $v) { + if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { + $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; + } + } + } + $obj['elapsed'] = TimeFormat(time() - strtotime($alert['time_logged'])); + if (!empty($extra['diff'])) { + $obj['diff'] = $extra['diff']; + } + } elseif ($alert['state'] == 0) { + $id = dbFetchRow('SELECT alert_log.id,alert_log.time_logged,alert_log.details FROM alert_log WHERE alert_log.state != 2 && alert_log.state != 0 && alert_log.rule_id = ? && alert_log.device_id = ? && alert_log.id < ? ORDER BY id DESC LIMIT 1', array($alert['rule_id'], $alert['device_id'], $alert['id'])); + if (empty($id['id'])) { + return false; + } + + $extra = json_decode(gzuncompress($id['details']), true); + if (!empty($tpl['title_rec'])) { + $obj['title'] = $tpl['title_rec']; + } else { + $obj['title'] = 'Device '.$device['hostname'].' recovered from '.($alert['name'] ? $alert['name'] : $alert['rule']); + } + $obj['elapsed'] = TimeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged'])); + $obj['id'] = $id['id']; + foreach ($extra['rule'] as $incident) { + $i++; + $obj['faults'][$i] = $incident; + foreach ($incident as $k => $v) { + if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { + $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; + } + } + } + } else { + return 'Unknown State'; + }//end if + $obj['uid'] = $alert['id']; + $obj['severity'] = $alert['severity']; + $obj['rule'] = $alert['rule']; + $obj['name'] = $alert['name']; + $obj['timestamp'] = $alert['time_logged']; + $obj['contacts'] = $extra['contacts']; + $obj['state'] = $alert['state']; + if (strstr($obj['title'], '%')) { + $obj['title'] = RunJail('$ret = "'.populate(addslashes($obj['title'])).'";', $obj); + } + return $obj; +}//end DescribeAlert() + +/** + * Format Elapsed Time + * @param integer $secs Seconds elapsed + * @return string + */ +function TimeFormat($secs) +{ + $bit = array( + 'y' => $secs / 31556926 % 12, + 'w' => $secs / 604800 % 52, + 'd' => $secs / 86400 % 7, + 'h' => $secs / 3600 % 24, + 'm' => $secs / 60 % 60, + 's' => $secs % 60, + ); + $ret = array(); + foreach ($bit as $k => $v) { + if ($v > 0) { + $ret[] = $v.$k; + } + } + + if (empty($ret)) { + return 'none'; + } + + return join(' ', $ret); +}//end TimeFormat() + + +function ClearStaleAlerts() +{ + $sql = "SELECT `alerts`.`id` AS `alert_id`, `devices`.`hostname` AS `hostname` FROM `alerts` LEFT JOIN `devices` ON `alerts`.`device_id`=`devices`.`device_id` RIGHT JOIN `alert_rules` ON `alerts`.`rule_id`=`alert_rules`.`id` WHERE `alerts`.`state`!=0 AND `devices`.`hostname` IS NULL"; + foreach (dbFetchRows($sql) as $alert) { + if (empty($alert['hostname']) && isset($alert['alert_id'])) { + dbDelete('alerts', '`id` = ?', array($alert['alert_id'])); + echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL; + } + } +} + +/** + * Re-Validate Rule-Mappings + * @param integer $device Device-ID + * @param integer $rule Rule-ID + * @return boolean + */ +function IsRuleValid($device, $rule) +{ + global $rulescache; + if (empty($rulescache[$device]) || !isset($rulescache[$device])) { + foreach (GetRules($device) as $chk) { + $rulescache[$device][$chk['id']] = true; + } + } + + if ($rulescache[$device][$rule] === true) { + return true; + } + + return false; +}//end IsRuleValid() + + +/** + * Issue Alert-Object + * @param array $alert + * @return boolean + */ +function IssueAlert($alert) +{ + global $config; + if (dbFetchCell('SELECT attrib_value FROM devices_attribs WHERE attrib_type = "disable_notify" && device_id = ?', array($alert['device_id'])) == '1') { + return true; + } + + if ($config['alert']['fixed-contacts'] == false) { + if (empty($alert['query'])) { + $alert['query'] = GenSQL($alert['rule']); + } + $sql = $alert['query']; + $qry = dbFetchRows($sql, array($alert['device_id'])); + $alert['details']['contacts'] = GetContacts($qry); + } + + $obj = DescribeAlert($alert); + if (is_array($obj)) { + echo 'Issuing Alert-UID #'.$alert['id'].'/'.$alert['state'].': '; + if (!empty($config['alert']['transports'])) { + ExtTransports($obj); + } + + echo "\r\n"; + } + + return true; +}//end IssueAlert() + + +/** + * Issue ACK notification + * @return void + */ +function RunAcks() +{ + foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state = 2 && alerts.open = 1') as $alert) { + $tmp = array( + $alert['rule_id'], + $alert['device_id'], + ); + $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); + if (empty($alert['rule']) || !IsRuleValid($tmp[1], $tmp[0])) { + // Alert-Rule does not exist anymore, let's remove the alert-state. + echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; + dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); + continue; + } + + $alert['details'] = json_decode(gzuncompress($alert['details']), true); + $alert['state'] = 2; + IssueAlert($alert); + dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } +}//end RunAcks() + + +/** + * Run Follow-Up alerts + * @return void + */ +function RunFollowUp() +{ + global $config; + foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state != 2 && alerts.state > 0 && alerts.open = 0') as $alert) { + $tmp = array( + $alert['rule_id'], + $alert['device_id'], + ); + $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule, alert_rules.query,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); + if (empty($alert['rule']) || !IsRuleValid($tmp[1], $tmp[0])) { + // Alert-Rule does not exist anymore, let's remove the alert-state. + echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; + dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); + continue; + } + + $alert['details'] = json_decode(gzuncompress($alert['details']), true); + $rextra = json_decode($alert['extra'], true); + if ($rextra['invert']) { + continue; + } + + if (empty($alert['query'])) { + $alert['query'] = GenSQL($alert['rule']); + } + $chk = dbFetchRows($alert['query'], array($alert['device_id'])); + //make sure we can json_encode all the datas later + $cnt = count($chk); + for ($i = 0; $i < $cnt; $i++) { + if (isset($chk[$i]['ip'])) { + $chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']); + } + } + $o = sizeof($alert['details']['rule']); + $n = sizeof($chk); + $ret = 'Alert #'.$alert['id']; + $state = 0; + if ($n > $o) { + $ret .= ' Worsens'; + $state = 3; + $alert['details']['diff'] = array_diff($chk, $alert['details']['rule']); + } elseif ($n < $o) { + $ret .= ' Betters'; + $state = 4; + $alert['details']['diff'] = array_diff($alert['details']['rule'], $chk); + } + + if ($state > 0 && $n > 0) { + $alert['details']['rule'] = $chk; + if (dbInsert(array('state' => $state, 'device_id' => $alert['device_id'], 'rule_id' => $alert['rule_id'], 'details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log')) { + dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + + echo $ret.' ('.$o.'/'.$n.")\r\n"; + } + }//end foreach +}//end RunFollowUp() + + +/** + * Run all alerts + * @return void + */ +function RunAlerts() +{ + global $config; + foreach (dbFetchRows('SELECT alerts.device_id, alerts.rule_id, alerts.state FROM alerts WHERE alerts.state != 2 && alerts.open = 1') as $alert) { + $tmp = array( + $alert['rule_id'], + $alert['device_id'], + ); + $alert = dbFetchRow('SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($alert['device_id'], $alert['rule_id'])); + if (empty($alert['rule_id']) || !IsRuleValid($tmp[1], $tmp[0])) { + echo 'Stale-Rule: #'.$tmp[0].'/'.$tmp[1]."\r\n"; + // Alert-Rule does not exist anymore, let's remove the alert-state. + dbDelete('alerts', 'rule_id = ? && device_id = ?', array($tmp[0], $tmp[1])); + continue; + } + + $alert['details'] = json_decode(gzuncompress($alert['details']), true); + $noiss = false; + $noacc = false; + $updet = false; + $rextra = json_decode($alert['extra'], true); + $chk = dbFetchRow('SELECT alerts.alerted,devices.ignore,devices.disabled FROM alerts,devices WHERE alerts.device_id = ? && devices.device_id = alerts.device_id && alerts.rule_id = ?', array($alert['device_id'], $alert['rule_id'])); + if ($chk['alerted'] == $alert['state']) { + $noiss = true; + } + + if (!empty($rextra['count']) && empty($rextra['interval'])) { + // This check below is for compat-reasons + if (!empty($rextra['delay'])) { + if ((time() - strtotime($alert['time_logged']) + $config['alert']['tolerance_window']) < $rextra['delay'] || (!empty($alert['details']['delay']) && (time() - $alert['details']['delay'] + $config['alert']['tolerance_window']) < $rextra['delay'])) { + continue; + } else { + $alert['details']['delay'] = time(); + $updet = true; + } + } + + if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { + if ($alert['details']['count'] < $rextra['count']) { + $noacc = true; + } + + $updet = true; + $noiss = false; + } + } else { + // This is the new way + if (!empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + $config['alert']['tolerance_window']) < $rextra['delay']) { + continue; + } + + if (!empty($rextra['interval'])) { + if (!empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + $config['alert']['tolerance_window']) < $rextra['interval']) { + continue; + } else { + $alert['details']['interval'] = time(); + $updet = true; + } + } + + if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { + if ($alert['details']['count'] < $rextra['count']) { + $noacc = true; + } + + $updet = true; + $noiss = false; + } + }//end if + if ($chk['ignore'] == 1 || $chk['disabled'] == 1) { + $noiss = true; + $updet = false; + $noacc = false; + } + + if (IsMaintenance($alert['device_id']) > 0) { + $noiss = true; + $noacc = true; + } + + if ($updet) { + dbUpdate(array('details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); + } + + if (!empty($rextra['mute'])) { + echo 'Muted Alert-UID #'.$alert['id']."\r\n"; + $noiss = true; + } + + if (!$noiss) { + IssueAlert($alert); + dbUpdate(array('alerted' => $alert['state']), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + + if (!$noacc) { + dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + }//end foreach +}//end RunAlerts() + + +/** + * Run external transports + * @param array $obj Alert-Array + * @return void + */ +function ExtTransports($obj) +{ + global $config; + $tmp = false; + // To keep scrutinizer from naging because it doesnt understand eval + foreach ($config['alert']['transports'] as $transport => $opts) { + if (is_array($opts)) { + $opts = array_filter($opts); + } + if (($opts === true || !empty($opts)) && $opts != false && file_exists($config['install_dir'].'/includes/alerts/transport.'.$transport.'.php')) { + $obj['transport'] = $transport; + $msg = FormatAlertTpl($obj); + $obj['msg'] = $msg; + echo $transport.' => '; + eval('$tmp = function($obj,$opts) { global $config; '.file_get_contents($config['install_dir'].'/includes/alerts/transport.'.$transport.'.php').' return false; };'); + $tmp = $tmp($obj,$opts); + $prefix = array( 0=>"recovery", 1=>$obj['severity']." alert", 2=>"acknowledgment" ); + $prefix[3] = &$prefix[0]; + $prefix[4] = &$prefix[0]; + if ($tmp === true) { + echo 'OK'; + log_event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 1); + } elseif ($tmp === false) { + echo 'ERROR'; + log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 5); + } else { + echo "ERROR: $tmp\r\n"; + log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $tmp, $obj['device_id'], null, 5); + } + } + echo '; '; + } +}//end ExtTransports() diff --git a/scripts/test-template.php b/scripts/test-template.php new file mode 100755 index 0000000000..68ea33b9fb --- /dev/null +++ b/scripts/test-template.php @@ -0,0 +1,41 @@ +#!/usr/bin/env php +