Subversion Repositories vnag

Rev

Rev 77 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
4 daniel-mar 1
<?php /* <ViaThinkSoftSignature>
78 daniel-mar 2
nPj2Ge28Yr6M12/PcAB0jcBhoEu8jOQxCmQGGimC9FD/I/AVWxpG+QqNDbQxjCueH
3
WFqtPEM2HsROB8HE8KZkUPo99/m+teX/wNro3ZK6rI+6NcHtnOipY2SimPpeLjn76
4
WBJ+r/4WX7UM9tA0ivyUycOAITO8XQtL1JbSyp7zrxN1fONVXnZ0P0JIhthc5myFa
5
h4OlEWbED0Dmpa23jL+amEeo27JOkewjt56iTkJTXOW8u+oKl86F0SKfPd7q5232y
6
0ioNIa3S6iWH4u9kHta4AHjoWPd7CpUyMB3kZGKN1YS4RJWjNNs5VZSUaDevYaOM0
7
rni8Rogtv3jDOGnBZH/wsysZcYzs/drn+72FN6gQGc0rlNbAxTvsa0EFnwx8D0oh4
8
XjiB/TBvzgEbCxVEkXYLrXJ7sXay8BiJY0958JxjCppWbuieRg621FEabG6r/gAs7
9
kjddREytPoN6mLd8H9/zNCGFH8BKSZMKKmjLkQjOJd5xk0Owb6QH99QtD5XoISbQx
10
rXc4UOSNq9YrVZTFDLo9inqay1Ne268MUwi8PGN0ouDUIuCcFQRIO/vQUp+aKmD/R
11
WJZcv/m7RLSsXplMC7FHUMjgM5f1vdSA70ac1zzStO2iq+jqnbBxf+NJBjn7A+sCP
12
8JaTM1Vt8VOoj0pJhV6SclkEEkSq2cEcDFDwy99LNcuz9Bf2k+eTRuutO+h0ESYR0
13
R6U6Spx7WQA11BMinkxaQ0spfOt5VAyHInW9Tbv2QD0tEz3h3VryAbqI0HO6kcGxK
14
gnyVJV69H15BPz6TGeoF5a7QAwqjQxxoWZwlDbnNTSG+c6wWcJhoyoiLsa8gYZ4tS
15
/C8+1V/QPX0X2nE74M2Q0AWQXGmjLnDTZHPDiN8ZCqayekuWT90SQIMAQ1j/nv703
16
K7Bf+IUuIwKNRdi+LQolu7X3ovgpGi9fBAKxXAR6k7Oi8U5FxKvo6mthIQO4imGQZ
17
aUNsutwqy2tLnLkQmb+aUdSkL/Fkynf7BkW+s64iSOt+OI3RTR/qPSaYL/MDD7eMC
18
Vwq2wjsueThpszPaAuv22BOAfhWngIjdbPlN7CDgB+8H5MuAA1Zkqmm5lsQyMlUNT
19
FecOvytEOA1BR0slyXZsZtf5dX6zZYB1d26U115+2utF1LBPJJ9fcPIEcjAvTakVx
20
5tRbDRFXW2Sn4MgPs7ljqoQ0sjzyUti6t+HODtDHgSdMZfjP7VimDqRmcsPUJfv50
21
YG2Lw0nvle1m6AMb4oDue8s06c3W29z+D75bVH0n2KLw/PLQx2ycbNUc7OgoCrLlr
22
gwwZ/IU3dhaQQxD2wgpq6RvYaNvMWd37snfIFXAVOdw6qsKyKjlUa5LqMs9ZqGZno
23
A==
4 daniel-mar 24
</ViaThinkSoftSignature> */ ?>
2 daniel-mar 25
<?php
26
 
27
/*
28
 * VNag - Nagios Framework for PHP
29
 * Developed by Daniel Marschall, ViaThinkSoft <www.viathinksoft.com>
30
 * Licensed under the terms of the Apache 2.0 license
31
 *
76 daniel-mar 32
 * Revision 2023-10-13
2 daniel-mar 33
 */
34
 
35
// QUE: should we allow usernames to have wildcards, regexes or comma separated?
36
 
37
declare(ticks=1);
38
 
39
class LastCheck extends VNag {
40
        private $argUser = null;
41
        private $argRegex = null;
42
        private $argWtmpFiles = null;
43
        private $argEmptyOk = null;
44
        private $argIpInfoToken = null;
45
 
46
        private $cache = null;
47
        private $cacheFile = null;
48
        private $cacheDirty = false;
49
 
50
        public function __construct() {
51
                parent::__construct();
52
 
53
                if ($this->is_http_mode()) {
54
                        // Don't allow the standard arguments via $_REQUEST
55
                        $this->registerExpectedStandardArguments('');
56
                } else {
57
                        $this->registerExpectedStandardArguments('Vhtv');
58
                }
59
 
60
                $this->addExpectedArgument($this->argUser = new VNagArgument('u', 'user', VNagArgument::VALUE_REQUIRED, 'user', 'The Linux username. If the argument is missing, all users will be checked.', null));
61
                $this->addExpectedArgument($this->argRegex = new VNagArgument('R', 'regex', VNagArgument::VALUE_REQUIRED, 'regex', 'The regular expression (in PHP: preg_match) which is applied on IP, Hostname, Country, AS number or ISP name. If the regular expression matches, the login will be accepted, otherweise an alert will be triggered. Example: /DE/ismU or /Telekom/ismU', null));
62
                $this->addExpectedArgument($this->argWtmpFiles = new VNagArgument('f', 'wtmpfile', VNagArgument::VALUE_REQUIRED, 'wtmpfile', 'Filemask of the wtmp file (important if you use logrotate), e.g. \'/var/log/wtmp*\'', '/var/log/wtmp*'));
63
                $this->addExpectedArgument($this->argEmptyOk = new VNagArgument('e', 'emptyok', VNagArgument::VALUE_FORBIDDEN, null, 'Treat an empty result (e.g. empty wtmp file after rotation) as success; otherwise treat it as status "Unknown"', null));
64
                $this->addExpectedArgument($this->argIpInfoToken = new VNagArgument(null, 'ipInfoToken', VNagArgument::VALUE_REQUIRED, 'token', 'If you have a token for ipinfo.io, please enter it here. Without token, you can query the service approx 1,000 times per day (which should be enough)', null));
65
 
66
                $this->getHelpManager()->setPluginName('vnag_last');
67
                $this->getHelpManager()->setVersion('1.2');
68
                $this->getHelpManager()->setShortDescription('This plugin checks the logs of the tool "LAST" an warns when users have logged in with an unexpected IP/Country/ISP.');
69
                $this->getHelpManager()->setCopyright('Copyright (C) 2011-$CURYEAR$ Daniel Marschall, ViaThinkSoft.');
70
                $this->getHelpManager()->setSyntax('$SCRIPTNAME$ [-v] [-e] [-u username] [-R regex] [--ipInfoToken token]');
71
                $this->getHelpManager()->setFootNotes('If you encounter bugs, please contact ViaThinkSoft at www.viathinksoft.com');
72
 
78 daniel-mar 73
                $this->cacheFile = $this->get_cache_dir().'/'.hash('sha256','LastCheck:last_ip_cache');
59 daniel-mar 74
                if (!file_exists($this->cacheFile)) @touch($this->cacheFile);
2 daniel-mar 75
                $this->cache = $this->cacheFile ? json_decode(file_get_contents($this->cacheFile),true) : array();
76
        }
77
 
78
        public function __destruct() {
79
                if ($this->cacheFile && $this->cacheDirty) {
80
                        @file_put_contents($this->cacheFile, json_encode($this->cache));
81
                }
82
        }
83
 
84
        private function getCountryAndOrg($ip) {
85
                if (isset($this->cache[$ip])) return $this->cache[$ip];
86
 
87
                $url = 'https://ipinfo.io/'.urlencode($ip).'/json';
88
                $token = $this->argIpInfoToken->getValue();
89
                if ($token) $url .= '?token='.urlencode($token);
90
 
91
                // fwrite(STDERR, "Note: Will query $url\n");
76 daniel-mar 92
                $cont = $this->url_get_contents($url);
93
                if ($cont === false) return array();
77 daniel-mar 94
                if (($data = @json_decode($cont, true)) === false) return array();
2 daniel-mar 95
                if (isset($data['error'])) return array();
96
 
97
                if (isset($data['bogon']) && ($data['bogon'])) {
98
                        // Things like 127.0.0.1 do not belong to anyone
99
                        $res = array();
100
                } else {
101
                        $res = array();
102
                        if (isset($data['hostname'])) $res[] = $data['hostname'];
103
                        if (isset($data['country'])) $res[] = $data['country'];
104
                        list($as, $orgName) = explode(' ', $data['org'], 2);
105
                        $res[] = $as;
106
                        $res[] = $orgName;
107
                }
108
 
109
                $this->cache[$ip] = $res;
110
                $this->cacheDirty = true;
111
                return $res;
112
        }
113
 
114
        private function getLastLoginIPs($username) {
115
                $cont = '';
116
                $files = glob($this->argWtmpFiles->getValue());
117
                foreach ($files as $file) {
118
                        if (trim($username) == '') {
119
                                $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '); // all users
120
                        } else {
121
                                $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '.escapeshellarg($username));
122
                        }
123
                }
70 daniel-mar 124
 
2 daniel-mar 125
                preg_match_all('@^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$@ismU', $cont, $m, PREG_SET_ORDER);
126
                foreach ($m as $key => &$a) {
127
                        if (($a[2] === 'system') && ($a[3] === 'boot')) {
128
                                // reboot   system boot  4.9.0-8-amd64    Fri Oct 12 02:10   still running
70 daniel-mar 129
                                // reboot   system boot  6.1.0-11-amd64   Fri Sep  8 13:10:27 2023 - Sat Sep  9 17:40:50 2023 (1+04:30)
2 daniel-mar 130
                                unset($m[$key]);
70 daniel-mar 131
                        //} else if ($a[2] === 'begins') {
132
                        } else if (substr($a[1],0,4) === 'wtmp') {
133
                                // wtmp.1 begins Fri Oct 12 02:10:43 2018   (English)
134
                                // wtmp beginnt Wed Aug 16 11:43:03 2023    (German)
2 daniel-mar 135
                                unset($m[$key]);
136
                        } else {
137
                                array_shift($a);
138
                        }
139
                }
140
                return $m;
141
        }
142
 
143
        protected function cbRun() {
144
                if (!`which which`) {
145
                        throw new VNagException("Program 'which' is not installed on your system");
146
                }
147
 
148
                if (!`which last`) {
149
                        throw new VNagException("Program 'last' (usually included in package smartmontools) is not installed on your system");
150
                }
151
 
152
                $username = $this->argUser->available() ? $this->argUser->getValue() : '';
153
                $regex = $this->argRegex->available() ? $this->argRegex->getValue() : null;
154
 
155
                if (($username != '') && function_exists('posix_getpwnam') && !posix_getpwnam($username)) {
156
                        $this->setStatus(VNag::STATUS_WARNING);
157
                        $this->addVerboseMessage("WARNING: Currently, there is no Linux user with name '$username'.", VNag::VERBOSITY_SUMMARY);
158
                }
159
 
160
                $count_total = 0;
161
                $count_ok = 0;
162
                $count_warning = 0;
163
 
164
                foreach ($this->getLastLoginIPs($username) as list($username, $pts, $ip, $times)) {
36 daniel-mar 165
                        // IP ":pts/0:S.0" means that there is a screen session
166
                        if (strpos($ip,':pts/') === 0) continue;
167
 
2 daniel-mar 168
                        $count_total++;
169
 
170
                        $fields = $this->getCountryAndOrg($ip);
171
                        $fields[] = $ip;
172
 
173
                        if (is_null($regex)) {
174
                                // No regex. Just show the logins for information (status stays VNag::STATUS_UNKNOWN)
175
                                $this->addVerboseMessage("INFO: ".implode(' ',$fields)." @ $username, $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
176
                        } else {
177
                                $match = false;
178
                                foreach ($fields as $f) {
179
                                        if (preg_match($regex, $f, $dummy)) {
180
                                                $match = true;
181
                                                break;
182
                                        }
183
                                }
184
 
185
                                if ($match) {
186
                                        $count_ok++;
187
                                        $this->addVerboseMessage("OK: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
188
                                        $this->setStatus(VNag::STATUS_OK);
189
                                } else {
190
                                        $count_warning++;
191
                                        $this->addVerboseMessage("WARNING: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_SUMMARY);
192
                                        $this->setStatus(VNag::STATUS_WARNING);
193
                                }
194
                        }
195
                }
196
 
197
                if (is_null($regex)) {
198
                        $this->setHeadline("Checked $count_total logins (No checks done because argument 'Regex' is missing)");
199
                } else {
200
                        if (($count_total == 0) && ($this->argEmptyOk->count() > 0)) {
201
                                $this->setStatus(VNag::STATUS_OK);
202
                        }
203
                        $this->setHeadline("Checked $count_total logins ($count_ok OK, $count_warning Warning)");
204
                }
205
        }
206
}