From 570dc7e91ebcd0f5a901720dab90a2c97745173b Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sun, 23 Apr 2023 17:47:43 +0200 Subject: [PATCH 01/22] chg: [misp-galaxy] updated to the latest version --- app/files/misp-galaxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/files/misp-galaxy b/app/files/misp-galaxy index ccc8f0f80..3c6c204f0 160000 --- a/app/files/misp-galaxy +++ b/app/files/misp-galaxy @@ -1 +1 @@ -Subproject commit ccc8f0f8018fece44f3afd18375894b16948da88 +Subproject commit 3c6c204f01682a27b3b573f7213bb3d3d9f284be From 7327539a682792af285a70c556af3c1e7ead3c77 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 24 Apr 2023 09:02:37 +0200 Subject: [PATCH 02/22] chg: [misp-galaxy] updated --- app/files/misp-galaxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/files/misp-galaxy b/app/files/misp-galaxy index 3c6c204f0..79b80b086 160000 --- a/app/files/misp-galaxy +++ b/app/files/misp-galaxy @@ -1 +1 @@ -Subproject commit 3c6c204f01682a27b3b573f7213bb3d3d9f284be +Subproject commit 79b80b0869e57dc14861c0c30792f64a243ecb3b From c3f7077abe111154017b52fac504f293ea41a471 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Mon, 24 Apr 2023 14:07:19 +0200 Subject: [PATCH 03/22] removed cogsec, domain not renewed --- app/files/community-metadata/defaults.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/files/community-metadata/defaults.json b/app/files/community-metadata/defaults.json index 67ab31d65..6e800b672 100644 --- a/app/files/community-metadata/defaults.json +++ b/app/files/community-metadata/defaults.json @@ -78,21 +78,6 @@ "misp_project_vetted": true, "scope_of_data_to_be_shared": "Cybersecurity Threat Intelligence including indicators, threat intelligence information, reports, contextual threat actor information or financial fraud information." }, - { - "name": "Cognitive Security Collaborative", - "uuid": "1ea46a83-cd51-40f5-a375-104e0acd6729", - "org_uuid": "5e2dd31a-3bcc-45e8-ba7e-2ab890d945c8", - "org_name": "Cogsec Collab", - "description": "The Cognitive Security Collaborative operates as a sharing community dedicated to information operations.", - "url": "https://www.cogsec-collab.org", - "sector": "undefined", - "nationality": "International", - "type": "Vetted Information Sharing Community", - "email": "misp@cogsec-collab.org", - "pgp_key": "\r\n\r\n-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nmQGNBF55bdcBDAC6+Fcey+0GcUw4iP4j15+/FylnvGa4wl8MRkYR5XryJn+n/O4s\r\nZbNCKpxwUA7lb2prn37lWMX7LswjvoxfmCTKi78UY1YH7Fqg3JG2PsV9Lw7uYnzC\r\nAImyAflzDpewo+eCF1aknvcbcbGkYFwdQ/37UfG/BkwCDQQGrBZ5EtL6CYXXNX/P\r\nX+4vYv23AVuchHvxeyW2dPLL3A6t3Mx8pZQBdN1cGZ1QAtE9IN0Yn2y+rMsNpDG4\r\ncOQ6bRqmue2I8JEB4AsQcufcqx69imBvBERsIZEyGZekLjmiuqDKI9Gti2VKZe/t\r\nxdl++gjplq6OAkdzXDGsMNtwxSk21IBrugAXK6K+4RPiMrPpBh81VGzBe2PRKUwT\r\nAZi06KZdaZudehvzIMLsNP5Aeep4+GXxoZ7Yrka/08SIv7SN5XY4o6xkli658Z+l\r\n8WAj2JiI684D/TK5MlvcBDQk1yKdDI2iC4eTFLkJ2PiDToUDT+vACrcnevstU+c8\r\nrNPFbvbB1DUIIo8AEQEAAbQ5Q29nbml0aXZlIFNlY3VyaXR5IENvbGxhYm9yYXRp\r\ndmUgPG1pc3BAY29nc2VjLWNvbGxhYi5vcmc+iQHUBBMBCAA+FiEEm65FjZ6Jbfp9\r\nCN50hA2Itf18R2cFAl55bdcCGwMFCQlmAYAFCwkIBwIGFQoJCAsCBBYCAwECHgEC\r\nF4AACgkQhA2Itf18R2e/ewv7BuCpmNIR0YOJld8RqrS4g5MV6eKJUuTRYUOxDyw9\r\nvgdpdvM1FgHPZ7pJcsijKQ+S+dL7ADmEbsCLWe1UhcwbnVRxJ0T+1yxRf6ONQA0/\r\ntRLmrcF4j6JCkl01irWRnYxMI1w1ABOQj4/J7BcTCzbYUdnxSuWhcZBqcsYIHf8J\r\nHnfbVd7OIML/80IRZbRXn1ST6OeXK9RpzqO7bnfPGnd506dt8sfHCWRidUSv2max\r\nrsi9xSyXeSKSNPQFVBgYnMVwBVUGIaWTnt7Ly4I8Bs5P9NWUpLYrRgYLMbDzLWaD\r\nxX7qNQjAKkNCx9k7qQN0Ck9YqeUIuJQPq2doGuLKnqjJBXizsXbAFqcKitQz7WV2\r\nPUsN/QUguVyZbhy7oJELlWDiDWxS6EwpU+q0SODHjCFKoUXvWFkk9bz1K4/kLDFO\r\nOdTABp7i65nJst5b3pVXimoTKqW7JRyCUWz3aaaqjWSTPKP2GmQbxOwM86rgmnGX\r\nqq8Ces6LQw6zGw08ubDDotEKuQGNBF55bdcBDACbmsVMV7azLYys6iMXTLVERasT\r\nUnw8FpKADA2uDgQme5o3CjeFtBBkgBNe8zdOEEslggETVmntp4n6woQzOknDHNx/\r\nVMliUaGuIYgmC8hTDTF269fdRTpKMrcwu2aBEUpHpG7Xvz91HIr213FTwU0LLq0g\r\n+DefSlwdcMPJiCUqshLw8q/D3qVg/VYVen5li55RQBBFLgYYNgag3WnSejE41uqz\r\nvt40FZ4C88Pj0I3f+PRtfHHeXTZehUjs3+W4jn1fLWNmbIScmIhwp/Vqh8R7JHf2\r\n69UGgWr4cOaLGh6C2Io+TVJ+Sq7TMt47qB6eO53Vr2nyizXTxjrmAWqjw3OLc8QX\r\nWsjbpTMqUaPisnCpog/3SqnE4Fe2rQYkroQao6dRL3FrmgvnyhLgjUtjk6fAfx1+\r\nH6fQFH/JJGCNefG9AWo41Er3oHGoV0yqlI697uk0QGdx/848hc0gXLrus82bw+BI\r\nx36ycevxkpmfvzC8lew/vLEB7t/jqXH2H9Qqtm0AEQEAAYkBvAQYAQgAJhYhBJuu\r\nRY2eiW36fQjedIQNiLX9fEdnBQJeeW3XAhsMBQkJZgGAAAoJEIQNiLX9fEdnmYsM\r\nAJzX6MCYoGPED1VXMoPXVS9s7V7hv+0Q4SKcoUxqROwA0wb3NwvdnzO/WAQlzIIj\r\ny1Sk9VX8qZkATN7+nti8jfhKnlMVqAXFFg9fMsq68WlTzHiyGm06DnM2DXBvdLRT\r\nwbcm5H4Ly1/bCFww6Spbxo3zScrSCeRrIHHGOHEzr/vhcZavRDpFmdpTCD6ID7oG\r\nw5jR6GdSCpvBT6Lq7M2xe6cVw/A9z5tE3cIf75uikKfch8HFVV2l1B9XLJVpvhqv\r\nYf+kUa7l7VP893yyTyf9G6SSaS77VKlHxn+OQ9AX+wdgSpD5SgVkvRFXejXw8oIZ\r\nBeTNYTvYYgV75ApnvT+hyeirGDCRRiTiuva0ijd71PzTRk+5Ad80rav1Jy864dUt\r\nDcSklY5T+wjJf7kb/3nIE5vqO/3YkJxdDTvZM23T+IZsCvamQ5pyyp+bP3HTAZkr\r\no6oiGFXbv5OF6/wkUG6vQ5w1RCUQVLfrM6Dh675dx/sdI+p0JMt6BlvlRUJSofu0\r\nWw==\r\n=4aXp\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n", - "misp_project_vetted": true, - "scope_of_data_to_be_shared": "Information Operation Threat Intelligence including disinformation, indicators, threat intelligence information, reports, contextual threat actor information or financial fraud information." - }, { "name": "COVID-19 MISP community", "uuid": "5e59659e-8e24-4e5d-b3fa-2ba744b7dd05", From c442c27dd5e6a72c7eb020ec755d6c29fcb572ac Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:21:15 +0200 Subject: [PATCH 04/22] new: [setting] added a new setting to remove email addresses from widgets that would otherwise display it - anonymise the widgets on demand --- app/Model/Server.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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, From 984be50b759b16e7d2cd77595d8778a6e35103d6 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:22:26 +0200 Subject: [PATCH 05/22] fix: [trending tags widget] reworked - added day based time_window option - much more perforant / memory friendly --- app/Lib/Dashboard/TrendingTagsWidget.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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; } From e5cd4155793ca5eec8eedcdc59fafc65aadea792 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:23:32 +0200 Subject: [PATCH 06/22] new: [usagedata widget] upgraded - allows for filtering based on organisation metadata - shows changes in current month - fixed several invalid statistics - moved all individual statistics to separate functions for readability - removed permission restriction - the data is only showing aggregates --- app/Lib/Dashboard/UsageDataWidget.php | 400 +++++++++++++++++++++++--- 1 file changed, 359 insertions(+), 41 deletions(-) diff --git a/app/Lib/Dashboard/UsageDataWidget.php b/app/Lib/Dashboard/UsageDataWidget.php index caebbae05..37781039b 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 $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; } +*/ } From 4578232ed147bcf2c6780d6bec9729e70e6bd364 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:26:29 +0200 Subject: [PATCH 07/22] chg: [widget UI] various improvements --- app/View/Elements/dashboard/Widgets/BarChart.ctp | 9 +++++++-- app/View/Elements/dashboard/Widgets/Index.ctp | 2 +- app/View/Elements/dashboard/Widgets/SimpleList.ctp | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) 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..525f8ebdc 100644 --- a/app/View/Elements/dashboard/Widgets/SimpleList.ctp +++ b/app/View/Elements/dashboard/Widgets/SimpleList.ctp @@ -18,12 +18,22 @@ $element['value'] = h($element['value']); } } + $change = ''; + if (!empty($element['change'])) { + $change = (int)$element['change']; + if ($change > 0) { + $change = ' (+' . $change . ')'; + } else { + $change = ' (-' . $change . ')'; + } + } echo sprintf( - '
%s: %s%s
', + '
%s: %s%s%s
', h($element['title']), empty($element['class']) ? 'blue' : h($element['class']), !isset($element['value']) ? '' : $element['value'], - empty($element['html']) ? '' : $element['html'] + empty($element['html']) ? '' : $element['html'], + $change ); } } From 96652cc7819fc011bc2cd08c19989d96fdb460ad Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:27:19 +0200 Subject: [PATCH 08/22] new: [widgets] Widget to list latest joined orgs - filter by org metadata / timeframe --- app/Lib/Dashboard/NewOrgsWidget.php | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 app/Lib/Dashboard/NewOrgsWidget.php 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 + ]; + } +} From 2048b546de5e629d24c07e48ae623480a781d83a Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:28:01 +0200 Subject: [PATCH 09/22] new: [widget] Widget to show latest users - filter by org metadata, etc --- app/Lib/Dashboard/NewUsersWidget.php | 171 +++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 app/Lib/Dashboard/NewUsersWidget.php 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 + ]; + } +} From 9224fc46f2aa79eb408ea0b741b67ff9234342d6 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:28:56 +0200 Subject: [PATCH 10/22] new: [widget] added a widget to monitor contribution counts per org - filterable --- .../OrgContributionToplistWidget.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 app/Lib/Dashboard/OrgContributionToplistWidget.php 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]; + } +} +?> From f6fabd2db05adbc0e91f669412550cf340863f93 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:29:33 +0200 Subject: [PATCH 11/22] new: [widget] User contribution widget - filterable --- .../UserContributionToplistWidget.php | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 app/Lib/Dashboard/UserContributionToplistWidget.php diff --git a/app/Lib/Dashboard/UserContributionToplistWidget.php b/app/Lib/Dashboard/UserContributionToplistWidget.php new file mode 100644 index 000000000..586d0ee2b --- /dev/null +++ b/app/Lib/Dashboard/UserContributionToplistWidget.php @@ -0,0 +1,115 @@ + '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]; + } +} +?> From cc9ab78fe4656ea26156974a612862118a47b25d Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 5 May 2023 14:29:59 +0200 Subject: [PATCH 12/22] new: [widget] monitor the trending attribute values - filter by timeframe among other filters --- .../Dashboard/TrendingAttributesWidget.php | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 app/Lib/Dashboard/TrendingAttributesWidget.php 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]; + } +} From 9e763ba0e519fd9a7e0de66275aa43a2686caba2 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 13:39:31 +0200 Subject: [PATCH 13/22] new: [auth] log api key usage in redis - lightweight per day slice of api key use - built as a ranked set in redis for the dashboards --- app/Controller/AppController.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 064a06862..0c6e50e35 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -417,16 +417,19 @@ 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')) { $this->loadModel('Log'); $this->Log->create(); $log = array( - 'org' => $user['Organisation']['name'], + 'org' => $user['Organisation']['0000000000000000000000000000000000000000name'], 'model' => 'User', 'model_id' => $user['id'], 'email' => $user['email'], @@ -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 From a60202d9d1cb6b99920a0d5538db1b22dad3add0 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 13:41:44 +0200 Subject: [PATCH 14/22] fix: [junk removed] removed accidentally inserted characters - fell asleep on the keyboard? --- app/Controller/AppController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 0c6e50e35..00ff8bf28 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -429,7 +429,7 @@ class AppController extends Controller $this->loadModel('Log'); $this->Log->create(); $log = array( - 'org' => $user['Organisation']['0000000000000000000000000000000000000000name'], + 'org' => $user['Organisation']['name'], 'model' => 'User', 'model_id' => $user['id'], 'email' => $user['email'], From 712321eb81f05b065bd10f72e97af0ee1c8cd7c8 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:04:32 +0200 Subject: [PATCH 15/22] new: [dashboard templates] show which modules will be visible to the given user --- app/Controller/DashboardsController.php | 11 +++++++ .../IndexTable/Fields/allow_deny_list.ctp | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 app/View/Elements/genericElements/IndexTable/Fields/allow_deny_list.ctp 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/View/Elements/genericElements/IndexTable/Fields/allow_deny_list.ctp b/app/View/Elements/genericElements/IndexTable/Fields/allow_deny_list.ctp new file mode 100644 index 000000000..78b0cbd03 --- /dev/null +++ b/app/View/Elements/genericElements/IndexTable/Fields/allow_deny_list.ctp @@ -0,0 +1,31 @@ + [ + 'name' => __('Allowed'), + 'color' => 'green' + ], + 'deny' => [ + 'name' => __('Denied'), + 'color' => 'red' + ] + ]; + foreach ($setup as $state => $settings) { + if (!empty($data[$state])) { + echo sprintf( + '
%s
', + $settings['color'], + $settings['name'] + ); + foreach ($data[$state] as $k => $element) { + $data[$state][$k] = sprintf( + '%s', + $settings['color'], + h($element) + ); + } + echo implode('
', $data[$state]); + } + } + +?> From aff872aeeea58f358dde9ec54a5f2b601544b63b Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:05:11 +0200 Subject: [PATCH 16/22] chg: [usage widget] removed autorefresh --- app/Lib/Dashboard/UsageDataWidget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Lib/Dashboard/UsageDataWidget.php b/app/Lib/Dashboard/UsageDataWidget.php index 37781039b..3bddb2531 100644 --- a/app/Lib/Dashboard/UsageDataWidget.php +++ b/app/Lib/Dashboard/UsageDataWidget.php @@ -7,7 +7,7 @@ class UsageDataWidget public $height = 5; 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)', ]; From b7bc526e1630f5bf1360927c166ac10d7015ca1e Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:05:34 +0200 Subject: [PATCH 17/22] chg: [usercontribution widget] added permission check for Security.disclose_user_emails --- app/Lib/Dashboard/UserContributionToplistWidget.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Lib/Dashboard/UserContributionToplistWidget.php b/app/Lib/Dashboard/UserContributionToplistWidget.php index 586d0ee2b..b9ea08c50 100644 --- a/app/Lib/Dashboard/UserContributionToplistWidget.php +++ b/app/Lib/Dashboard/UserContributionToplistWidget.php @@ -111,5 +111,13 @@ class UserContributionToplistWidget } 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; + } } ?> From 6e39da801eddf145db84be9b531056ee5fccdcd4 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:06:35 +0200 Subject: [PATCH 18/22] new: [organisation usage widget (map)] added world map listing the countries / counts for each country of users --- app/Lib/Dashboard/OrganisationMapWidget.php | 250 ++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 app/Lib/Dashboard/OrganisationMapWidget.php 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; + } +} +?> From e205d79dacdc184fae839a2398ec99dbefa860bd Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:09:27 +0200 Subject: [PATCH 19/22] new: [widget] login widget added for admins - who logged into the instance via the UI in the past x days / current month / current year, and how frequently? --- app/Lib/Dashboard/LoginsWidget.php | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/Lib/Dashboard/LoginsWidget.php 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; + } +} From c1ad695a9fb80e56aeb9d315349830af5074ddea Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:10:21 +0200 Subject: [PATCH 20/22] chg: [list dashboard templates] view updated with the relevant changes to show allowed/denied widgets in a given template --- app/View/Dashboards/list_templates.ctp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'), From c702f5366d9d8b70f8681a469e9e96f89b0668e0 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 16 May 2023 14:11:02 +0200 Subject: [PATCH 21/22] chg: [dashboard widget UI] made some changes to accomodate the new widgets --- app/View/Elements/dashboard/Widgets/SimpleList.ctp | 7 ++++++- app/View/Elements/dashboard/Widgets/WorldMap.ctp | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/View/Elements/dashboard/Widgets/SimpleList.ctp b/app/View/Elements/dashboard/Widgets/SimpleList.ctp index 525f8ebdc..255b7a9eb 100644 --- a/app/View/Elements/dashboard/Widgets/SimpleList.ctp +++ b/app/View/Elements/dashboard/Widgets/SimpleList.ctp @@ -27,9 +27,14 @@ $change = ' (-' . $change . ')'; } } + if (!empty($element['html_title'])) { + $title = $element['html_title']; + } else { + $title = h($element['title']); + } echo sprintf( '
%s: %s%s%s
', - h($element['title']), + $title, empty($element['class']) ? 'blue' : h($element['class']), !isset($element['value']) ? '' : $element['value'], empty($element['html']) ? '' : $element['html'], 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); } ?> -