mirror of https://github.com/MISP/MISP
chg: [internal] Code cleanup for user login profile
parent
f8632849c6
commit
786becad1a
|
@ -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
|
// 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;
|
$rows = 0;
|
||||||
// group authentications by type of loginprofile, to make the list shorter
|
// 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']);
|
$loginProfile = $this->UserLoginProfile->_fromLog($logEntry['Log']);
|
||||||
if (!$loginProfile) continue; // skip if empty log
|
if (!$loginProfile) continue; // skip if empty log
|
||||||
$loginProfile['ip'] = $logEntry['Log']['ip'] ?? null; // transitional workaround
|
$loginProfile['ip'] = $logEntry['Log']['ip'] ?? null; // transitional workaround
|
||||||
|
|
|
@ -12,6 +12,7 @@ App::uses('BlowfishConstantPasswordHasher', 'Controller/Component/Auth');
|
||||||
* @property Organisation $Organisation
|
* @property Organisation $Organisation
|
||||||
* @property Role $Role
|
* @property Role $Role
|
||||||
* @property UserSetting $UserSetting
|
* @property UserSetting $UserSetting
|
||||||
|
* @property UserLoginProfile $UserLoginProfile
|
||||||
* @property Event $Event
|
* @property Event $Event
|
||||||
* @property AuthKey $AuthKey
|
* @property AuthKey $AuthKey
|
||||||
* @property Server $Server
|
* @property Server $Server
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
App::uses('AppModel', 'Model');
|
App::uses('AppModel', 'Model');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property User $User
|
||||||
|
*/
|
||||||
class UserLoginProfile extends AppModel
|
class UserLoginProfile extends AppModel
|
||||||
{
|
{
|
||||||
public $actsAs = array(
|
public $actsAs = array(
|
||||||
|
@ -20,34 +22,37 @@ class UserLoginProfile extends AppModel
|
||||||
'rule' => '/^(trusted|malicious)$/',
|
'rule' => '/^(trusted|malicious)$/',
|
||||||
'message' => 'Must be one of: trusted, malicious'
|
'message' => 'Must be one of: trusted, malicious'
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public $order = array("UserLoginProfile.id" => "DESC");
|
public $order = array("UserLoginProfile.id" => "DESC");
|
||||||
|
|
||||||
public $belongsTo = [
|
public $belongsTo = [
|
||||||
'User' => [
|
'User' => [
|
||||||
'className' => 'User',
|
'className' => 'User',
|
||||||
'foreignKey' => 'user_id',
|
'foreignKey' => 'user_id',
|
||||||
'conditions' => '',
|
'conditions' => '',
|
||||||
'fields' => '',
|
'fields' => '',
|
||||||
'order' => ''
|
'order' => ''
|
||||||
]];
|
]
|
||||||
|
];
|
||||||
|
|
||||||
protected $browscapCacheDir = APP . DS . 'tmp' . DS . 'browscap';
|
const BROWSER_CACHE_DIR = 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
|
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
|
||||||
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 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 = [];
|
private $knownUserProfiles = [];
|
||||||
|
|
||||||
public function _buildBrowscapCache() {
|
public function _buildBrowscapCache()
|
||||||
|
{
|
||||||
$this->log("Browscap - building new cache from browscap.ini file.", "info");
|
$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);
|
$cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache);
|
||||||
|
|
||||||
$logger = new \Monolog\Logger('name');
|
$logger = new \Monolog\Logger('name');
|
||||||
$bc = new \BrowscapPHP\BrowscapUpdater($cache, $logger);
|
$bc = new \BrowscapPHP\BrowscapUpdater($cache, $logger);
|
||||||
$bc->convertFile($this->browscapIniFile);
|
$bc->convertFile(UserLoginProfile::BROWSER_INI_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function beforeSave($options = [])
|
public function beforeSave($options = [])
|
||||||
|
@ -56,7 +61,8 @@ class UserLoginProfile extends AppModel
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hash($data) {
|
public function hash($data)
|
||||||
|
{
|
||||||
unset($data['hash']);
|
unset($data['hash']);
|
||||||
unset($data['created_at']);
|
unset($data['created_at']);
|
||||||
return md5(serialize($data));
|
return md5(serialize($data));
|
||||||
|
@ -66,12 +72,13 @@ class UserLoginProfile extends AppModel
|
||||||
* slow function - don't call it too often
|
* slow function - don't call it too often
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function _getUserProfile() {
|
public function _getUserProfile()
|
||||||
|
{
|
||||||
if (!$this->userProfile) {
|
if (!$this->userProfile) {
|
||||||
// below uses https://github.com/browscap/browscap-php
|
// below uses https://github.com/browscap/browscap-php
|
||||||
if (class_exists('\BrowscapPHP\Browscap')) {
|
if (class_exists('\BrowscapPHP\Browscap')) {
|
||||||
try {
|
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);
|
$cache = new \Roave\DoctrineSimpleCache\SimpleCacheAdapter($fileCache);
|
||||||
$logger = new \Monolog\Logger('name');
|
$logger = new \Monolog\Logger('name');
|
||||||
$bc = new \BrowscapPHP\Browscap($cache, $logger);
|
$bc = new \BrowscapPHP\Browscap($cache, $logger);
|
||||||
|
@ -82,7 +89,7 @@ class UserLoginProfile extends AppModel
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// a primitive OS & browser extraction capability
|
// a primitive OS & browser extraction capability
|
||||||
$ua = env('HTTP_USER_AGENT');
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
$browser = new stdClass();
|
$browser = new stdClass();
|
||||||
$browser->browser_name_pattern = $ua;
|
$browser->browser_name_pattern = $ua;
|
||||||
if (mb_strpos($ua, 'Linux') !== false) $browser->platform = "Linux";
|
if (mb_strpos($ua, 'Linux') !== false) $browser->platform = "Linux";
|
||||||
|
@ -95,16 +102,16 @@ class UserLoginProfile extends AppModel
|
||||||
}
|
}
|
||||||
$ip = $this->_remoteIp();
|
$ip = $this->_remoteIp();
|
||||||
if (class_exists('GeoIp2\Database\Reader')) {
|
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);
|
$record = $geoDbReader->country($ip);
|
||||||
$country = $record->country->isoCode;
|
$country = $record->country->isoCode;
|
||||||
} else {
|
} else {
|
||||||
$country = 'None';
|
$country = 'None';
|
||||||
}
|
}
|
||||||
$this->userProfile = [
|
$this->userProfile = [
|
||||||
'user_agent' => env('HTTP_USER_AGENT'),
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'accept_lang' => env('HTTP_ACCEPT_LANGUAGE'),
|
'accept_lang' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null,
|
||||||
'geoip' => $country,
|
'geoip' => $country,
|
||||||
'ua_pattern' => $browser->browser_name_pattern,
|
'ua_pattern' => $browser->browser_name_pattern,
|
||||||
'ua_platform' => $browser->platform,
|
'ua_platform' => $browser->platform,
|
||||||
|
@ -114,16 +121,20 @@ class UserLoginProfile extends AppModel
|
||||||
return $this->userProfile;
|
return $this->userProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _fromLog($logEntry) {
|
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) ?? []);
|
$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['ip'] = $logEntry['ip'];
|
||||||
$data['timestamp'] = $logEntry['created'];
|
$data['timestamp'] = $logEntry['created'];
|
||||||
if ($data['user_agent'] == "") return false;
|
if ($data['user_agent'] == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _isSimilar($a, $b) {
|
public function _isSimilar($a, $b)
|
||||||
|
{
|
||||||
// if one is not initialized
|
// if one is not initialized
|
||||||
if (!$a || !$b) return false;
|
if (!$a || !$b) return false;
|
||||||
// transition for old logs where UA was not known
|
// transition for old logs where UA was not known
|
||||||
|
@ -146,7 +157,8 @@ class UserLoginProfile extends AppModel
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _isIdentical($a, $b) {
|
public function _isIdentical($a, $b)
|
||||||
|
{
|
||||||
if ($a['ip'] == $b['ip'] &&
|
if ($a['ip'] == $b['ip'] &&
|
||||||
$a['ua_browser'] == $b['ua_browser'] &&
|
$a['ua_browser'] == $b['ua_browser'] &&
|
||||||
$a['ua_platform'] == $b['ua_platform'] &&
|
$a['ua_platform'] == $b['ua_platform'] &&
|
||||||
|
@ -157,7 +169,8 @@ class UserLoginProfile extends AppModel
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _getTrustStatus($userProfileToCheck, $user_id = null) {
|
public function _getTrustStatus($userProfileToCheck, $user_id = null)
|
||||||
|
{
|
||||||
if (!$user_id) {
|
if (!$user_id) {
|
||||||
$user_id = AuthComponent::user('id');
|
$user_id = AuthComponent::user('id');
|
||||||
}
|
}
|
||||||
|
@ -183,17 +196,19 @@ class UserLoginProfile extends AppModel
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _isTrusted() {
|
public function _isTrusted()
|
||||||
|
{
|
||||||
if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'trusted') !== false) {
|
if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'trusted') !== false) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _isSuspicious() {
|
public function _isSuspicious()
|
||||||
|
{
|
||||||
// previously marked loginuserprofile as malicious by the user
|
// previously marked loginuserprofile as malicious by the user
|
||||||
if (strpos($this->_getTrustStatus($this->_getUserProfile()), 'malicious') !== false) {
|
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
|
// same IP as previous malicious user
|
||||||
$maliciousWithSameIP = $this->find('first', [
|
$maliciousWithSameIP = $this->find('first', [
|
||||||
|
@ -205,7 +220,7 @@ class UserLoginProfile extends AppModel
|
||||||
'fields' => array('UserLoginProfile.*')]
|
'fields' => array('UserLoginProfile.*')]
|
||||||
);
|
);
|
||||||
if ($maliciousWithSameIP) {
|
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:
|
// 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?
|
// - 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function email_newlogin($user) {
|
public function email_newlogin($user)
|
||||||
|
{
|
||||||
if (!Configure::read('MISP.disable_emailing')) {
|
if (!Configure::read('MISP.disable_emailing')) {
|
||||||
$date_time = date('c');
|
$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
|
// inform the org admin
|
||||||
$date_time = $userLoginProfile['timestamp']; // LATER not ideal as timestamp is string without timezone info
|
$date_time = $userLoginProfile['timestamp']; // LATER not ideal as timestamp is string without timezone info
|
||||||
$body = new SendEmailTemplate('userloginprofile_report_malicious');
|
$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')) {
|
if (!Configure::read('MISP.disable_emailing')) {
|
||||||
$date_time = date('c');
|
$date_time = date('c');
|
||||||
// inform the user
|
// inform the user
|
||||||
|
@ -300,6 +318,4 @@ class UserLoginProfile extends AppModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue