Passed
Push — master ( 0baeb0...d4017c )
by Tony
19:18 queued 08:55
created

includes/alerts.inc.php (2 issues)

1
<?php
2
/* Copyright (C) 2014 Daniel Preussker <[email protected]>
3
 * This program is free software: you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation, either version 3 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. */
15
16
/*
17
 * Alerts Tracking
18
 * @author Daniel Preussker <[email protected]>
19
 * @copyright 2014 f0o, LibreNMS
20
 * @license GPL
21
 * @package LibreNMS
22
 * @subpackage Alerts
23
 */
24
25
use App\Models\DevicePerf;
26
use LibreNMS\Alert\Template;
27
use LibreNMS\Alert\AlertData;
28
use LibreNMS\Alerting\QueryBuilderParser;
29
use LibreNMS\Authentication\LegacyAuth;
30
use LibreNMS\Alert\AlertUtil;
31
use LibreNMS\Config;
32
use PHPMailer\PHPMailer\PHPMailer;
33
use LibreNMS\Util\Time;
34
35
/**
36
 * @param $rule
37
 * @param $query_builder
38
 * @return bool|string
39
 */
40
function GenSQL($rule, $query_builder = false)
41
{
42
    if ($query_builder) {
43
        return QueryBuilderParser::fromJson($query_builder)->toSql();
44
    } else {
45
        return GenSQLOld($rule);
46
    }
47
}
48
49
/**
50
 * Generate SQL from Rule
51
 * @param string $rule Rule to generate SQL for
52
 * @return string|boolean
53
 */
54
function GenSQLOld($rule)
55
{
56
    $rule = RunMacros($rule);
57
    if (empty($rule)) {
58
        //Cannot resolve Macros due to recursion. Rule is invalid.
59
        return false;
60
    }
61
    //Pretty-print rule to dissect easier
62
    $pretty = array('&&' => ' && ', '||' => ' || ');
63
    $rule = str_replace(array_keys($pretty), $pretty, $rule);
64
    $tmp = explode(" ", $rule);
65
    $tables = array();
66
    foreach ($tmp as $opt) {
67
        if (strstr($opt, '%') && strstr($opt, '.')) {
68
            $tmpp = explode(".", $opt, 2);
69
            $tmpp[0] = str_replace("%", "", $tmpp[0]);
70
            $tables[] = mres(str_replace("(", "", $tmpp[0]));
71
            $rule = str_replace($opt, $tmpp[0].'.'.$tmpp[1], $rule);
72
        }
73
    }
74
    $tables = array_keys(array_flip($tables));
75
    if (dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', array($tables[0],'device_id')) != 1) {
76
        //Our first table has no valid glue, append the 'devices' table to it!
77
        array_unshift($tables, 'devices');
78
    }
79
    $x = sizeof($tables)-1;
80
    $i = 0;
81
    $join = "";
82
    while ($i < $x) {
83
        if (isset($tables[$i+1])) {
84
            $gtmp = ResolveGlues(array($tables[$i+1]), 'device_id');
85
            if ($gtmp === false) {
86
                //Cannot resolve glue-chain. Rule is invalid.
87
                return false;
88
            }
89
            $last = "";
90
            $qry = "";
91
            foreach ($gtmp as $glue) {
92
                if (empty($last)) {
93
                    list($tmp,$last) = explode('.', $glue);
94
                    $qry .= $glue.' = ';
95
                } else {
96
                    list($tmp,$new) = explode('.', $glue);
97
                    $qry .= $tmp.'.'.$last.' && '.$tmp.'.'.$new.' = ';
98
                    $last = $new;
99
                }
100
                if (!in_array($tmp, $tables)) {
101
                    $tables[] = $tmp;
102
                }
103
            }
104
            $join .= "( ".$qry.$tables[0].".device_id ) && ";
105
        }
106
        $i++;
107
    }
108
    $sql = "SELECT * FROM ".implode(",", $tables)." WHERE (".$join."".str_replace("(", "", $tables[0]).".device_id = ?) && (".str_replace(array("%","@","!~","~"), array("",".*","NOT REGEXP","REGEXP"), $rule).")";
109
    return $sql;
110
}
111
112
/**
113
 * Process Macros
114
 * @param string $rule Rule to process
115
 * @param int $x Recursion-Anchor
116
 * @return string|boolean
117
 */
