Subversion Repositories vnag

Rev

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

  1. <?php /* <ViaThinkSoftSignature>
  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==
  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 2023-10-13
  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.  
  73.                 $this->cacheFile = $this->get_cache_dir().'/'.hash('sha256','LastCheck:last_ip_cache');
  74.                 if (!file_exists($this->cacheFile)) @touch($this->cacheFile);
  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");
  92.                 $cont = $this->url_get_contents($url);
  93.                 if ($cont === false) return array();
  94.                 if (($data = @json_decode($cont, true)) === false) return array();
  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.                 }
  124.  
  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
  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)
  130.                                 unset($m[$key]);
  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)
  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)) {
  165.                         // IP ":pts/0:S.0" means that there is a screen session
  166.                         if (strpos($ip,':pts/') === 0) continue;
  167.  
  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. }
  207.