diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index ebccc94ab..7b7372071 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -417,9 +417,12 @@ class AppController extends Controller } } if ($foundMispAuthKey) { - $authKeyToStore = substr($authKey, 0, 4) + $start = substr($authKey, 0, 4); + $end = substr($authKey, -4); + $authKeyToStore = $start . str_repeat('*', 32) - . substr($authKey, -4); + . $end; + $this->__logApiKeyUse($start . $end); if ($user) { // User found in the db, add the user info to the session if (Configure::read('MISP.log_auth')) { @@ -642,6 +645,15 @@ class AppController extends Controller return in_array($this->request->params['action'], $actionsToCheck[$controller], true); } + private function __logApiKeyUse($apikey) + { + $redis = $this->User->setupRedis(); + if (!$redis) { + return; + } + $redis->zIncrBy('misp:authkey_log:' . date("Ymd"), 1, $apikey); + } + /** * User access monitoring * @param array $user diff --git a/app/Controller/DashboardsController.php b/app/Controller/DashboardsController.php index a974be4c4..50fb894b4 100644 --- a/app/Controller/DashboardsController.php +++ b/app/Controller/DashboardsController.php @@ -316,6 +316,8 @@ class DashboardsController extends AppController public function listTemplates() { $conditions = array(); + // load all widgets for internal use, won't be displayed to the user. Thus we circumvent the ACL on it. + $accessible_widgets = array_keys($this->Dashboard->loadAllWidgets($this->Auth->user())); if (!$this->_isSiteAdmin()) { $permission_flags = array(); foreach ($this->Auth->user('Role') as $perm => $value) { @@ -394,6 +396,15 @@ class DashboardsController extends AppController } $element['Dashboard']['widgets'] = array_keys($widgets); sort($element['Dashboard']['widgets']); + $temp = []; + foreach ($element['Dashboard']['widgets'] as $widget) { + if (in_array($widget, $accessible_widgets)) { + $temp['allow'][] = $widget; + } else { + $temp['deny'][] = $widget; + } + } + $element['Dashboard']['widgets'] = $temp; if ($element['Dashboard']['user_id'] != $this->Auth->user('id')) { $element['User']['email'] = ''; } diff --git a/app/Lib/Dashboard/APIActivityWidget.php b/app/Lib/Dashboard/APIActivityWidget.php new file mode 100644 index 000000000..1b66c0e5e --- /dev/null +++ b/app/Lib/Dashboard/APIActivityWidget.php @@ -0,0 +1,131 @@ + 'A list of filters by organisation meta information (sector, type, nationality, id, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed APIkeys. (-1 will list all) Default: -1', + 'days' => 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + ]; + public $description = 'Basic widget showing some server statistics in regards to MISP.'; + public $cacheLifetime = 10; + public $autoRefreshDelay = null; + private $User = null; + private $AuthKey = null; + + + private function getDates($options) + { + if (!empty($options['days'])) { + $begin = new DateTime(date('Y-m-d', strtotime(sprintf("-%s days", $options['days'])))); + } else if (!empty($options['month'])) { + $begin = new DateTime(date('Y-m-d', strtotime('first day of this month 00:00:00', time()))); + } else if (!empty($options['year'])) { + $begin = new DateTime(date('Y-m-d', strtotime('first day of this year 00:00:00', time()))); + } else { + $begin = new DateTime(date('Y-m-d', strtotime('-7 days', time())));; + } + $now = new DateTime(); + $dates = new DatePeriod( + $begin, + new DateInterval('P1D'), + $now + ); + $results = []; + foreach ($dates as $date) { + $results[] = $date->format('Ymd'); + } + return $results; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $this->AuthKey = ClassRegistry::init('AuthKey'); + $redis = $this->User->setupRedis(); + if (!$redis) { + throw new NotFoundException(__('No redis connection found.')); + } + + $params = ['conditions' => []]; + $dates = $this->getDates($options); + $pipe = $redis->pipeline(); + foreach ($dates as $date) { + $pipe->zrange('misp:authkey_log:' . $date, 0, -1, true); + } + $temp = $pipe->exec(); + $raw_results = []; + $counts = []; + foreach ($dates as $k => $date) { + $raw_results[$date] = $temp[$k]; + if (!empty($temp[$k])) { + foreach ($temp[$k] as $key => $count) { + if (isset($counts[$key])) { + $counts[$key] += (int)$count; + } else { + $counts[$key] = (int)$count; + } + } + } + } + arsort($counts); + $this->AuthKey->Behaviors->load('Containable'); + $temp_apikeys = array_flip(array_keys($counts)); + foreach ($temp_apikeys as $apikey => $value) { + $temp_apikeys[$apikey] = $this->AuthKey->find('first', [ + 'conditions' => [ + 'AuthKey.authkey_start' => substr($apikey, 0, 4), + 'AuthKey.authkey_end' => substr($apikey, 4) + ], + 'fields' => ['AuthKey.authkey_start', 'AuthKey.authkey_end', 'AuthKey.id', 'User.id', 'User.email'], + 'recursive' => 1 + ]); + } + $baseurl = empty(Configure::read('MISP.external_baseurl')) ? h(Configure::read('MISP.baseurl')) : Configure::read('MISP.external_baseurl'); + foreach ($counts as $key => $junk) { + $data = $temp_apikeys[$key]; + if (!empty($data)) { + $results[] = [ + 'html_title' => sprintf( + '%s', + h($baseurl), + h($data['AuthKey']['id']), + $key + ), + 'html' => sprintf( + '%s (%s)', + h($counts[$key]), + h($baseurl), + h($data['User']['id']), + h($data['User']['email']) + ) + ]; + } else { + $results[] = [ + 'title' => $key, + 'html' => sprintf( + '%s (%s)', + h($counts[$key]), + __('An unknown key can be caused by the given key having been permanently deleted or falsely mis-identified (for the purposes of this widget) on instances using legacy API key authentication.'), + __('Unknown key') + ) + ]; + } + } + return $results; + } + + public function checkPermissions($user) + { + if (empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} diff --git a/app/Lib/Dashboard/LoginsWidget.php b/app/Lib/Dashboard/LoginsWidget.php new file mode 100644 index 000000000..7280d6863 --- /dev/null +++ b/app/Lib/Dashboard/LoginsWidget.php @@ -0,0 +1,88 @@ + 'A list of filters by organisation meta information (sector, type, nationality, id, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed APIkeys. (-1 will list all) Default: -1', + 'days' => 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + ]; + public $description = 'Basic widget showing some server statistics in regards to MISP.'; + public $cacheLifetime = 10; + public $autoRefreshDelay = null; + private $User = null; + private $Log = null; + + + private function getDates($options) + { + if (!empty($options['days'])) { + $begin = date('Y-m-d H:i:s', strtotime(sprintf("-%s days", $options['days']))); + } else if (!empty($options['month'])) { + $begin = date('Y-m-d H:i:s', strtotime('first day of this month 00:00:00', time())); + } else if (!empty($options['year'])) { + $begin = date('Y-m-d', strtotime('first day of this year 00:00:00', time())); + } else { + $begin = date('Y-m-d H:i:s', strtotime('-7 days', time())); + } + return $begin ? ['Log.created >=' => $begin] : []; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $this->Log = ClassRegistry::init('Log'); + $conditions = $this->getDates($options); + $conditions['Log.action'] = 'login'; + $this->Log->Behaviors->load('Containable'); + $this->Log->bindModel([ + 'belongsTo' => [ + 'User' + ] + ]); + $this->Log->virtualFields['count'] = 0; + $this->Log->virtualFields['email'] = ''; + $logs = $this->Log->find('all', [ + 'recursive' => -1, + 'conditions' => $conditions, + 'fields' => ['Log.user_id', 'COUNT(Log.id) AS Log__count', 'User.email AS Log__email'], + 'contain' => ['User'], + 'group' => ['Log.user_id'] + ]); + $counts = []; + $emails = []; + foreach ($logs as $log) { + $counts[$log['Log']['user_id']] = $log['Log']['count']; + $emails[$log['Log']['user_id']] = $log['Log']['email']; + } + $results = []; + arsort($counts); + $baseurl = empty(Configure::read('MISP.external_baseurl')) ? h(Configure::read('MISP.baseurl')) : Configure::read('MISP.external_baseurl'); + foreach ($counts as $user_id => $count) { + $results[] = [ + 'html_title' => sprintf( + '%s', + h($baseurl), + h($user_id), + h($emails[$user_id]) + ), + 'value' => $count + ]; + } + return $results; + } + + public function checkPermissions($user) + { + if (empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} diff --git a/app/Lib/Dashboard/NewOrgsWidget.php b/app/Lib/Dashboard/NewOrgsWidget.php new file mode 100644 index 000000000..881796d9c --- /dev/null +++ b/app/Lib/Dashboard/NewOrgsWidget.php @@ -0,0 +1,157 @@ + 'Maximum number of joining organisations shown. (integer, defaults to 10 if not set)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'days' => 'How many days back should the list go - for example, setting 7 will only show the organisations that were added in the past 7 days. (integer)', + 'month' => 'Which organisations have been added this month? (boolean)', + 'year' => 'Which organisations have been added this year? (boolean)', + 'local' => 'Should the list only show local organisations? (boolean or list of booleans, defaults to 1. To get both sets, use [0,1])', + 'fields' => 'Which fields should be displayed, by default all are selected. Pass a list with the following options: [id, uuid, name, sector, type, nationality, creation_date]' + ]; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + + public $placeholder = + '{ + "limit": 5, + "filter": { + "nationality": [ + "Hungary", + "Russia", + "North Korea" + ] + }, + "month": true +}'; + + private $Organisation = null; + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + $this->tableDescription = __('The %d newest organisations created in the past %d days', $limit, (int)$options['days']); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + $this->tableDescription = __('The %d newest organisations created during the current month', $limit); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + $this->tableDescription = __('The %d newest organisations created during the current year', $limit); + } else { + $this->tableDescription = __('The %d newest organisations created', $limit); + return null; + } + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + return $datetime->format('Y-m-d H:i:s'); + } + + public function handler($user, $options = array()) + { + $this->Organisation = ClassRegistry::init('Organisation'); + $field_options = [ + 'id' => [ + 'name' => '#', + 'url' => Configure::read('MISP.baseurl') . '/organisations/view', + 'element' => 'links', + 'data_path' => 'Organisation.id', + 'url_params_data_paths' => 'Organisation.id' + ], + 'date_created' => [ + 'name' => 'Creation date', + 'data_path' => 'Organisation.date_created' + ], + 'name' => [ + 'name' => 'Name', + 'data_path' => 'Organisation.name', + ], + 'uuid' => [ + 'name' => 'UUID', + 'data_path' => 'Organisation.uuid', + ], + 'sector' => [ + 'name' => 'Sector', + 'data_path' => 'Organisation.sector', + ], + 'nationality' => [ + 'name' => 'Nationality', + 'data_path' => 'Organisation.nationality', + ], + 'type' => [ + 'name' => 'Type', + 'data_path' => 'Organisation.type', + ] + ]; + $params = [ + 'conditions' => [ + 'AND' => ['Organisation.local' => !isset($options['local']) ? 1 : $options['local']] + ], + 'limit' => 10, + 'recursive' => -1 + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['Organisation.date_created >=' => $timeConditions]; + } + if (isset($options['fields'])) { + $fields = []; + foreach ($options['fields'] as $field) { + if (isset($field_options[$field])) { + $fields[$field] = $field_options[$field]; + } + } + } else { + $fields = $field_options; + } + $data = $this->Organisation->find('all', [ + 'recursive' => -1, + 'conditions' => $params['conditions'], + 'limit' => isset($options['limit']) ? (int)$options['limit'] : 10, + 'fields' => array_keys($fields), + 'order' => 'Organisation.date_created DESC' + ]); + + return [ + 'data' => $data, + 'fields' => $fields, + 'description' => $this->tableDescription + ]; + } +} diff --git a/app/Lib/Dashboard/NewUsersWidget.php b/app/Lib/Dashboard/NewUsersWidget.php new file mode 100644 index 000000000..f41d43570 --- /dev/null +++ b/app/Lib/Dashboard/NewUsersWidget.php @@ -0,0 +1,171 @@ + 'Maximum number of joining users shown. (integer, defaults to 10 if not set)', + 'filter' => 'A list of filters for the organisations (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'days' => 'How many days back should the list go - for example, setting 7 will only show the organisations that were added in the past 7 days. (integer)', + 'month' => 'Which organisations have been added this month? (boolean)', + 'year' => 'Which organisations have been added this year? (boolean)', + 'fields' => 'Which fields should be displayed, by default all are selected. Pass a list with the following options: [id, email, Organisation.name, Role.name, date_created]' + ]; + private $validFilterKeys = [ + 'id', + 'email', + 'Organisation.name', + 'Role.name', + 'date_created' + ]; + + public $placeholder = + '{ + "limit": 10, + "filter": { + "Organisation.name": [ + "!FSB", + "!GRU", + "!Kaspersky" + ], + "email": [ + "!andras.iklody@circl.lu" + ], + "Role.name": [ + "Publisher", + "User" + ] + }, + "year": true +}'; + + private $User = null; + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + $this->tableDescription = __('The %d newest users created in the past %d days', $limit, (int)$options['days']); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + $this->tableDescription = __('The %d newest users created during the current month', $limit); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + $this->tableDescription = __('The %d newest users created during the current year', $limit); + } else { + $this->tableDescription = __('The %d newest users created', $limit); + return null; + } + return $condition; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $field_options = [ + 'id' => [ + 'name' => '#', + 'url' => empty($user['Role']['perm_site_admin']) ? null : Configure::read('MISP.baseurl') . '/admin/users/view', + 'element' => 'links', + 'data_path' => 'User.id', + 'url_params_data_paths' => 'User.id' + ], + 'date_created' => [ + 'name' => 'Creation date', + 'data_path' => 'User.date_created' + ], + 'email' => [ + 'name' => 'E-mail', + 'data_path' => 'User.email', + ], + 'Organisation.name' => [ + 'name' => 'Organisation', + 'data_path' => 'Organisation.name', + ], + 'Role.name' => [ + 'name' => 'Role', + 'data_path' => 'Role.name', + ] + ]; + $params = [ + 'conditions' => [], + 'limit' => 10, + 'recursive' => -1 + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + $filterName = strpos($filterKey, '.') ? $filterKey : 'User.' . $filterKey; + if ($value[0] === '!') { + $tempConditionBucket[$filterName . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket[$filterName . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['User.date_created >=' => $timeConditions]; + } + if (isset($options['fields'])) { + $fields = []; + foreach ($options['fields'] as $field) { + if (isset($field_options[$field])) { + $fields[$field] = $field_options[$field]; + } + } + } else { + $fields = $field_options; + } + + // redact e-mails for non site admins unless specifically allowed + if ( + empty($user['Role']['perm_site_admin']) && + !Configure::read('Security.disclose_user_emails') && + isset($fields['email']) + ) { + unset($fields['email']); + } + $data = $this->User->find('all', [ + 'recursive' => -1, + 'contain' => ['Organisation.name', 'Role.name'], + 'conditions' => $params['conditions'], + 'limit' => isset($options['limit']) ? $options['limit'] : 10, + 'fields' => array_keys($fields), + 'order' => 'User.date_created DESC' + ]); + + foreach ($data as &$u) { + if (empty($u['User']['date_created'])) { + continue; + } + $tempDate = new DateTime(); + $tempDate->setTimestamp($u['User']['date_created']); + $u['User']['date_created'] = $tempDate->format('Y-m-d H:i:s'); + } + + return [ + 'data' => $data, + 'fields' => $fields, + 'description' => $this->tableDescription + ]; + } +} diff --git a/app/Lib/Dashboard/OrgContributionToplistWidget.php b/app/Lib/Dashboard/OrgContributionToplistWidget.php new file mode 100644 index 000000000..ed7079fe3 --- /dev/null +++ b/app/Lib/Dashboard/OrgContributionToplistWidget.php @@ -0,0 +1,107 @@ + 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + public $placeholder = +'{ + "days": "7d", + "threshold": 15, + "filter": { + "sector": "Financial" + } +}'; + private $Org = null; + private $Event = null; + + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + } else { + return null; + } + return $condition; + } + + + public function handler($user, $options = array()) + { + $params = ['conditions' => []]; + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['Event.timestamp >=' => $timeConditions]; + } + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + if (isset($options['filter']['local'])) { + $params['conditions']['AND']['local'] = $options['filter']['local']; + } + + $this->Org = ClassRegistry::init('Organisation'); + $org_ids = $this->Org->find('list', [ + 'fields' => ['Organisation.id', 'Organisation.name'], + 'conditions' => $params['conditions'] + ]); + $conditions = ['Event.orgc_id IN' => array_keys($org_ids)]; + $this->Event = ClassRegistry::init('Event'); + $this->Event->virtualFields['frequency'] = 0; + $orgs = $this->Event->find('all', [ + 'recursive' => -1, + 'fields' => ['orgc_id', 'count(Event.orgc_id) as Event__frequency'], + 'group' => ['orgc_id'], + 'conditions' => $conditions, + 'order' => 'count(Event.orgc_id) desc', + 'limit' => empty($options['limit']) ? 10 : $options['limit'] + ]); + $results = []; + foreach($orgs as $org) { + $results[$org_ids[$org['Event']['orgc_id']]] = $org['Event']['frequency']; + } + return ['data' => $results]; + } +} +?> diff --git a/app/Lib/Dashboard/OrganisationMapWidget.php b/app/Lib/Dashboard/OrganisationMapWidget.php new file mode 100644 index 000000000..77605aa85 --- /dev/null +++ b/app/Lib/Dashboard/OrganisationMapWidget.php @@ -0,0 +1,250 @@ + 'A list of filters by organisation meta information (sector, type, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'sector', + 'type', + 'local' + ]; + public $placeholder = +'{ + "type": "Member", + "local": [0,1] +}'; + private $Organisation = null; + + public $countryCodes = array( + 'Afghanistan' => 'AF', + 'Albania' => 'AL', + 'Algeria' => 'DZ', + 'Angola' => 'AO', + 'Argentina' => 'AR', + 'Armenia' => 'AM', + 'Australia' => 'AU', + 'Austria' => 'AT', + 'Azerbaijan' => 'AZ', + 'Bahamas' => 'BS', + 'Bangladesh' => 'BD', + 'Belarus' => 'BY', + 'Belgium' => 'BE', + 'Belize' => 'BZ', + 'Benin' => 'BJ', + 'Bhutan' => 'BT', + 'Bolivia' => 'BO', + 'Bosnia and Herz.' => 'BA', + 'Botswana' => 'BW', + 'Brazil' => 'BR', + 'Brunei' => 'BN', + 'Bulgaria' => 'BG', + 'Burkina Faso' => 'BF', + 'Burundi' => 'BI', + 'Cambodia' => 'KH', + 'Cameroon' => 'CM', + 'Canada' => 'CA', + 'Central African Rep.' => 'CF', + 'Chad' => 'TD', + 'Chile' => 'CL', + 'China' => 'CN', + 'Colombia' => 'CO', + 'Congo' => 'CG', + 'Costa Rica' => 'CR', + 'Croatia' => 'HR', + 'Cuba' => 'CU', + 'Cyprus' => 'CY', + 'Czech Rep.' => 'CZ', + 'Côte d\'Ivoire' => 'CI', + 'Dem. Rep. Congo' => 'CD', + 'Dem. Rep. Korea' => 'KP', + 'Denmark' => 'DK', + 'Djibouti' => 'DJ', + 'Dominican Rep.' => 'DO', + 'Ecuador' => 'EC', + 'Egypt' => 'EG', + 'El Salvador' => 'SV', + 'Eq. Guinea' => 'GQ', + 'Eritrea' => 'ER', + 'Estonia' => 'EE', + 'Ethiopia' => 'ET', + 'Falkland Is.' => 'FK', + 'Fiji' => 'FJ', + 'Finland' => 'FI', + 'Fr. S. Antarctic Lands' => 'TF', + 'France' => 'FR', + 'Gabon' => 'GA', + 'Gambia' => 'GM', + 'Georgia' => 'GE', + 'Germany' => 'DE', + 'Ghana' => 'GH', + 'Greece' => 'GR', + 'Greenland' => 'GL', + 'Guatemala' => 'GT', + 'Guinea' => 'GN', + 'Guinea-Bissau' => 'GW', + 'Guyana' => 'GY', + 'Haiti' => 'HT', + 'Honduras' => 'HN', + 'Hungary' => 'HU', + 'Iceland' => 'IS', + 'India' => 'IN', + 'Indonesia' => 'ID', + 'Iran' => 'IR', + 'Iraq' => 'IQ', + 'Ireland' => 'IE', + 'Israel' => 'IL', + 'Italy' => 'IT', + 'Jamaica' => 'JM', + 'Japan' => 'JP', + 'Jordan' => 'JO', + 'Kazakhstan' => 'KZ', + 'Kenya' => 'KE', + 'Korea' => 'KR', + 'Kuwait' => 'KW', + 'Kyrgyzstan' => 'KG', + 'Lao PDR' => 'LA', + 'Latvia' => 'LV', + 'Lebanon' => 'LB', + 'Lesotho' => 'LS', + 'Liberia' => 'LR', + 'Libya' => 'LY', + 'Lithuania' => 'LT', + 'Luxembourg' => 'LU', + 'Macedonia' => 'MK', + 'Madagascar' => 'MG', + 'Mainland China' => 'CN', + 'Malawi' => 'MW', + 'Malaysia' => 'MY', + 'Mali' => 'ML', + 'Mauritania' => 'MR', + 'Mexico' => 'MX', + 'Moldova' => 'MD', + 'Mongolia' => 'MN', + 'Montenegro' => 'ME', + 'Morocco' => 'MA', + 'Mozamb' => 'MZ', + 'Myanmar' => 'MM', + 'Namibia' => 'NA', + 'Nepal' => 'NP', + 'Netherlands' => 'NL', + 'New Caledonia' => 'NC', + 'New Zealand' => 'NZ', + 'Nicaragua' => 'NI', + 'Niger' => 'NE', + 'Nigeria' => 'NG', + 'Norway' => 'NO', + 'Oman' => 'OM', + 'Pakistan' => 'PK', + 'Palestine' => 'PS', + 'Panama' => 'PA', + 'Papua New Guinea' => 'PG', + 'Paraguay' => 'PY', + 'Peru' => 'PE', + 'Philippines' => 'PH', + 'Poland' => 'PL', + 'Portugal' => 'PT', + 'Puerto Rico' => 'PR', + 'Qatar' => 'QA', + 'Romania' => 'RO', + 'Russia' => 'RU', + 'Rwanda' => 'RW', + 'S. Sudan' => 'SS', + 'Saudi Arabia' => 'SA', + 'Senegal' => 'SN', + 'Serbia' => 'RS', + 'Sierra Leone' => 'SL', + 'Slovakia' => 'SK', + 'Slovenia' => 'SI', + 'Solomon Is.' => 'SB', + 'Somalia' => 'SO', + 'South Africa' => 'ZA', + 'Spain' => 'ES', + 'Sri Lanka' => 'LK', + 'Sudan' => 'SD', + 'Suriname' => 'SR', + 'Swaziland' => 'SZ', + 'Sweden' => 'SE', + 'Switzerland' => 'CH', + 'Syria' => 'SY', + 'Taiwan' => 'TW', + 'Tajikistan' => 'TJ', + 'Tanzania' => 'TZ', + 'Thailand' => 'TH', + 'Timor-Leste' => 'TL', + 'Togo' => 'TG', + 'Trinidad and Tobago' => 'TT', + 'Tunisia' => 'TN', + 'Turkey' => 'TR', + 'Turkmenistan' => 'TM', + 'Uganda' => 'UG', + 'Ukraine' => 'UA', + 'United Arab Emirates' => 'AE', + 'United Kingdom' => 'GB', + 'United States' => 'US', + 'Uruguay' => 'UY', + 'Uzbekistan' => 'UZ', + 'Vanuatu' => 'VU', + 'Venezuela' => 'VE', + 'Vietnam' => 'VN', + 'W. Sahara' => 'EH', + 'Yemen' => 'YE', + 'Zambia' => 'ZM', + 'Zimbabwe' => 'ZW' + ); + + public function handler($user, $options = array()) + { + $params = [ + 'conditions' => [ + 'Nationality !=' => '' + ] + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $this->Organisation = ClassRegistry::init('Organisation'); + $orgs = $this->Organisation->find('all', [ + 'recursive' => -1, + 'fields' => ['Organisation.nationality', 'COUNT(Organisation.nationality) AS frequency'], + 'conditions' => $params['conditions'], + 'group' => ['Organisation.nationality'] + ]); + $results = ['data' => [], 'scope' => 'Organisations']; + foreach($orgs as $org) { + $country = $org['Organisation']['nationality']; + $count = $org['0']['frequency']; + if (isset($this->countryCodes[$country])) { + $countryCode = $this->countryCodes[$country]; + $results['data'][$countryCode] = $count; + } + } + return $results; + } +} +?> diff --git a/app/Lib/Dashboard/TrendingAttributesWidget.php b/app/Lib/Dashboard/TrendingAttributesWidget.php new file mode 100644 index 000000000..e8faff0a6 --- /dev/null +++ b/app/Lib/Dashboard/TrendingAttributesWidget.php @@ -0,0 +1,136 @@ + 'The time window, going back in seconds, that should be included. (allows for filtering by days - example: 5d. -1 Will fetch all historic data)', + 'exclude' => 'List of values to exclude - for example "8.8.8.8".', + 'threshold' => 'Limits the number of displayed attribute values. Default: 10', + 'type' => 'List of Attribute types to include', + 'category' => 'List of Attribute categories to exclude', + 'to_ids' => 'A list of to_ids settings accepted for the data displayed ([0], [1], [0,1])', + 'org_filter' => 'List of organisation filters to exclude events by, based on organisation meta-data (Organisation.sector, Organisation.type, Organisation.nationality). Pre-pending a value with a "!" negates it.' + ); + private $validOrgFilters = [ + 'sector', + 'type', + 'national', + 'uuid', + 'local' + ]; + public $placeholder = + '{ + "time_window": "7d", + "threshold": 15, + "org_filter": { + "sector": ["Financial"] + } +}'; + public $description = 'Widget showing the trending tags over the past x seconds, along with the possibility to include/exclude tags.'; + public $cacheLifetime = 3; + + private function getOrgList($options) + { + $organisationModel = ClassRegistry::init('Organisation'); + if (!empty($options['org_filter']) && is_array($options['org_filter'])) { + foreach ($this->validOrgFilters as $filterKey) { + if (!empty($options['org_filter'][$filterKey])) { + if ($filterKey === 'local') { + $tempConditionBucket['Organisation.local'] = $options['org_filter']['local']; + } else { + if (!is_array($options['org_filter'][$filterKey])) { + $options['org_filter'][$filterKey] = [$options['org_filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['org_filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + } + if (!empty($tempConditionBucket)) { + $orgConditions[] = $tempConditionBucket; + } + } + } + return $organisationModel->find('column', [ + 'recursive' => -1, + 'conditions' => $orgConditions, + 'fields' => ['Organisation.id'] + ]); + } + } + + public function handler($user, $options = array()) + { + /** @var Event $eventModel */ + $attributeModel = ClassRegistry::init('Attribute'); + $threshold = empty($options['threshold']) ? 10 : $options['threshold']; + $time_window = empty($options['time_window']) ? (7 * 24 * 60 * 60) : (int)$options['time_window']; + if (is_string($time_window) && substr($time_window, -1) === 'd') { + $time_window = ((int)substr($time_window, 0, -1)) * 24 * 60 * 60; + } + $conditions = $time_window === -1 ? [] : ['timestamp >=' => time() - $time_window]; + $conditions['deleted'] = 0; + $conditionsToParse = ['type', 'category', 'to_ids']; + foreach ($conditionsToParse as $parsedCondition) { + if (!empty($options[$parsedCondition])) { + $conditions[$parsedCondition] = $options[$parsedCondition]; + } + } + if (!empty($options['exclude'])) { + $conditions['value1 NOT IN'] = $options['exclude']; + } + if (!empty($options['org_filter'])) { + $conditions['Event.orgc_id IN'] = $this->getOrgList($options); + if (empty($conditions['Event.orgc_id IN'])) { + $conditions['Event.orgc_id IN'] = [-1]; + } + } + $attributeModel->virtualFields['frequency'] = 0; + if (!empty($user['Role']['perm_site_admin'])) { + $values = $attributeModel->find('all', [ + 'recursive' => -1, + 'fields' => ['value1', 'count(Attribute.value1) as Attribute__frequency'], + 'group' => ['value1'], + 'conditions' => $conditions, + 'contain' => ['Event.orgc_id'], + 'order' => 'count(Attribute.value1) desc', + 'limit' => empty($options['threshold']) ? 10 : $options['threshold'] + ]); + } else { + $conditions['AND'][] = [ + 'OR' => [ + 'Event.orgc_id' => $user['org_id'], + + ] + ]; + $values = $attributeModel->find('all', [ + 'recursive' => -1, + 'fields' => ['value1', 'count(Attribute.value1) as Attribute__frequency', 'distribution', 'sharing_group_id'], + 'group' => 'value1', + 'contain' => [ + 'Event.org_id', + 'Event.distribution', + 'Event.sharing_group_id', + 'Object.distribution', + 'Object.sharing_group_id' + ], + 'conditions' => $conditions, + 'order' => 'count(Attribute.value1) desc', + 'limit' => empty($options['threshold']) ? 10 : $options['threshold'] + ]); + } + $data = []; + foreach ($values as $value) { + $data[$value['Attribute']['value1']] = $value['Attribute']['frequency']; + } + return ['data' => $data]; + } +} diff --git a/app/Lib/Dashboard/TrendingTagsWidget.php b/app/Lib/Dashboard/TrendingTagsWidget.php index cf50cf707..b3d6f6048 100644 --- a/app/Lib/Dashboard/TrendingTagsWidget.php +++ b/app/Lib/Dashboard/TrendingTagsWidget.php @@ -7,7 +7,7 @@ class TrendingTagsWidget public $width = 3; public $height = 4; public $params = array( - 'time_window' => 'The time window, going back in seconds, that should be included.', + 'time_window' => 'The time window, going back in seconds, that should be included. (allows for filtering by days - example: 5d. -1 Will fetch all historic data)', 'exclude' => 'List of substrings to exclude tags by - for example "sofacy" would exclude any tag containing sofacy.', 'include' => 'List of substrings to include tags by - for example "sofacy" would include any tag containing sofacy.', 'threshold' => 'Limits the number of displayed tags. Default: 10', @@ -16,23 +16,26 @@ class TrendingTagsWidget ); public $placeholder = '{ - "time_window": "86400", + "time_window": "7d", "threshold": 15, "exclude": ["tlp:", "pap:"], "include": ["misp-galaxy:", "my-internal-taxonomy"], "filter_event_tags": ["misp-galaxy:threat-actor="APT 29"], }'; public $description = 'Widget showing the trending tags over the past x seconds, along with the possibility to include/exclude tags.'; - public $cacheLifetime = 600; + public $cacheLifetime = 3; public function handler($user, $options = array()) { /** @var Event $eventModel */ $eventModel = ClassRegistry::init('Event'); $threshold = empty($options['threshold']) ? 10 : $options['threshold']; - $params = [ - 'timestamp' => time() - (empty($options['time_window']) ? 8640000 : $options['time_window']), - ]; + $time_window = empty($options['time_window']) ? (7 * 24 * 60 * 60) : $options['time_window']; + if (is_string($time_window) && substr($time_window, -1) === 'd') { + $time_window = ((int)substr($time_window, 0, -1)) * 24 * 60 * 60; + } + $params = $time_window === -1 ? [] : ['timestamp' => time() - $time_window]; + if (!empty($options['filter_event_tags'])) { $params['event_tags'] = $options['filter_event_tags']; } @@ -48,6 +51,7 @@ class TrendingTagsWidget $events = $eventModel->fetchEvent($user, [ 'eventid' => $eventIds, 'order' => 'Event.timestamp', + 'metadata' => 1 ]); foreach ($events as $event) { @@ -111,7 +115,6 @@ class TrendingTagsWidget } } - return $data; } diff --git a/app/Lib/Dashboard/UsageDataWidget.php b/app/Lib/Dashboard/UsageDataWidget.php index caebbae05..3bddb2531 100644 --- a/app/Lib/Dashboard/UsageDataWidget.php +++ b/app/Lib/Dashboard/UsageDataWidget.php @@ -5,40 +5,82 @@ class UsageDataWidget public $render = 'SimpleList'; public $width = 2; public $height = 5; - public $params = array(); public $description = 'Shows usage data / statistics.'; public $cacheLifetime = false; - public $autoRefreshDelay = 3; + public $autoRefreshDelay = false; + public $params = [ + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + ]; + private $User = null; + private $Event = null; + private $Correlation = null; + private $Thread = null; + private $AuthKey = null; + + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + + private $validFields = [ + 'Events', + 'Attributes', + 'Attributes / event', + 'Correlations', + 'Active proposals', + 'Users', + 'Users with PGP keys', + 'Organisations', + 'Local organisations', + 'Event creator orgs', + 'Average users / org', + 'Discussion threads', + 'Discussion posts' + ]; public function handler($user, $options = array()){ $this->User = ClassRegistry::init('User'); - - $orgsCount = $this->User->Organisation->find('count'); - $localOrgsParams['conditions']['Organisation.local'] = 1; - $localOrgsCount = $this->User->Organisation->find('count', $localOrgsParams); - - $thisMonth = strtotime('first day of this month'); $this->Event = ClassRegistry::init('Event'); - $eventsCount = $this->Event->find('count', array('recursive' => -1)); - $eventsCountMonth = $this->Event->find('count', array('conditions' => array('Event.timestamp >' => $thisMonth), 'recursive' => -1)); - - $this->Attribute = ClassRegistry::init('Attribute'); - $attributesCount = $this->Attribute->find('count', array('conditions' => array('Attribute.deleted' => 0), 'recursive' => -1)); - $attributesCountMonth = $this->Attribute->find('count', array('conditions' => array('Attribute.timestamp >' => $thisMonth, 'Attribute.deleted' => 0), 'recursive' => -1)); - $attributesPerEvent = round($attributesCount / $eventsCount); - - $this->Correlation = ClassRegistry::init('Correlation'); - $correlationsCount = $this->Correlation->find('count', array('recursive' => -1)) / 2; - - $proposalsCount = $this->Event->ShadowAttribute->find('count', array('recursive' => -1, 'conditions' => array('deleted' => 0))); - - $usersCount = $this->User->find('count', array('recursive' => -1)); - $usersCountPgp = $this->User->find('count', array('recursive' => -1, 'conditions' => array('User.gpgkey !=' => ''))); - $usersCountPgpPercentage = round(100* ($usersCountPgp / $usersCount), 1); - $contributingOrgsCount = $this->Event->find('count', array('recursive' => -1, 'group' => array('Event.orgc_id'))); - $averageUsersPerOrg = round($usersCount / $localOrgsCount, 1); - $this->Thread = ClassRegistry::init('Thread'); + $this->Correlation = ClassRegistry::init('Correlation'); + $thisMonth = strtotime('first day of this month'); + $orgConditions = []; + $orgIdList = null; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $orgConditions[] = $tempConditionBucket; + } + } + } + $orgIdList = $this->User->Organisation->find('column', [ + 'recursive' => -1, + 'conditions' => $orgConditions, + 'fields' => ['Organisation.id'] + ]); + } + $eventsCount = $this->getEventsCount($orgConditions, $orgIdList, $thisMonth); + $attributesCount = $this->getAttributesCount($orgConditions, $orgIdList, $thisMonth); + $usersCount = $this->getUsersCount($orgConditions, $orgIdList, $thisMonth); + $usersCountPgp = $this->getUsersCountPgp($orgConditions, $orgIdList, $thisMonth); + $localOrgsCount = $this->getLocalOrgsCount($orgConditions, $orgIdList, $thisMonth); + + $threadCount = $this->Thread->find('count', array('conditions' => array('Thread.post_count >' => 0), 'recursive' => -1)); $threadCountMonth = $this->Thread->find('count', array('conditions' => array('Thread.date_created >' => date("Y-m-d H:i:s", $thisMonth), 'Thread.post_count >' => 0), 'recursive' => -1)); @@ -47,21 +89,69 @@ class UsageDataWidget //Monhtly data is not added to the widget at the moment, could optionally add these later and give user choice? - $statistics = array( - array('title' => 'Events', 'value' => $eventsCount), - array('title' => 'Attributes', 'value' => $attributesCount), - array('title' => 'Attributes / event', 'value' => $attributesPerEvent), - array('title' => 'Correlations', 'value' => $correlationsCount), - array('title' => 'Active proposals', 'value' => $proposalsCount), - array('title' => 'Users', 'value' => $usersCount), - array('title' => 'Users with PGP keys', 'value' => $usersCountPgp . ' (' . $usersCountPgpPercentage . '%)'), - array('title' => 'Organisations', 'value' => $orgsCount), - array('title' => 'Local organisations', 'value' => $localOrgsCount), - array('title' => 'Event creator orgs', 'value' => $contributingOrgsCount), - array('title' => 'Average users / org', 'value' => $averageUsersPerOrg), - array('title' => 'Discussions threads', 'value' => $threadCount), - array('title' => 'Discussion posts', 'value' => $postCount) - ); + $statistics = [ + 'Events' => [ + 'title' => 'Events', + 'value' => $eventsCount, + 'change' => $this->getEventsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Attributes' => [ + 'title' => 'Attributes', + 'value' => $attributesCount, + 'change' => $this->getAttributesCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Attributes / event' => [ + 'title' => 'Attributes / event', + 'value' => $eventsCount ? round($attributesCount / $eventsCount) : 0 + ], + 'Correlations' => [ + 'title' => 'Correlations', + 'value' => $this->getCorrelationsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Active proposals' => [ + 'title' => 'Active proposals', + 'value' => $this->getProposalsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Users' => [ + 'title' => 'Users', + 'value' => $usersCount, + 'change' => $this->getUsersCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Users with PGP keys' => [ + 'title' => 'Users with PGP keys', + 'value' => sprintf( + '%s (%s %%)', + $usersCountPgp, + $usersCount ? round(100* ($usersCountPgp / $usersCount), 1) : 0 + ) + ], + 'Organisations' => [ + 'title' => 'Organisations', + 'value' => $this->getOrgsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Local organisations' => [ + 'title' => 'Local organisations', + 'value' => $localOrgsCount, + 'change' => $this->getLocalOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Event creator orgs' => [ + 'title' => 'Event creator orgs', 'value' => $this->getContributingOrgsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Average users / org' => [ + 'title' => 'Average users / org', 'value' => round($usersCount / $localOrgsCount, 1) + ], + 'Discussion threads' => [ + 'title' => 'Discussions threads', + 'value' => $this->getThreadsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getThreadsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Discussion posts' => [ + 'title' => 'Discussion posts', + 'value' => $this->getPostsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getPostsCountMonth($orgConditions, $orgIdList, $thisMonth) + ] + ]; if(!empty(Configure::read('Security.advanced_authkeys'))){ $this->AuthKey = ClassRegistry::init('AuthKey'); $authkeysCount = $this->AuthKey->find('count', array('recursive' => -1)); @@ -70,6 +160,233 @@ class UsageDataWidget return $statistics; } + private function getEventsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getCorrelationsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['AND']['OR'][] = ['Correlation.org_id IN' => $orgIdList]; + $conditions['AND']['OR'][] = ['Correlation.1_org_id IN' => $orgIdList]; + } + return $this->Correlation->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getEventsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Event.timestamp >' => $thisMonth]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getAttributesCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Attribute.deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->Attribute->find('count', [ + 'conditions' => $conditions, + 'contain' => ['Event'], + 'recursive' => -1 + ]); + } + + private function getAttributesCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Attribute.timestamp >' => $thisMonth, 'Attribute.deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->Attribute->find('count', [ + 'conditions' => $conditions, + 'contain' => 'Event.orgc_id', + 'recursive' => -1 + ]); + } + + private function getOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'AND' => $orgConditions + ] + ]); + } + + private function getOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $datetime = new DateTime(); + $datetime->setTimestamp($thisMonth); + $thisMonth = $datetime->format('Y-m-d H:i:s'); + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'AND' => $orgConditions, + 'Organisation.date_created >' => $thisMonth + ] + ]); + } + + private function getLocalOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'Organisation.local' => 1, + 'AND' => $orgConditions + ] + ]); + } + + private function getLocalOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $datetime = new DateTime(); + $datetime->setTimestamp($thisMonth); + $thisMonth = $datetime->format('Y-m-d H:i:s'); + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'Organisation.local' => 1, + 'AND' => $orgConditions, + 'Organisation.date_created >' => $thisMonth + ] + ]); + } + + private function getProposalsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['ShadowAttribute.org_id IN'] = $orgIdList; + } + return $this->Event->ShadowAttribute->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['User.date_created >' => $thisMonth]; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCountPgp($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['User.gpgkey !=' => '']; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getContributingOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if ($orgConditions) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'recursive' => -1, + 'group' => ['Event.orgc_id'], + 'conditions' => $conditions + ]); + } + + private function getThreadsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Thread.post_count >' => 0]; + if ($orgConditions) { + $conditions['AND'][] = ['Thread.org_id IN' => $orgIdList]; + } + return $this->Thread->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getThreadsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = [ + 'Thread.post_count >' => 0, + 'Thread.date_created >=' => $thisMonth + ]; + if ($orgConditions) { + $conditions['AND'][] = ['Thread.org_id IN' => $orgIdList]; + } + return $this->Thread->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getPostsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if ($orgConditions) { + $conditions['AND'][] = ['User.org_id IN' => $orgIdList]; + } + return $this->Thread->Post->find('count', [ + 'conditions' => $conditions, + 'contain' => ['User.org_id'], + 'recursive' => -1 + ]); + } + + private function getPostsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = [ + 'Post.date_created >=' => $thisMonth + ]; + if ($orgConditions) { + $conditions['AND'][] = ['User.org_id IN' => $orgIdList]; + } + return $this->Thread->Post->find('count', [ + 'conditions' => $conditions, + 'contain' => ['User.org_id'], + 'recursive' => -1 + ]); + } + + +/* There is nothing sensitive in here. public function checkPermissions($user) { if (empty($user['Role']['perm_site_admin'])) { @@ -77,4 +394,5 @@ class UsageDataWidget } return true; } +*/ } diff --git a/app/Lib/Dashboard/UserContributionToplistWidget.php b/app/Lib/Dashboard/UserContributionToplistWidget.php new file mode 100644 index 000000000..b9ea08c50 --- /dev/null +++ b/app/Lib/Dashboard/UserContributionToplistWidget.php @@ -0,0 +1,123 @@ + 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + public $placeholder = +'{ + "days": "7d", + "threshold": 15, + "filter": { + "sector": "Financial" + } +}'; + private $Org = null; + private $Event = null; + + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + } else { + return null; + } + return $condition; + } + + + public function handler($user, $options = array()) + { + $params = ['conditions' => []]; + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['Event.timestamp >=' => $timeConditions]; + } + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + if (isset($options['filter']['local'])) { + $params['conditions']['AND']['local'] = $options['filter']['local']; + } + + $this->Org = ClassRegistry::init('Organisation'); + $org_ids = $this->Org->find('list', [ + 'fields' => ['Organisation.id', 'Organisation.name'], + 'conditions' => $params['conditions'] + ]); + $userConditions = []; + if (!empty($org_ids)) { + $userConditions = ['User.org_id IN' => array_keys($org_ids)]; + } + $user_ids = $this->Org->User->find('list', [ + 'fields' => ['User.id', 'User.email'], + 'conditions' => $userConditions + ]); + $conditions = empty($user_ids) ? [] : ['Event.user_id IN' => array_keys($user_ids)]; + $this->Event = ClassRegistry::init('Event'); + $this->Event->virtualFields['frequency'] = 0; + $users = $this->Event->find('all', [ + 'recursive' => -1, + 'fields' => ['user_id', 'count(Event.user_id) as Event__frequency'], + 'group' => ['user_id'], + 'conditions' => $conditions, + 'order' => 'count(Event.user_id) desc', + 'limit' => empty($options['limit']) ? 10 : $options['limit'] + ]); + $results = []; + foreach($users as $user) { + $results[$user_ids[$user['Event']['user_id']]] = $user['Event']['frequency']; + } + return ['data' => $results]; + } + + public function checkPermissions($user) + { + if (empty(Configure::read('Security.disclose_user_emails')) && empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} +?> diff --git a/app/Model/Server.php b/app/Model/Server.php index cc850d4ca..b0d69eea5 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -6584,7 +6584,15 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true, 'cli_only' => true - ] + ], + 'disclose_user_emails' => array( + 'level' => 0, + 'description' => __('Enable this setting to allow for the user e-mail addresses to be shown to non site-admin users. Keep in mind that in broad communities this can be abused.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ), ), 'SecureAuth' => array( 'branch' => 1, diff --git a/app/View/Dashboards/list_templates.ctp b/app/View/Dashboards/list_templates.ctp index edd37d9cf..0f9aaf5a0 100644 --- a/app/View/Dashboards/list_templates.ctp +++ b/app/View/Dashboards/list_templates.ctp @@ -46,7 +46,7 @@ array( 'name' => __('Widgets Used'), 'data_path' => 'Dashboard.widgets', - 'element' => 'list' + 'element' => 'allow_deny_list' ), array( 'name' => __('Selectable'), diff --git a/app/View/Elements/dashboard/Widgets/BarChart.ctp b/app/View/Elements/dashboard/Widgets/BarChart.ctp index 80e73351e..a2748fde3 100644 --- a/app/View/Elements/dashboard/Widgets/BarChart.ctp +++ b/app/View/Elements/dashboard/Widgets/BarChart.ctp @@ -15,10 +15,15 @@ if (!empty($data['logarithmic'])) { $value = $data['logarithmic'][$entry]; } + $shortlabel = $entry; + if (mb_strlen($shortlabel) > 30) { + $shortlabel = mb_substr($shortlabel, 0, 30) . '...'; + } echo sprintf( - '%s%s', - 'text-align:right;width:33%;white-space:nowrap;', + '%s%s', + 'text-align:right;width:35em;white-space:nowrap;', h($entry), + h($shortlabel), 'width:100%', sprintf( '
%s%s
', diff --git a/app/View/Elements/dashboard/Widgets/Index.ctp b/app/View/Elements/dashboard/Widgets/Index.ctp index d7a432423..dedb30890 100644 --- a/app/View/Elements/dashboard/Widgets/Index.ctp +++ b/app/View/Elements/dashboard/Widgets/Index.ctp @@ -2,10 +2,10 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data['data'], + 'description' => empty($data['description']) ? false : $data['description'], 'top_bar' => [], 'fields' => $data['fields'], 'title' => false, - 'description' => false, 'pull' => 'right', 'skip_pagination' => true, 'actions' => [] diff --git a/app/View/Elements/dashboard/Widgets/SimpleList.ctp b/app/View/Elements/dashboard/Widgets/SimpleList.ctp index 7a44710f3..255b7a9eb 100644 --- a/app/View/Elements/dashboard/Widgets/SimpleList.ctp +++ b/app/View/Elements/dashboard/Widgets/SimpleList.ctp @@ -18,12 +18,27 @@ $element['value'] = h($element['value']); } } + $change = ''; + if (!empty($element['change'])) { + $change = (int)$element['change']; + if ($change > 0) { + $change = ' (+' . $change . ')'; + } else { + $change = ' (-' . $change . ')'; + } + } + if (!empty($element['html_title'])) { + $title = $element['html_title']; + } else { + $title = h($element['title']); + } echo sprintf( - '
%s: %s%s
', - h($element['title']), + '
%s: %s%s%s
', + $title, empty($element['class']) ? 'blue' : h($element['class']), !isset($element['value']) ? '' : $element['value'], - empty($element['html']) ? '' : $element['html'] + empty($element['html']) ? '' : $element['html'], + $change ); } } diff --git a/app/View/Elements/dashboard/Widgets/WorldMap.ctp b/app/View/Elements/dashboard/Widgets/WorldMap.ctp index 2123a4b34..a89d4d978 100644 --- a/app/View/Elements/dashboard/Widgets/WorldMap.ctp +++ b/app/View/Elements/dashboard/Widgets/WorldMap.ctp @@ -20,7 +20,6 @@ ), true); } ?> -