118
function RunMacros($rule, $x = 1)
119
{
120
    global $config;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
121
    krsort($config['alert']['macros']['rule']);
122
    foreach ($config['alert']['macros']['rule'] as $macro => $value) {
123
        if (!strstr($macro, " ")) {
124
            $rule = str_replace('%macros.'.$macro, '('.$value.')', $rule);
125
        }
126
    }
127
    if (strstr($rule, "%macros.")) {
128
        if (++$x < 30) {
129
            $rule = RunMacros($rule, $x);
130
        } else {
131
            return false;
132
        }
133
    }
134
    return $rule;
135
}
136
137
/**
138
 * Get Alert-Rules for Devices
139
 * @param int $device_id Device-ID
140
 * @return array
141
 */
142
function GetRules($device_id)
143
{
144
    $query = "SELECT DISTINCT a.* FROM alert_rules a
145
  LEFT JOIN alert_device_map d ON a.id=d.rule_id
146
  LEFT JOIN alert_group_map g ON a.id=g.rule_id
147
  LEFT JOIN device_group_device dg ON g.group_id=dg.device_group_id
148
  WHERE a.disabled = 0 AND ((d.device_id IS NULL AND g.group_id IS NULL) OR d.device_id=? OR dg.device_id=?)";
149
150
    $params = [$device_id, $device_id];
151
    return dbFetchRows($query, $params);
152
}
153
154
/**
155
 * Check if device is under maintenance
156
 * @param int $device_id Device-ID
157
 * @return bool
158
 */
159
function IsMaintenance($device_id)
160
{
161
    $device = \App\Models\Device::find($device_id);
162
    return !is_null($device) && $device->isUnderMaintenance();
163
}
164
/**
165
 * Run all rules for a device
166
 * @param int $device_id Device-ID
167
 * @return void
168
 */
