Subversion Repositories vnag

Rev

Rev 36 | Go to most recent revision | Blame | Last modification | View Log | RSS feed

  1. <?php /* <ViaThinkSoftSignature>
  2. Fm798K8OnhYRk2wZ5LGT1eZv1U9J4+xksI2BNLu4KZxIw2YI06hHPgG9ZkiYp88+Y
  3. EuW8MeTg1z1ee4f8qNi9ZLLpSyiWEK9sAvJYJeWNq9Nx3avUf10Caa9iU3UQp4cT5
  4. d2NC00RqKZRtBC5rPZ6u+ZXNoEz1UN6Z3b//FlXtRW1adXqgPpCNhw4HM192iXDld
  5. 5YXYDhEsH7/mF0TJ4bFkRmNG5FDsX9uM0+SD5vZ8VgW4HW0KwO3qJG89QOelc1TmO
  6. 88fFklxG3wmTZU/c1mCOT7tgrl4AVmUamNyZRygaeKWORfTM8m1PrFm86MyGCvLR+
  7. ZVr8mqtAV5mJt3NR9NxpkxuLJN8qPpEuMzbQV/0MfjsRjdA/bLaXA186maJ7IaS1a
  8. 69+xLqrLbcBn+c4Keej72vAdhSs2HmpPqO6wVTsV24LMVDWgS5IEYDfK5ylHCikTI
  9. YQesBoQELvXEtKuJI5EJCBsH0uDuIiuL2xe7btJJi8fUYaBHXOW50KoY0HWlIntXd
  10. UuI/CABDWgVE1XMqUu4xemM6mGoPcZY1V5FtV4A06KtXTEbmqRwileEe12jO3aVKq
  11. HrOoGY6ZB3gk1AxH7sUeUKFWJ6yS+zzUriEoka0YrJVrBgG3g6yaNDebNU4KVNx+G
  12. zQalJ6+k0RaWMdysrRy3/IJlDhu9Vt77QRb7twP9c8WARJsGgEKCTwuutlD10Cl8m
  13. IpJ0CJj4fHDqLJHvXJNb9IH2G1W1YxfmPYqsscfW+n0mVBnp3ZjVo4iuV0Gr4hrS6
  14. WwBMX6XApPU8shgUwUf3mENE7WY8EKaNpybZsn3OTxAGXnJ2293x+o1Zm29Xv3Ovq
  15. R9VszqfHCaGq2V51BAPKGydtgF2QF93mL8vwlfqH1v/m/kRujRjLQfYv7T8+6CxI7
  16. hpXCp9LLsr2KsCNF1B8cxiyBv8fFF1bB9+HpzU+Z5sCIB91XdTs21XaaLHXDg2qJZ
  17. E9SjmfIK3IqG1AYN6CFsEUX0wCloHI1Ua3XtClBQAJaxbsvRdk0SM0nJjPTxsC9MX
  18. ZGVlDxPR0RZSA5XD93OYrF28ZWnGMhSyet2zEUSEcLmAKlCO1WJbGzgq9Prd+HvRL
  19. UJqjbc1FjmPN/ZllnxHiP3/N7Xq307hUmar8lNF4FcLjXcaMzA4umMBUcY2wMxugc
  20. ENsHEkIBh9jzOFnvRDP1K0T44ZVR4l376Qm3nb2ULcvTA1E2Dq5U0H3t1AuniPgCP
  21. D62RRZOp3ZH+5RNQnsMHPL87eyK0XA/VAT7wQah9GnL1d54ocXDyDrVGrJXEiq8dN
  22. J2Zrd6Dd/7TAI1lcPuwUv4BxPP2o2ecsFVC2wi49v/HNPwrpGaRIocVG7XB2/tQvA
  23. A==
  24. </ViaThinkSoftSignature> */ ?>
  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.  *
  32.  * Revision 2022-12-18
  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
  42.  * 2022-12-18   1.2.2 Use the new cache directory now
  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.  
  83.                 $this->cacheFile = $this->get_cache_dir() . '/.last_ip_cache';
  84.                 if (!file_exists($this->cacheFile)) @touch($this->cacheFile);
  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.                 }
  134.                 preg_match_all('@^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$@ismU', $cont, $m, PREG_SET_ORDER);
  135.                 foreach ($m as $key => &$a) {
  136.                         if (($a[2] === 'system') && ($a[3] === 'boot')) {
  137.                                 // reboot   system boot  4.9.0-8-amd64    Fri Oct 12 02:10   still running
  138.                                 unset($m[$key]);
  139.                         } else if ($a[2] === 'begins') {
  140.                                 // wtmp.1 begins Fri Oct 12 02:10:43 2018
  141.                                 unset($m[$key]);
  142.                         } else {
  143.                                 array_shift($a);
  144.                         }
  145.                 }
  146.                 return $m;
  147.         }
  148.  
  149.         protected function cbRun() {
  150.                 if (!`which which`) {
  151.                         throw new VNagException("Program 'which' is not installed on your system");
  152.                 }
  153.  
  154.                 if (!`which last`) {
  155.                         throw new VNagException("Program 'last' (usually included in package smartmontools) is not installed on your system");
  156.                 }
  157.  
  158.                 $username = $this->argUser->available() ? $this->argUser->getValue() : '';
  159.                 $regex = $this->argRegex->available() ? $this->argRegex->getValue() : null;
  160.  
  161.                 if (($username != '') && function_exists('posix_getpwnam') && !posix_getpwnam($username)) {
  162.                         $this->setStatus(VNag::STATUS_WARNING);
  163.                         $this->addVerboseMessage("WARNING: Currently, there is no Linux user with name '$username'.", VNag::VERBOSITY_SUMMARY);
  164.                 }
  165.  
  166.                 $count_total = 0;
  167.                 $count_ok = 0;
  168.                 $count_warning = 0;
  169.  
  170.                 foreach ($this->getLastLoginIPs($username) as list($username, $pts, $ip, $times)) {
  171.                         // IP ":pts/0:S.0" means that there is a screen session
  172.                         if (strpos($ip,':pts/') === 0) continue;
  173.  
  174.                         $count_total++;
  175.  
  176.                         $fields = $this->getCountryAndOrg($ip);
  177.                         $fields[] = $ip;
  178.  
  179.                         if (is_null($regex)) {
  180.                                 // No regex. Just show the logins for information (status stays VNag::STATUS_UNKNOWN)
  181.                                 $this->addVerboseMessage("INFO: ".implode(' ',$fields)." @ $username, $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
  182.                         } else {
  183.                                 $match = false;
  184.                                 foreach ($fields as $f) {
  185.                                         if (preg_match($regex, $f, $dummy)) {
  186.                                                 $match = true;
  187.                                                 break;
  188.                                         }
  189.                                 }
  190.  
  191.                                 if ($match) {
  192.                                         $count_ok++;
  193.                                         $this->addVerboseMessage("OK: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
  194.                                         $this->setStatus(VNag::STATUS_OK);
  195.                                 } else {
  196.                                         $count_warning++;
  197.                                         $this->addVerboseMessage("WARNING: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_SUMMARY);
  198.                                         $this->setStatus(VNag::STATUS_WARNING);
  199.                                 }
  200.                         }
  201.                 }
  202.  
  203.                 if (is_null($regex)) {
  204.                         $this->setHeadline("Checked $count_total logins (No checks done because argument 'Regex' is missing)");
  205.                 } else {
  206.                         if (($count_total == 0) && ($this->argEmptyOk->count() > 0)) {
  207.                                 $this->setStatus(VNag::STATUS_OK);
  208.                         }
  209.                         $this->setHeadline("Checked $count_total logins ($count_ok OK, $count_warning Warning)");
  210.                 }
  211.         }
  212. }
  213.