diff --git a/app/Controller/UsersController.php b/app/Controller/UsersController.php index 94a00155d..01d83c187 100644 --- a/app/Controller/UsersController.php +++ b/app/Controller/UsersController.php @@ -3179,7 +3179,7 @@ class UsersController extends AppController // We didn't filter the data at SQL query too much, nor by age, as we want to show "enough" data, even if old $rows = 0; // group authentications by type of loginprofile, to make the list shorter - foreach($logs as $logEntry) { + foreach ($logs as $logEntry) { $loginProfile = $this->UserLoginProfile->_fromLog($logEntry['Log']); if (!$loginProfile) continue; // skip if empty log $loginProfile['ip'] = $logEntry['Log']['ip'] ?? null; // transitional workaround diff --git a/app/Model/User.php b/app/Model/User.php index d56899f19..1abdcd166 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -12,6 +12,7 @@ App::uses('BlowfishConstantPasswordHasher', 'Controller/Component/Auth'); * @property Organisation $Organisation * @property Role $Role * @property UserSetting $UserSetting + * @property UserLoginProfile $UserLoginProfile * @property Event $Event * @property AuthKey $AuthKey * @property Server $Server diff --git a/app/Model/UserLoginProfile.php b/app/Model/UserLoginProfile.php index 35c8d0c2a..080987e09 100644 --- a/app/Model/UserLoginProfile.php +++ b/app/Model/UserLoginProfile.php @@ -1,7 +1,9 @@ '/^(trusted|malicious)$/', 'message' => 'Must be one of: trusted, malicious' ], - ]; public $order = array("UserLoginProfile.id" => "DESC"); public $belongsTo = [ - 'User' => [ - 'className' => 'User', - 'foreignKey' => 'user_id', - 'conditions' => '', - 'fields' => '', - 'order' => '' - ]]; + 'User' => [ + 'className' => 'User', + 'foreignKey' => 'user_id', + 'conditions' => '', + 'fields' => '', + 'order' => '' + ] + ]; - protected $browscapCacheDir = APP . DS . 'tmp' . DS . 'browscap'; - protected $browscapIniFile = APP . DS . 'files' . DS . 'browscap'. DS . 'browscap.ini'; // Browscap file managed by MISP - https://browscap.org/stream?q=Lite_PHP_BrowsCapINI - protected $geoIpDbFile = APP . DS . 'files' . DS . 'geo-open' . DS . 'GeoOpen-Country.mmdb'; // GeoIP file managed by MISP - https://data.public.lu/en/datasets/geo-open-ip-address-geolocation-per-country-in-mmdb-format/ + const BROWSER_CACHE_DIR = APP . DS . 'tmp' . DS . 'browscap'; + const BROWSER_INI_FILE = APP . DS . 'files' . DS . 'browscap'. DS . 'browscap.ini'; // Browscap file managed by MISP - https://browscap.org/stream?q=Lite_PHP_BrowsCapINI + const GEOIP_DB_FILE = APP . DS . 'files' . DS . 'geo-open' . DS . 'GeoOpen-Country.mmdb'; // GeoIP file managed by MISP - https://data.public.lu/en/datasets/geo-open-ip-address-geolocation-per-country-in-mmdb-format/ + + private $userProfile; private $knownUserProfiles = []; - public function _buildBrowscapCache() { + public function _buildBrowscapCache() + { $this->log("Browscap - building new cache from browscap.ini file.", "info"); - $fileCache = new \Doctrine\Common\Cache\FilesystemCache($this->browscapCacheDir); + $fileCache = new \Doctrine\Common\Cache\FilesystemCache(UserLoginProfile::BROWSER_CACHE_DIR); $cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache); $logger = new \Monolog\Logger('name'); $bc = new \BrowscapPHP\BrowscapUpdater($cache, $logger); - $bc->convertFile($this->browscapIniFile); + $bc->convertFile(UserLoginProfile::BROWSER_INI_FILE); } public function beforeSave($options = []) @@ -56,7 +61,8 @@ class UserLoginProfile extends AppModel return true; } - public function hash($data) { + public function hash($data) + { unset($data['hash']); unset($data['created_at']); return md5(serialize($data)); @@ -66,12 +72,13 @@ class UserLoginProfile extends AppModel * slow function - don't call it too often * @return array */ - public function _getUserProfile() { + public function _getUserProfile() + { if (!$this->userProfile) { // below uses https://github.com/browscap/browscap-php if (class_exists('\BrowscapPHP\Browscap')) { try { - $fileCache = new \Doctrine\Common\Cache\FilesystemCache($this->browscapCacheDir); + $fileCache = new \Doctrine\Common\Cache\FilesystemCache(UserLoginProfile::BROWSER_CACHE_DIR); $cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache); $logger = new \Monolog\Logger('name'); $bc = new \BrowscapPHP\Browscap($cache, $logger); @@ -82,7 +89,7 @@ class UserLoginProfile extends AppModel } } else { // a primitive OS & browser extraction capability - $ua = env('HTTP_USER_AGENT'); + $ua = $_SERVER['HTTP_USER_AGENT'] ?? null; $browser = new stdClass(); $browser->browser_name_pattern = $ua; if (mb_strpos($ua, 'Linux') !== false) $browser->platform = "Linux"; @@ -95,16 +102,16 @@ class UserLoginProfile extends AppModel } $ip = $this->_remoteIp(); if (class_exists('GeoIp2\Database\Reader')) { - $geoDbReader = new GeoIp2\Database\Reader($this->geoIpDbFile); + $geoDbReader = new GeoIp2\Database\Reader(UserLoginProfile::GEOIP_DB_FILE); $record = $geoDbReader->country($ip); $country = $record->country->isoCode; } else { $country = 'None'; } $this->userProfile = [ - 'user_agent' => env('HTTP_USER_AGENT'), + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, 'ip' => $ip, - 'accept_lang' => env('HTTP_ACCEPT_LANGUAGE'), + 'accept_lang' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null, 'geoip' => $country, 'ua_pattern' => $browser->browser_name_pattern, 'ua_platform' => $browser->platform, @@ -114,16 +121,20 @@ class UserLoginProfile extends AppModel return $this->userProfile; } - public function _fromLog($logEntry) { - $data = json_decode('{"user_agent": "", "ip": "", "accept_lang":"", "geoip":"", "ua_pattern":"", "ua_platform":"", "ua_browser":""}', true); - $data = array_merge($data, json_decode($logEntry['change'], true) ?? []); + public function _fromLog($logEntry) + { + $data = ["user_agent" => "", "ip" => "", "accept_lang" => "", "geoip" => "", "ua_pattern" => "", "ua_platform" => "", "ua_browser" => ""]; + $data = array_merge($data, JsonTool::decode($logEntry['change']) ?? []); $data['ip'] = $logEntry['ip']; $data['timestamp'] = $logEntry['created']; - if ($data['user_agent'] == "") return false; + if ($data['user_agent'] == "") { + return false; + } return $data; } - public function _isSimilar($a, $b) { + public function _isSimilar($a, $b) + { // if one is not initialized if (!$a || !$b) return false; // transition for old logs where UA was not known @@ -146,7 +157,8 @@ class UserLoginProfile extends AppModel return false; } - public function _isIdentical($a, $b) { + public function _isIdentical($a, $b) + { if ($a['ip'] == $b['ip'] && $a['ua_browser'] == $b['ua_browser'] && $a['ua_platform'] == $b['ua_platform'] && @@ -157,7 +169,8 @@ class UserLoginProfile extends AppModel return false; } - public function _getTrustStatus($userProfileToCheck, $user_id = null) { + public function _getTrustStatus($userProfileToCheck, $user_id = null) + { if (!$user_id) { $user_id = AuthComponent::user('id'); } @@ -183,17 +196,19 @@ class UserLoginProfile extends AppModel return 'unknown'; } - public function _isTrusted() { + public function _isTrusted() + { if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'trusted') !== false) { return true; } return false; } - public function _isSuspicious() { + public function _isSuspicious() + { // previously marked loginuserprofile as malicious by the user if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'malicious') !== false) { - return _('A user reported a similar login profile as malicious.'); + return __('A user reported a similar login profile as malicious.'); } // same IP as previous malicious user $maliciousWithSameIP = $this->find('first', [ @@ -205,7 +220,7 @@ class UserLoginProfile extends AppModel 'fields' => array('UserLoginProfile.*')] ); if ($maliciousWithSameIP) { - return _('The source IP was reported as as malicious by a user.'); + return __('The source IP was reported as as malicious by a user.'); } // LATER - use other data to identify suspicious logins, such as: // - what with use-case where a user marks something as legitimate, but is marked by someone else as suspicious? @@ -214,7 +229,8 @@ class UserLoginProfile extends AppModel return false; } - public function email_newlogin($user) { + public function email_newlogin($user) + { if (!Configure::read('MISP.disable_emailing')) { $date_time = date('c'); @@ -233,7 +249,8 @@ class UserLoginProfile extends AppModel } } - public function email_report_malicious($user, $userLoginProfile) { + public function email_report_malicious($user, $userLoginProfile) + { // inform the org admin $date_time = $userLoginProfile['timestamp']; // LATER not ideal as timestamp is string without timezone info $body = new SendEmailTemplate('userloginprofile_report_malicious'); @@ -259,7 +276,8 @@ class UserLoginProfile extends AppModel } } - public function email_suspicious($user, $suspiciousness_reason) { + public function email_suspicious($user, $suspiciousness_reason) + { if (!Configure::read('MISP.disable_emailing')) { $date_time = date('c'); // inform the user @@ -300,6 +318,4 @@ class UserLoginProfile extends AppModel } } } - - }