169
function RunRules($device_id)
170
{
171
    if (IsMaintenance($device_id) > 0) {
172
        echo "Under Maintenance, Skipping alerts.\r\n";
173
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type void.
Loading history...
174
    }
175
    foreach (GetRules($device_id) as $rule) {
176
        c_echo('Rule %p#'.$rule['id'].' (' . $rule['name'] . '):%n ');
177
        $extra = json_decode($rule['extra'], true);
178
        if (isset($extra['invert'])) {
179
            $inv = (bool) $extra['invert'];
180
        } else {
181
            $inv = false;
182
        }
183
        d_echo(PHP_EOL);
184
        if (empty($rule['query'])) {
185
            $rule['query'] = GenSQL($rule['rule'], $rule['builder']);
186
        }
187
        $sql = $rule['query'];
188
        $qry = dbFetchRows($sql, array($device_id));
189
        $cnt = count($qry);
190
        for ($i = 0; $i < $cnt; $i++) {
191
            if (isset($qry[$i]['ip'])) {
192
                $qry[$i]['ip'] = inet6_ntop($qry[$i]['ip']);
193
            }
194
        }
195
        $s = sizeof($qry);
196
        if ($s == 0 && $inv === false) {
197
            $doalert = false;
198
        } elseif ($s > 0 && $inv === false) {
199
            $doalert = true;
200
        } elseif ($s == 0 && $inv === true) {
201
            $doalert = true;
202
        } else { //( $s > 0 && $inv == false ) {
203
            $doalert = false;
204
        }
205
206
        $current_state = dbFetchCell("SELECT state FROM alerts WHERE rule_id = ? AND device_id = ? ORDER BY id DESC LIMIT 1", [$rule['id'], $device_id]);
207
        if ($doalert) {
208
            if ($current_state == 2) {
209
                c_echo('Status: %ySKIP');
210
            } elseif ($current_state >= 1) {
211
                c_echo('Status: %bNOCHG');
212
                // NOCHG here doesn't mean no change full stop. It means no change to the alert state
213
                // So we update the details column with any fresh changes to the alert output we might have.
214
                $alert_log           = dbFetchRow('SELECT alert_log.id, alert_log.details 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($device_id, $rule['id']));
215
                $details             = [];
216
                if (!empty($alert_log['details'])) {
217
                    $details = json_decode(gzuncompress($alert_log['details']), true);
218
                }
219
                $details['contacts'] = GetContacts($qry);
220
                $details['rule']     = $qry;
221
                $details             = gzcompress(json_encode($details), 9);
222
                dbUpdate(array('details' => $details), 'alert_log', 'id = ?', array($alert_log['id']));
223
            } else {
224
                $extra = gzcompress(json_encode(array('contacts' => GetContacts($qry), 'rule'=>$qry)), 9);
225
                if (dbInsert(['state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'details' => $extra], 'alert_log')) {
226
                    if (is_null($current_state)) {
227
                        dbInsert(array('state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1,'alerted' => 0), 'alerts');
228
                    } else {
229
                        dbUpdate(['state' => 1, 'open' => 1], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
230
                    }
231
                    c_echo(PHP_EOL . 'Status: %rALERT');
232
                }
233
            }
234
        } else {
235
            if (!is_null($current_state) && $current_state == 0) {
236
                c_echo('Status: %bNOCHG');
237
            } else {
238
                if (dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id']], 'alert_log')) {
239
                    if (is_null($current_state)) {
240
                        dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1, 'alerted' => 0], 'alerts');
241
                    } else {
242
                        dbUpdate(['state' => 0, 'open' => 1, 'note' => ''], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]);
243
                    }
244
245
                    c_echo(PHP_EOL . 'Status: %gOK');
246
                }
247
            }
248
        }
249
        c_echo('%n' . PHP_EOL);
250
    }
251
}
252
253
/**
254
 * Find contacts for alert
255
 * @param array $results Rule-Result
256
 * @return array
257
 */
258
function GetContacts($results)
259
{
260
    global $config;
261
262
    if (empty($results)) {
263
        return [];
264
    }
265
    if (Config::get('alert.default_only') === true || Config::get('alerts.email.default_only') === true) {
266
        $email = Config::get('alert.default_mail', Config::get('alerts.email.default'));
267
        return $email ? [$email => ''] : [];
268
    }
269
    $users = LegacyAuth::get()->getUserlist();
270
    $contacts = array();
271
    $uids = array();
272
    foreach ($results as $result) {
273
        $tmp  = null;
274
        if (is_numeric($result["bill_id"])) {
275
            $tmpa = dbFetchRows("SELECT user_id FROM bill_perms WHERE bill_id = ?", array($result["bill_id"]));
276
            foreach ($tmpa as $tmp) {
277
                $uids[$tmp['user_id']] = $tmp['user_id'];
278
            }
279
        }
280
        if (is_numeric($result["port_id"])) {
281
            $tmpa = dbFetchRows("SELECT user_id FROM ports_perms WHERE port_id = ?", array($result["port_id"]));
282
            foreach ($tmpa as $tmp) {
283
                $uids[$tmp['user_id']] = $tmp['user_id'];
284
            }
285
        }
286
        if (is_numeric($result["device_id"])) {
287
            if ($config['alert']['syscontact'] == true) {
288
                if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result["device_id"]])) {
289
                    $tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", array($result["device_id"]));
290
                } else {
291
                    $tmpa = dbFetchCell("SELECT sysContact FROM devices WHERE device_id = ?", array($result["device_id"]));
292
                }
293
                if (!empty($tmpa)) {
294
                    $contacts[$tmpa] = '';
295
                }
296
            }
297
            $tmpa = dbFetchRows("SELECT user_id FROM devices_perms WHERE device_id = ?", array($result["device_id"]));
298
            foreach ($tmpa as $tmp) {
299
                $uids[$tmp['user_id']] = $tmp['user_id'];
300
            }
301
        }
302
    }
303
    foreach ($users as $user) {
304
        if (empty($user['email'])) {
305
            continue; // no email, skip this user
306
        }
307
        if (empty($user['realname'])) {
308
            $user['realname'] = $user['username'];
309
        }
310
        if (empty($user['level'])) {
311
            $user['level'] = LegacyAuth::get()->getUserlevel($user['username']);
312
        }
313
        if ($config['alert']['globals'] && ( $user['level'] >= 5 && $user['level'] < 10 )) {
314
            $contacts[$user['email']] = $user['realname'];
315
        } elseif ($config['alert']['admins'] && $user['level'] == 10) {
316
            $contacts[$user['email']] = $user['realname'];
317
        } elseif ($config['alert']['users'] == true && in_array($user['user_id'], $uids)) {
318
            $contacts[$user['email']] = $user['realname'];
319
        }
320
    }
321
322
    $tmp_contacts = array();
323
    foreach ($contacts as $email => $name) {
324
        if (strstr($email, ',')) {
325
            $split_contacts = preg_split('/[,\s]+/', $email);
326
            foreach ($split_contacts as $split_email) {
327
                if (!empty($split_email)) {
328
                    $tmp_contacts[$split_email] = $name;
329
                }
330
            }
331
        } else {
332
            $tmp_contacts[$email] = $name;
333
        }
334
    }
335
336
    if (!empty($tmp_contacts)) {
337
        // Validate contacts so we can fall back to default if configured.
338
        $mail = new PHPMailer();
339
        foreach ($tmp_contacts as $tmp_email => $tmp_name) {
340
            if ($mail->validateAddress($tmp_email) != true) {
341
                unset($tmp_contacts[$tmp_email]);
342
            }
343
        }
344
    }
345
346
    # Copy all email alerts to default contact if configured.
347
    if (!isset($tmp_contacts[$config['alert']['default_mail']]) && ($config['alert']['default_copy'])) {
348
        $tmp_contacts[$config['alert']['default_mail']] = '';
349
    }
350
351
    # Send email to default contact if no other contact found
352
    if ((count($tmp_contacts) == 0) && ($config['alert']['default_if_none']) && (!empty($config['alert']['default_mail']))) {
353
        $tmp_contacts[$config['alert']['default_mail']] = '';
354
    }
355
356
    return $tmp_contacts;
357
}
358
359
/**
360
 * Populate variables
361
 * @param string  $txt  Text with variables
362
 * @param boolean $wrap Wrap variable for text-usage (default: true)
363
 * @return string
364
 */
