Subversion Repositories vnag

Rev

Rev 59 | 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>
70 daniel-mar 2
Dn0TAtS8an72QeGrW4rRe6KqVYdOn1iBSJMs07mG3B21qZSA77cSJqdqIvni4+mQ+
3
k3ee/H/eGuu/j6XsdzIRNXk+p+CjfSaXf9jn0VwWXhurSgRzF7uOIbhv3jpKI9mss
4
dY4aQVblJiTTKuN27AB7LjqSSmJyEidWDYVe35g79sj76MJMP5Dz3+dcZSlcqhDrH
5
6T+uR5aoz09Kzro50iZ3M6n1e4EY9hDJH+Bqdj8kJRk0qGGt+XFHwqEAahJMiE1Sq
6
eFZmiwhEBmck70HpC2gcmrm5L3cp5pv7z3UFGXmi2QoKyzZMQKfyw2BD+uGv9uKR4
7
LE5sdpJy4ZsHjRzKuKXmu+eLNEh8loDzYzIRBnikk9NV9FrO9Rraw14aVhcRvxY5K
8
j3CmReIweYJRqJ9kfEjTh5RMMut33ZazaIw1rgGvKSNq5HVgP/tPc3Nx1RIEmgBu5
9
e5gB+ou9i4w01gi+b5Akw/yWsTrr+JE83j1BXWaDXLtenaCRAMGB7/q/kEcFgoJlt
10
qSxcMFmvTExlFldfF9ohXlE52F9AVQJv76AYu86FTZH8qQFMn6Z2yL/HHrBJvpEsW
11
AsKOWsvKH9x8EZLQKzwuaQRGX01Z936d2V/e7Rb+EJjmUbQ/9TJrpYSzAZB4PCPnU
12
YHeWNMwIHi82MDwG+7ey+bF9A0DJyosa0f+tgQ0szpoOJgCXZNm+guCYFkp5rnbWs
13
16VlVsqYxn20yFUX3uci7s31ToIqJfLGpZKi1yaWtavhW2Jb6SxuMU7nsaQgsyv6J
14
VR2eJ49itgeTpArGWLscTJ9BHDeOjhkLqoVPVWut2XJF8mJTn26pnvIn04997JL+I
15
e7FSV+6mtWLae+fh7A/aASM/4PJQhKqBARZtGAsgH111z4iqE4psACt6Z62ekgO7O
16
/slAwR+9Je7VT/CZdOoNnShNIYGlVgm1cKnHxAP7H7vBDhMmTbTeZUtdZU2hOAJME
17
1C99Q4btFMnE1nW5oB9jQpeKXZ6DEivyNsy7gG+YPqauMlOu30gXYx7V4juAaBvlO
18
kGwJ1jpqsaPI4C0o7smkksy6WHWGsRioTgxc+vOBnqOczYWaste5TlFIEaW4KAHcg
19
3hEeebMPeNWhgUp/uJnfpEuSt3M89tEhD6Hr1LxNY5dflperss3LeXAtCuuqdT6qu
20
STrB6iTCcAvngMfqyZKrnrq1xhraH+b7iC8qpmDZIPM1pjoCTJjVa6DrEmmh/3+mQ
21
ZuOwqRNWSmhvMf4V1gzx4py7KCPP+bwMe8fphXG9Qu3Drl0vV13PB1QXzDkMAQ5zn
22
+LZva+1J5Q33Xu1DH8ih2rqFYdhKYS+Er9KP1iJsTmpFoJHe+b8IXzI++MeL0WGEV
23
w==
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
 *
59 daniel-mar 32
 * Revision 2022-12-18
2 daniel-mar 33
 *
34
 * Changelog:
35
 * 2018-08-01   1.0   Initial release
36
 * 2018-09-02   1.1   Added argument -e|--emptyok
37
 *                    Output a warning if the Linux user does not exist.
38
 * 2018-10-01   1.2   Fixed a bug where too many unnecessary requests were sent to ipinfo.io
39
 *                    Cache file location ~/.last_ipcache is now preferred
40
 *                    A token for ipinfo.io can now be provided
41
 * 2018-11-03   1.2.1 "system boot" lines are now excluded
59 daniel-mar 42
 * 2022-12-18   1.2.2 Use the new cache directory now
2 daniel-mar 43
 */
44
 
45
// QUE: should we allow usernames to have wildcards, regexes or comma separated?
46
 
47
declare(ticks=1);
48
 
49
class LastCheck extends VNag {
50
        private $argUser = null;
51
        private $argRegex = null;
52
        private $argWtmpFiles = null;
53
        private $argEmptyOk = null;
54
        private $argIpInfoToken = null;
55
 
56
        private $cache = null;
57
        private $cacheFile = null;
58
        private $cacheDirty = false;
59
 
60
        public function __construct() {
61
                parent::__construct();
62
 
63
                if ($this->is_http_mode()) {
64
                        // Don't allow the standard arguments via $_REQUEST
65
                        $this->registerExpectedStandardArguments('');
66
                } else {
67
                        $this->registerExpectedStandardArguments('Vhtv');
68
                }
69
 
70
                $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));
71
                $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));
72
                $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*'));
73
                $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));
74
                $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));
75
 
76
                $this->getHelpManager()->setPluginName('vnag_last');
77
                $this->getHelpManager()->setVersion('1.2');
78
                $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.');
79
                $this->getHelpManager()->setCopyright('Copyright (C) 2011-$CURYEAR$ Daniel Marschall, ViaThinkSoft.');
80
                $this->getHelpManager()->setSyntax('$SCRIPTNAME$ [-v] [-e] [-u username] [-R regex] [--ipInfoToken token]');
81
                $this->getHelpManager()->setFootNotes('If you encounter bugs, please contact ViaThinkSoft at www.viathinksoft.com');
82
 
59 daniel-mar 83
                $this->cacheFile = $this->get_cache_dir() . '/.last_ip_cache';
84
                if (!file_exists($this->cacheFile)) @touch($this->cacheFile);
2 daniel-mar 85
                $this->cache = $this->cacheFile ? json_decode(file_get_contents($this->cacheFile),true) : array();
86
        }
87
 
88
        public function __destruct() {
89
                if ($this->cacheFile && $this->cacheDirty) {
90
                        @file_put_contents($this->cacheFile, json_encode($this->cache));
91
                }
92
        }
93
 
94
        private function getCountryAndOrg($ip) {
95
                if (isset($this->cache[$ip])) return $this->cache[$ip];
96
 
97
                $url = 'https://ipinfo.io/'.urlencode($ip).'/json';
98
                $token = $this->argIpInfoToken->getValue();
99
                if ($token) $url .= '?token='.urlencode($token);
100
 
101
                // fwrite(STDERR, "Note: Will query $url\n");
102
                $cont = file_get_contents($url);
103
                if (!$cont) return array();
104
                if (!($data = @json_decode($cont, true))) return array();
105
                if (isset($data['error'])) return array();
106
 
107
                if (isset($data['bogon']) && ($data['bogon'])) {
108
                        // Things like 127.0.0.1 do not belong to anyone
109
                        $res = array();
110
                } else {
111
                        $res = array();
112
                        if (isset($data['hostname'])) $res[] = $data['hostname'];
113
                        if (isset($data['country'])) $res[] = $data['country'];
114
                        list($as, $orgName) = explode(' ', $data['org'], 2);
115
                        $res[] = $as;
116
                        $res[] = $orgName;
117
                }
118
 
119
                $this->cache[$ip] = $res;
120
                $this->cacheDirty = true;
121
                return $res;
122
        }
123
 
124
        private function getLastLoginIPs($username) {
125
                $cont = '';
126
                $files = glob($this->argWtmpFiles->getValue());
127
                foreach ($files as $file) {
128
                        if (trim($username) == '') {
129
                                $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '); // all users
130
                        } else {
131
                                $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '.escapeshellarg($username));
132
                        }
133
                }
70 daniel-mar 134
 
2 daniel-mar 135
                preg_match_all('@^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$@ismU', $cont, $m, PREG_SET_ORDER);
136
                foreach ($m as $key => &$a) {
137
                        if (($a[2] === 'system') && ($a[3] === 'boot')) {
138
                                // reboot   system boot  4.9.0-8-amd64    Fri Oct 12 02:10   still running
70 daniel-mar 139
                                // 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 140
                                unset($m[$key]);
70 daniel-mar 141
                        //} else if ($a[2] === 'begins') {
142
                        } else if (substr($a[1],0,4) === 'wtmp') {
143
                                // wtmp.1 begins Fri Oct 12 02:10:43 2018   (English)
144
                                // wtmp beginnt Wed Aug 16 11:43:03 2023    (German)
2 daniel-mar 145
                                unset($m[$key]);
146
                        } else {
147
                                array_shift($a);
148
                        }
149
                }
150
                return $m;
151
        }
152
 
153
        protected function cbRun() {
154
                if (!`which which`) {
155
                        throw new VNagException("Program 'which' is not installed on your system");
156
                }
157
 
158
                if (!`which last`) {
159
                        throw new VNagException("Program 'last' (usually included in package smartmontools) is not installed on your system");
160
                }
161
 
162
                $username = $this->argUser->available() ? $this->argUser->getValue() : '';
163
                $regex = $this->argRegex->available() ? $this->argRegex->getValue() : null;
164
 
165
                if (($username != '') && function_exists('posix_getpwnam') && !posix_getpwnam($username)) {
166
                        $this->setStatus(VNag::STATUS_WARNING);
167
                        $this->addVerboseMessage("WARNING: Currently, there is no Linux user with name '$username'.", VNag::VERBOSITY_SUMMARY);
168
                }
169
 
170
                $count_total = 0;
171
                $count_ok = 0;
172
                $count_warning = 0;
173
 
174
                foreach ($this->getLastLoginIPs($username) as list($username, $pts, $ip, $times)) {
36 daniel-mar 175
                        // IP ":pts/0:S.0" means that there is a screen session
176
                        if (strpos($ip,':pts/') === 0) continue;
177
 
2 daniel-mar 178
                        $count_total++;
179
 
180
                        $fields = $this->getCountryAndOrg($ip);
181
                        $fields[] = $ip;
182
 
183
                        if (is_null($regex)) {
184
                                // No regex. Just show the logins for information (status stays VNag::STATUS_UNKNOWN)
185
                                $this->addVerboseMessage("INFO: ".implode(' ',$fields)." @ $username, $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
186
                        } else {
187
                                $match = false;
188
                                foreach ($fields as $f) {
189
                                        if (preg_match($regex, $f, $dummy)) {
190
                                                $match = true;
191
                                                break;
192
                                        }
193
                                }
194
 
195
                                if ($match) {
196
                                        $count_ok++;
197
                                        $this->addVerboseMessage("OK: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
198
                                        $this->setStatus(VNag::STATUS_OK);
199
                                } else {
200
                                        $count_warning++;
201
                                        $this->addVerboseMessage("WARNING: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_SUMMARY);
202
                                        $this->setStatus(VNag::STATUS_WARNING);
203
                                }
204
                        }
205
                }
206
 
207
                if (is_null($regex)) {
208
                        $this->setHeadline("Checked $count_total logins (No checks done because argument 'Regex' is missing)");
209
                } else {
210
                        if (($count_total == 0) && ($this->argEmptyOk->count() > 0)) {
211
                                $this->setStatus(VNag::STATUS_OK);
212
                        }
213
                        $this->setHeadline("Checked $count_total logins ($count_ok OK, $count_warning Warning)");
214
                }
215
        }
216
}