Subversion Repositories vnag

Rev

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

  1. <?php /* <ViaThinkSoftSignature>akRkqMquPkfxpwy3lnAlcQ1qVAUdTJBHZNOG+o13yNrgyHRqadEskkSxmItifQzCENCOBCXf4QUqo8xtFT0uxyzv/R0ZrB8H0dF+2LslQ6eYj+IrPgbiQoFY4FoxAS24ZCpH8v+GYD4lq/UmU+c7eggeNGtv9ZYkQaz2ZUS4hT0nrP7qO2quF1dpm+8dsB5pKj9iY/ujHP9iEWzn3d8BhgkaSed+GBkqurqY1lc+2l2tCsT1ZPEOR41uWLeSya1WGwEAWy6/2ih4x5dG3lsNgKRcJyDdcDYw+mhXq3vxPeqcMWFiplq6cF9XxxY3NHOk6HcIO7qR52jOkU7Kvy31CPso0C12h0xFgo9fvj3BJUJWZvSw00I1SqRMJn2o2+KtVCWSfMGJ5brgRCMNdHDiCCbkFaRtElsv1q0ewTLotrRohZxJpRwoOK7dEDsR+V0hhOfYzZ/zRaNqG/LPIqSeBsZhA7km8IpZlwSbUom2PKKWdL8YBwSdqjtgM9Z3B0TQDkhSvU17mICuIgjQfeMwxyaUHFYvA+yqofcy9g2GQHTj15B2s39gnW9pKgVK35lRKOCoQ6rYptBfHUXH02VjSLqcFgoV7cOamBftfC55+RRpvfZURQUvgWbBIYtQ3HDeze775vdbr85HL6ADKA2Rdd/HSG1E+GMuXUokcWOxg80ih+HnnrGqfFcohmfs1qy6ZZtYQBnJszFZATsMF+AdztxJahbVf7lrM36Zl22kXpkz8lX2lp382bBvBBYwWUaf3j+UBLWHoFfnOjc1BnxECvuAIlgiD7mRIYIx/WQxlF1MU3ByrVuIzfOR2j3Xy+Iv0ettCVNSQdCnN+B2WV/Hqtj1qLsXgZcxdYatlqDKLytyk2bSyX/WLnRj5xnjWEGDPAoblIjknHz9BiDWRYNdSqZ1skixKIZTbynhqWrG9dKSPvDfdXpvguRB6tSWK3/70aOfWf3TnmTBHXS3/NvGzV+3WtZfujCXTxBSCuV6ReTIuqUbKoVT/rCX5sMNMDuElsbHjg031VKNyqjx3L9F/fMopue8G1vu2QCsjiGU59EwtakcrobMyopZwV8eqe3/By7SjVVL11MquTEV+qIWV3gNtlFFroCO+ztVdKj1+FMsZtZfDhHkxeHG/sx64CW7B1mw5pPCr3FriDScWy3XfD29ah1Tx0/6AsHMViLNCMK65bP5ttp7BBb2vnGX68vc9qMNCBCIhR9lOwPobtzB4MpVAHX6dEn/3XVhkoW7O7jcz3CkgZOZJv8ymKE4SUw7s/ns6w/m4wyujDdp9343WuALsxrxXMswqiTx7v1hmeJccT9dRMJYGQ7DRnB8Z1K+px6FALfwMg7OMcbQh+wiwg==</ViaThinkSoftSignature> */ ?>
  2. <?php
  3.  
  4. /*
  5.  * VNag - Nagios Framework for PHP
  6.  * Developed by Daniel Marschall, ViaThinkSoft <www.viathinksoft.com>
  7.  * Licensed under the terms of the Apache 2.0 license
  8.  *
  9.  * Revision 2018-11-03
  10.  *
  11.  * Changelog:
  12.  * 2018-08-01   1.0   Initial release
  13.  * 2018-09-02   1.1   Added argument -e|--emptyok
  14.  *                    Output a warning if the Linux user does not exist.
  15.  * 2018-10-01   1.2   Fixed a bug where too many unnecessary requests were sent to ipinfo.io
  16.  *                    Cache file location ~/.last_ipcache is now preferred
  17.  *                    A token for ipinfo.io can now be provided
  18.  * 2018-11-03   1.2.1 "system boot" lines are now excluded
  19.  */
  20.  
  21. // QUE: should we allow usernames to have wildcards, regexes or comma separated?
  22.  
  23. declare(ticks=1);
  24.  
  25. class LastCheck extends VNag {
  26.         private $argUser = null;
  27.         private $argRegex = null;
  28.         private $argWtmpFiles = null;
  29.         private $argEmptyOk = null;
  30.         private $argIpInfoToken = null;
  31.  
  32.         private $cache = null;
  33.         private $cacheFile = null;
  34.         private $cacheDirty = false;
  35.  
  36.         protected function getIpCacheFile() {
  37.                 $homedir = @getenv('HOME');
  38.                 if ($homedir) {
  39.                         $try = "${homedir}/.last_ipcache";
  40.                         if (file_exists($try)) return $try;
  41.                         if (@touch($try)) return $try;
  42.                 }
  43.  
  44.                 $user = posix_getpwuid(posix_geteuid());
  45.                 if (isset($user['dir'])) {
  46.                         $homedir = $user['dir'];
  47.                         $try = "${homedir}/.last_ipcache";
  48.                         if (file_exists($try)) return $try;
  49.                         if (@touch($try)) return $try;
  50.                 }
  51.  
  52.                 if (isset($user['name'])) {
  53.                         $username = $user['name'];
  54.                         $try = "/tmp/ipcache_${username}.tmp";
  55.                         if (file_exists($try)) return $try;
  56.                         if (@touch($try)) return $try;
  57.                 }
  58.  
  59.                 return false; // should usually never happen
  60.         }
  61.  
  62.         public function __construct() {
  63.                 parent::__construct();
  64.  
  65.                 if ($this->is_http_mode()) {
  66.                         // Don't allow the standard arguments via $_REQUEST
  67.                         $this->registerExpectedStandardArguments('');
  68.                 } else {
  69.                         $this->registerExpectedStandardArguments('Vhtv');
  70.                 }
  71.  
  72.                 $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));
  73.                 $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));
  74.                 $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*'));
  75.                 $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));
  76.                 $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));
  77.  
  78.                 $this->getHelpManager()->setPluginName('vnag_last');
  79.                 $this->getHelpManager()->setVersion('1.2');
  80.                 $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.');
  81.                 $this->getHelpManager()->setCopyright('Copyright (C) 2011-$CURYEAR$ Daniel Marschall, ViaThinkSoft.');
  82.                 $this->getHelpManager()->setSyntax('$SCRIPTNAME$ [-v] [-e] [-u username] [-R regex] [--ipInfoToken token]');
  83.                 $this->getHelpManager()->setFootNotes('If you encounter bugs, please contact ViaThinkSoft at www.viathinksoft.com');
  84.  
  85.                 $this->cacheFile = $this->getIpCacheFile();
  86.                 $this->cache = $this->cacheFile ? json_decode(file_get_contents($this->cacheFile),true) : array();
  87.         }
  88.  
  89.         public function __destruct() {
  90.                 if ($this->cacheFile && $this->cacheDirty) {
  91.                         @file_put_contents($this->cacheFile, json_encode($this->cache));
  92.                 }
  93.         }
  94.  
  95.         private function getCountryAndOrg($ip) {
  96.                 if (isset($this->cache[$ip])) return $this->cache[$ip];
  97.  
  98.                 $url = 'https://ipinfo.io/'.urlencode($ip).'/json';
  99.                 $token = $this->argIpInfoToken->getValue();
  100.                 if ($token) $url .= '?token='.urlencode($token);
  101.  
  102.                 // fwrite(STDERR, "Note: Will query $url\n");
  103.                 $cont = file_get_contents($url);
  104.                 if (!$cont) return array();
  105.                 if (!($data = @json_decode($cont, true))) return array();
  106.                 if (isset($data['error'])) return array();
  107.  
  108.                 if (isset($data['bogon']) && ($data['bogon'])) {
  109.                         // Things like 127.0.0.1 do not belong to anyone
  110.                         $res = array();
  111.                 } else {
  112.                         $res = array();
  113.                         if (isset($data['hostname'])) $res[] = $data['hostname'];
  114.                         if (isset($data['country'])) $res[] = $data['country'];
  115.                         list($as, $orgName) = explode(' ', $data['org'], 2);
  116.                         $res[] = $as;
  117.                         $res[] = $orgName;
  118.                 }
  119.  
  120.                 $this->cache[$ip] = $res;
  121.                 $this->cacheDirty = true;
  122.                 return $res;
  123.         }
  124.  
  125.         private function getLastLoginIPs($username) {
  126.                 $cont = '';
  127.                 $files = glob($this->argWtmpFiles->getValue());
  128.                 foreach ($files as $file) {
  129.                         if (trim($username) == '') {
  130.                                 $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '); // all users
  131.                         } else {
  132.                                 $cont .= shell_exec('last -f '.escapeshellarg($file).' -F -w '.escapeshellarg($username));
  133.                         }
  134.                 }
  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
  139.                                 unset($m[$key]);
  140.                         } else if ($a[2] === 'begins') {
  141.                                 // wtmp.1 begins Fri Oct 12 02:10:43 2018
  142.                                 unset($m[$key]);
  143.                         } else {
  144.                                 array_shift($a);
  145.                         }
  146.                 }
  147.                 return $m;
  148.         }
  149.  
  150.         protected function cbRun() {
  151.                 if (!`which which`) {
  152.                         throw new VNagException("Program 'which' is not installed on your system");
  153.                 }
  154.  
  155.                 if (!`which last`) {
  156.                         throw new VNagException("Program 'last' (usually included in package smartmontools) is not installed on your system");
  157.                 }
  158.  
  159.                 $username = $this->argUser->available() ? $this->argUser->getValue() : '';
  160.                 $regex = $this->argRegex->available() ? $this->argRegex->getValue() : null;
  161.  
  162.                 if (($username != '') && function_exists('posix_getpwnam') && !posix_getpwnam($username)) {
  163.                         $this->setStatus(VNag::STATUS_WARNING);
  164.                         $this->addVerboseMessage("WARNING: Currently, there is no Linux user with name '$username'.", VNag::VERBOSITY_SUMMARY);
  165.                 }
  166.  
  167.                 $count_total = 0;
  168.                 $count_ok = 0;
  169.                 $count_warning = 0;
  170.  
  171.                 foreach ($this->getLastLoginIPs($username) as list($username, $pts, $ip, $times)) {
  172.                         $count_total++;
  173.  
  174.                         $fields = $this->getCountryAndOrg($ip);
  175.                         $fields[] = $ip;
  176.  
  177.                         if (is_null($regex)) {
  178.                                 // No regex. Just show the logins for information (status stays VNag::STATUS_UNKNOWN)
  179.                                 $this->addVerboseMessage("INFO: ".implode(' ',$fields)." @ $username, $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
  180.                         } else {
  181.                                 $match = false;
  182.                                 foreach ($fields as $f) {
  183.                                         if (preg_match($regex, $f, $dummy)) {
  184.                                                 $match = true;
  185.                                                 break;
  186.                                         }
  187.                                 }
  188.  
  189.                                 if ($match) {
  190.                                         $count_ok++;
  191.                                         $this->addVerboseMessage("OK: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
  192.                                         $this->setStatus(VNag::STATUS_OK);
  193.                                 } else {
  194.                                         $count_warning++;
  195.                                         $this->addVerboseMessage("WARNING: ".implode(' ',$fields)." @ $username $pts $times", VNag::VERBOSITY_SUMMARY);
  196.                                         $this->setStatus(VNag::STATUS_WARNING);
  197.                                 }
  198.                         }
  199.                 }
  200.  
  201.                 if (is_null($regex)) {
  202.                         $this->setHeadline("Checked $count_total logins (No checks done because argument 'Regex' is missing)");
  203.                 } else {
  204.                         if (($count_total == 0) && ($this->argEmptyOk->count() > 0)) {
  205.                                 $this->setStatus(VNag::STATUS_OK);
  206.                         }
  207.                         $this->setHeadline("Checked $count_total logins ($count_ok OK, $count_warning Warning)");
  208.                 }
  209.         }
  210. }
  211.