365
function populate($txt, $wrap = true)
366
{
367
    preg_match_all('/%([\w\.]+)/', $txt, $m);
368
    foreach ($m[1] as $tmp) {
369
        $orig = $tmp;
370
        $rep  = false;
371
        if ($tmp == 'key' || $tmp == 'value') {
372
            $rep = '$'.$tmp;
373
        } else {
374
            if (strstr($tmp, '.')) {
375
                $tmp = explode('.', $tmp, 2);
376
                $pre = '$'.$tmp[0];
377
                $tmp = $tmp[1];
378
            } else {
379
                $pre = '$obj';
380
            }
381
382
            $rep = $pre."['".str_replace('.', "']['", $tmp)."']";
383
            if ($wrap) {
384
                $rep = '{'.$rep.'}';
385
            }
386
        }
387
388
        $txt = str_replace('%'.$orig, $rep, $txt);
389
    }//end foreach
390
    return $txt;
391
}//end populate()
392
393
/**
394
 * Describe Alert
395
 * @param array $alert Alert-Result from DB
396
 * @return array|boolean
397
 */
398
function DescribeAlert($alert)
399
{
400
    $obj         = array();
401
    $i           = 0;
402
    $device      = dbFetchRow('SELECT hostname, sysName, sysDescr, sysContact, os, type, ip, hardware, version, purpose, notes, uptime, status, status_reason, locations.location FROM devices LEFT JOIN locations ON locations.id = devices.location_id WHERE device_id = ?', array($alert['device_id']));
403
    $attribs     = get_dev_attribs($alert['device_id']);
404
405
    $obj['hostname']      = $device['hostname'];
406
    $obj['sysName']       = $device['sysName'];
407
    $obj['sysDescr']      = $device['sysDescr'];
408
    $obj['sysContact']    = $device['sysContact'];
409
    $obj['os']            = $device['os'];
410
    $obj['type']          = $device['type'];
411
    $obj['ip']            = inet6_ntop($device['ip']);
412
    $obj['hardware']      = $device['hardware'];
413
    $obj['version']       = $device['version'];
414
    $obj['location']      = $device['location'];
415
    $obj['uptime']        = $device['uptime'];
416
    $obj['uptime_short']  = Time::formatInterval($device['uptime'], 'short');
417
    $obj['uptime_long']   = Time::formatInterval($device['uptime']);
418
    $obj['description']   = $device['purpose'];
419
    $obj['notes']         = $device['notes'];
420
    $obj['alert_notes']   = $alert['note'];
421
    $obj['device_id']     = $alert['device_id'];
422
    $obj['rule_id']       = $alert['rule_id'];
423
    $obj['status']        = $device['status'];
424
    $obj['status_reason'] = $device['status_reason'];
425
    if (can_ping_device($attribs)) {
426
        $ping_stats = DevicePerf::where('device_id', $alert['device_id'])->latest('timestamp')->first();
427
        $obj['ping_timestamp'] = $ping_stats->template;
428
        $obj['ping_loss']      = $ping_stats->loss;
429
        $obj['ping_min']       = $ping_stats->min;
430
        $obj['ping_max']       = $ping_stats->max;
431
        $obj['ping_avg']       = $ping_stats->avg;
432
        $obj['debug']          = json_decode($ping_stats->debug, true);
433
    }
434
    $extra               = $alert['details'];
435
436
    $tpl                 = new Template;
437
    $template            = $tpl->getTemplate($obj);
438
439
    if ($alert['state'] >= 1) {
440
        $obj['title'] = $template->title ?: 'Alert for device '.$device['hostname'].' - '.($alert['name'] ? $alert['name'] : $alert['rule']);
441
        if ($alert['state'] == 2) {
442
            $obj['title'] .= ' got acknowledged';
443
        } elseif ($alert['state'] == 3) {
444
            $obj['title'] .= ' got worse';
445
        } elseif ($alert['state'] == 4) {
446
            $obj['title'] .= ' got better';
447
        }
448
449
        foreach ($extra['rule'] as $incident) {
450
            $i++;
451
            $obj['faults'][$i] = $incident;
452
            $obj['faults'][$i]['string'] = null;
453
            foreach ($incident as $k => $v) {
454
                if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) {
455
                    $obj['faults'][$i]['string'] .= $k.' = '.$v.'; ';
456
                }
457
            }
458
        }
459
        $obj['elapsed'] = TimeFormat(time() - strtotime($alert['time_logged']));
460
        if (!empty($extra['diff'])) {
461
            $obj['diff'] = $extra['diff'];
462
        }
463
    } elseif ($alert['state'] == 0) {
464
        // Alert is now cleared
465
        $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']));
466
        if (empty($id['id'])) {
467
            return false;
468
        }
469
470
        $extra = [];
471
        if (!empty($id['details'])) {
472
            $extra = json_decode(gzuncompress($id['details']), true);
473
        }
474
475
        // Reset count to 0 so alerts will continue
476
        $extra['count'] = 0;
477
        dbUpdate(array('details' => gzcompress(json_encode($id['details']), 9)), 'alert_log', 'id = ?', array($alert['id']));
478
479
        $obj['title'] = $template->title_rec ?: 'Device '.$device['hostname'].' recovered from '.($alert['name'] ? $alert['name'] : $alert['rule']);
480
        $obj['elapsed'] = TimeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged']));
481
        $obj['id']      = $id['id'];
482
        foreach ($extra['rule'] as $incident) {
483
            $i++;
484
            $obj['faults'][$i] = $incident;
485
            foreach ($incident as $k => $v) {
486
                if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) {
487
                    $obj['faults'][$i]['string'] .= $k.' => '.$v.'; ';
488
                }
489
            }
490
        }
491
    } else {
492
        return 'Unknown State';
493
    }//end if
494
    $obj['builder']   = $alert['builder'];
495
    $obj['uid']       = $alert['id'];
496
    $obj['alert_id']  = $alert['alert_id'];
497
    $obj['severity']  = $alert['severity'];
498
    $obj['rule']      = $alert['rule'];
499
    $obj['name']      = $alert['name'];
500
    $obj['timestamp'] = $alert['time_logged'];
501
    $obj['contacts']  = $extra['contacts'];
502
    $obj['state']     = $alert['state'];
503
    $obj['template']  = $template;
504
    return $obj;
505
}//end DescribeAlert()
506
507
/**
508
 * Format Elapsed Time
509
 * @param integer $secs Seconds elapsed
510
 * @return string
511
 */
512
function TimeFormat($secs)
513
{
514
    $bit = array(
515
        'y' => $secs / 31556926 % 12,
516
        'w' => $secs / 604800 % 52,
517
        'd' => $secs / 86400 % 7,
518
        'h' => $secs / 3600 % 24,
519
        'm' => $secs / 60 % 60,
520
        's' => $secs % 60,
521
    );
522
    $ret = array();
523
    foreach ($bit as $k => $v) {
524
        if ($v > 0) {
525
            $ret[] = $v.$k;
526
        }
527
    }
528
529
    if (empty($ret)) {
530
        return 'none';
531
    }
532
533
    return join(' ', $ret);
534
}//end TimeFormat()
535
536
537
function ClearStaleAlerts()
538
{
539
    $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";
540
    foreach (dbFetchRows($sql) as $alert) {
541
        if (empty($alert['hostname']) && isset($alert['alert_id'])) {
542
            dbDelete('alerts', '`id` = ?', array($alert['alert_id']));
543
            echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL;
544
        }
545
    }
546
}
547
548
/**
549
 * Re-Validate Rule-Mappings
550
 * @param integer $device_id Device-ID
551
 * @param integer $rule   Rule-ID
552
 * @return boolean
553
 */
554
function IsRuleValid($device_id, $rule)
555
{
556
    global $rulescache;
557
    if (empty($rulescache[$device_id]) || !isset($rulescache[$device_id])) {
558
        foreach (GetRules($device_id) as $chk) {
559
            $rulescache[$device_id][$chk['id']] = true;
560
        }
561
    }
562
563
    if ($rulescache[$device_id][$rule] === true) {
564
        return true;
565
    }
566
567
    return false;
568
}//end IsRuleValid()
569
570
571
/**
572
 * Issue Alert-Object
573
 * @param array $alert
574
 * @return boolean
575
 */
576
function IssueAlert($alert)
577
{
578
    global $config;
579
    if (dbFetchCell('SELECT attrib_value FROM devices_attribs WHERE attrib_type = "disable_notify" && device_id = ?', array($alert['device_id'])) == '1') {
580
        return true;
581
    }
582
583
    if ($config['alert']['fixed-contacts'] == false) {
584
        if (empty($alert['query'])) {
585
            $alert['query'] = GenSQL($alert['rule'], $alert['builder']);
586
        }
587
        $sql = $alert['query'];
588
        $qry = dbFetchRows($sql, array($alert['device_id']));
589
        $alert['details']['contacts'] = GetContacts($qry);
590
    }
591
592
    $obj = DescribeAlert($alert);
593
    if (is_array($obj)) {
594
        echo 'Issuing Alert-UID #'.$alert['id'].'/'.$alert['state'].':' . PHP_EOL;
595
        ExtTransports($obj);
596
597
        echo "\r\n";
598
    }
599
600
    return true;
601
}//end IssueAlert()
602
603
604
/**
605
 * Issue ACK notification
606
 * @return void
607
 */
608
function RunAcks()
609
{
610
611
    foreach (loadAlerts('alerts.state = 2 && alerts.open = 1') as $alert) {
612
        IssueAlert($alert);
613
        dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
614
    }
615
}//end RunAcks()
616
617
/**
618
 * Run Follow-Up alerts
619
 * @return void
620
 */
621
function RunFollowUp()
622
{
623
    foreach (loadAlerts('alerts.state > 0 && alerts.open = 0') as $alert) {
624
        if ($alert['state'] != 2 || ($alert['info']['until_clear'] === false)) {
625
            $rextra = json_decode($alert['extra'], true);
626
            if ($rextra['invert']) {
627
                continue;
628
            }
629
630
            if (empty($alert['query'])) {
631
                $alert['query'] = GenSQL($alert['rule'], $alert['builder']);
632
            }
633
            $chk = dbFetchRows($alert['query'], array($alert['device_id']));
634
            //make sure we can json_encode all the datas later
635
            $cnt = count($chk);
636
            for ($i = 0; $i < $cnt; $i++) {
637
                if (isset($chk[$i]['ip'])) {
638
                    $chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']);
639
                }
640
            }
641
            $o = sizeof($alert['details']['rule']);
642
            $n = sizeof($chk);
643
            $ret = 'Alert #' . $alert['id'];
644
            $state = 0;
645
            if ($n > $o) {
646
                $ret .= ' Worsens';
647
                $state = 3;
648
                $alert['details']['diff'] = array_diff($chk, $alert['details']['rule']);
649
            } elseif ($n < $o) {
650
                $ret .= ' Betters';
651
                $state = 4;
652
                $alert['details']['diff'] = array_diff($alert['details']['rule'], $chk);
653
            }
654
655
            if ($state > 0 && $n > 0) {
656
                $alert['details']['rule'] = $chk;
657
                if (dbInsert(array(
658
                    'state' => $state,
659
                    'device_id' => $alert['device_id'],
660
                    'rule_id' => $alert['rule_id'],
661
                    'details' => gzcompress(json_encode($alert['details']), 9)
662
                ), 'alert_log')) {
663
                    dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
664
                }
665
666
                echo $ret . ' (' . $o . '/' . $n . ")\r\n";
667
            }
668
        }
669
    }//end foreach
670
}//end RunFollowUp()
671
672
function loadAlerts($where)
673
{
674
    $alerts = [];
675
    foreach (dbFetchRows("SELECT alerts.id, alerts.device_id, alerts.rule_id, alerts.state, alerts.note, alerts.info FROM alerts WHERE $where") as $alert_status) {
676
        $alert = dbFetchRow(
677
            '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,alert_rules.query,alert_rules.builder 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',
678
            array($alert_status['device_id'], $alert_status['rule_id'])
679
        );
680
681
        if (empty($alert['rule_id']) || !IsRuleValid($alert_status['device_id'], $alert_status['rule_id'])) {
682
            echo 'Stale-Rule: #' . $alert_status['rule_id'] . '/' . $alert_status['device_id'] . "\r\n";
683
            // Alert-Rule does not exist anymore, let's remove the alert-state.
684
            dbDelete('alerts', 'rule_id = ? && device_id = ?', [$alert_status['rule_id'], $alert_status['device_id']]);
685
        } else {
686
            $alert['alert_id'] = $alert_status['id'];
687
            $alert['state'] = $alert_status['state'];
688
            $alert['note'] = $alert_status['note'];
689
            if (!empty($alert['details'])) {
690
                $alert['details'] = json_decode(gzuncompress($alert['details']), true);
691
            }
692
            $alert['info'] = json_decode($alert_status['info'], true);
693
            $alerts[] = $alert;
694
        }
695
    }
696
697
    return $alerts;
698
}
699
700
/**
701
 * Run all alerts
702
 * @return void
703
 */
704
function RunAlerts()
705
{
706
    global $config;
707
    foreach (loadAlerts('alerts.state != 2 && alerts.open = 1') as $alert) {
708
        $noiss            = false;
709
        $noacc            = false;
710
        $updet            = false;
711
        $rextra           = json_decode($alert['extra'], true);
712
        if (!isset($rextra['recovery'])) {
713
            // backwards compatibility check
714
            $rextra['recovery'] = true;
715
        }
716
717
        $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']));
718
719
        if ($chk['alerted'] == $alert['state']) {
720
            $noiss = true;
721
        }
722
723
        if (!empty($rextra['count']) && empty($rextra['interval'])) {
724
            // This check below is for compat-reasons
725
            if (!empty($rextra['delay'])) {
726
                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'])) {
727
                    continue;
728
                } else {
729
                    $alert['details']['delay'] = time();
730
                    $updet = true;
731
                }
732
            }
733
734
            if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) {
735
                if ($alert['details']['count'] < $rextra['count']) {
736
                    $noacc = true;
737
                }
738
739
                $updet = true;
740
                $noiss = false;
741
            }
742
        } else {
743
            // This is the new way
744
            if (!empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + $config['alert']['tolerance_window']) < $rextra['delay']) {
745
                continue;
746
            }
747
748
            if (!empty($rextra['interval'])) {
749
                if (!empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + $config['alert']['tolerance_window']) < $rextra['interval']) {
750
                    continue;
751
                } else {
752
                    $alert['details']['interval'] = time();
753
                    $updet = true;
754
                }
755
            }
756
757
            if (in_array($alert['state'], [1,3,4]) && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) {
758
                if ($alert['details']['count'] < $rextra['count']) {
759
                    $noacc = true;
760
                }
761
762
                $updet = true;
763
                $noiss = false;
764
            }
765
        }//end if
766
        if ($chk['ignore'] == 1 || $chk['disabled'] == 1) {
767
            $noiss = true;
768
            $updet = false;
769
            $noacc = false;
770
        }
771
772
        if (IsMaintenance($alert['device_id']) > 0) {
773
            $noiss = true;
774
            $noacc = true;
775
        }
776
777
        if ($updet) {
778
            dbUpdate(array('details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log', 'id = ?', array($alert['id']));
779
        }
780
781
        if (!empty($rextra['mute'])) {
782
            echo 'Muted Alert-UID #'.$alert['id']."\r\n";
783
            $noiss = true;
784
        }
785
786
        if (IsParentDown($alert['device_id'])) {
787
            $noiss = true;
788
            log_event('Skipped alerts because all parent devices are down', $alert['device_id'], 'alert', 1);
789
        }
790
791
        if ($alert['state'] == 0 && $rextra['recovery'] == false) {
792
            // Rule is set to not send a recovery alert
793
            $noiss = true;
794
        }
795
796
        if (!$noiss) {
797
            IssueAlert($alert);
798
            dbUpdate(array('alerted' => $alert['state']), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
799
        }
800
801
        if (!$noacc) {
802
            dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id']));
803
        }
804
    }//end foreach
805
}//end RunAlerts()
806
807
808
/**
809
 * Run external transports
810
 * @param array $obj Alert-Array
811
 * @return void
812
 */
813
function ExtTransports($obj)
814
{
815
    $type  = new Template;
816
817
    // If alert transport mapping exists, override the default transports
818
    $transport_maps = AlertUtil::getAlertTransports($obj['alert_id']);
819
820
    if (!$transport_maps) {
821
        $transport_maps = AlertUtil::getDefaultAlertTransports();
822
    }
823
824
    // alerting for default contacts, etc
825
    if (Config::get('alert.transports.mail') === true && !empty($obj['contacts'])) {
826
        $transport_maps[] = [
827
            'transport_id' => null,
828
            'transport_type' => 'mail',
829
            'opts' => $obj,
830
        ];
831
    }
832
833
    foreach ($transport_maps as $item) {
834
        $class = 'LibreNMS\\Alert\\Transport\\'.ucfirst($item['transport_type']);
835
        if (class_exists($class)) {
836
            //FIXME remove Deprecated transport
837
            $transport_title = "Transport {$item['transport_type']}";
838
            $obj['transport'] = $item['transport_type'];
839
            $obj['transport_name'] = $item['transport_name'];
840
            $obj['alert']     = new AlertData($obj);
841
            $obj['title']     = $type->getTitle($obj);
842
            $obj['alert']['title'] = $obj['title'];
843
            $obj['msg']       = $type->getBody($obj);
844
            c_echo(" :: $transport_title => ");
845
            $instance = new $class($item['transport_id']);
846
            $tmp = $instance->deliverAlert($obj, $item['opts']);
847
            AlertLog($tmp, $obj, $obj['transport']);
848
            unset($instance);
849
            echo PHP_EOL;
850
        }
851
    }
852
853
    if (count($transport_maps) === 0) {
854
        echo 'No configured transports';
855
    }
856
}//end ExtTransports()
857
858
// Log alert event
859
function AlertLog($result, $obj, $transport)
860
{
861
    $prefix = [
862
        0 => "recovery",
863
        1 => $obj['severity']." alert",
864
        2 => "acknowledgment"
865
    ];
866
    $prefix[3] = &$prefix[0];
867
    $prefix[4] = &$prefix[0];
868
    if ($result === true) {
869
        echo 'OK';
870
        log_event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], 'alert', 1);
871
    } elseif ($result === false) {
872
        echo 'ERROR';
873
        log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 5);
874
    } else {
875
        echo "ERROR: $result\r\n";
876
        log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $result, $obj['device_id'], 'error', 5);
877
    }
878
    return;
879
}//end AlertLog()
880
881
882
/**
883
 * Check if a device's all parent are down
884
 * Returns true if all parents are down
885
 * @param int $device Device-ID
886
 * @return bool
887
 */
888
function IsParentDown($device)
889
{
890
    $parent_count = dbFetchCell("SELECT count(*) from `device_relationships` WHERE `child_device_id` = ?", array($device));
891
    if (!$parent_count) {
892
        return false;
893
    }
894
895
896
    $down_parent_count = dbFetchCell("SELECT count(*) from devices as d LEFT JOIN devices_attribs as a ON d.device_id=a.device_id LEFT JOIN device_relationships as r ON d.device_id=r.parent_device_id WHERE d.status=0 AND d.ignore=0 AND d.disabled=0 AND r.child_device_id=? AND (d.status_reason='icmp' OR (a.attrib_type='override_icmp_disable' AND a.attrib_value=true))", array($device));
897
    if ($down_parent_count == $parent_count) {
898
        return true;
899
    }
900
901
    return false;
902
} //end IsParentDown()
903