CVE-2019-16202 (published 2019-09-10T14:15Z)

MISP before 2.4.115 allows privilege escalation in certain situations. After updating to 2.4.115, escalation attempts are blocked by the __checkLoggedActions function with a "This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)" message.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-269
cwe_name: Improper Privilege Management
description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/MISP/MISP
commit_details: repo_url: https://github.com/MISP/MISP
author: iglocska
author_date: 2019-09-09 13:00:21+02:00
msg: fix: [security] Fix to a vulnerability related to the server index

- along with various support tools
- more information coming soon
repository: repo_url: https://github.com/MISP/MISP
repo_name: MISP/MISP
description: MISP (core software) - Open Source Threat Intelligence and Sharing Platform
repo_language: PHP
owner: MISP
file_changes: filename: AdminShell.php
change_type: ModificationType.MODIFY
diff: @@ -506,4 +506,30 @@ class AdminShell extends AppShell
         $this->Server->cleanCacheFiles();
         echo '...caches lost in time, like tears in rain.' . PHP_EOL;
     }
+
+    public function resetSyncAuthkeys()
+    {
+        if (empty($this->args[0])) {
+            echo sprintf(
+                __("MISP mass sync authkey reset command line tool.\n\nUsage: %sConsole/cake resetSyncAuthkeys [user_id]") . "\n\n",
+                APP
+            );
+            die();
+        } else {
+            $userId = $this->args[0];
+            $user = $this->User->getAuthUser($userId);
+            if (empty($user)) {
+                echo __('Invalid user.') . "\n\n";
+            }
+            if (!$user['Role']['perm_site_admin']) {
+                echo __('User has to be a site admin.') . "\n\n";
+            }
+            if (!empty($this->args[1])) {
+                $jobId = $this->args[1];
+            } else {
+                $jobId = false;
+            }
+            $this->User->resetAllSyncAuthKeys($user, $jobId);
+        }
+    }
 }

programming_language: PHP
filename: ACLComponent.php
change_type: ModificationType.MODIFY
diff: @@ -335,7 +335,7 @@ class ACLComponent extends Component
             ),
             'servers' => array(
                     'add' => array(),
-                    'cache' => array('perm_site_admin'),
+                    'cache' => array(),
                     'checkout' => array(),
                     'createSync' => array('perm_sync'),
                     'delete' => array(),
@@ -348,14 +348,14 @@ class ACLComponent extends Component
                     'getInstanceUUID' => array('perm_sync'),
                     'getPyMISPVersion' => array('*'),
                     'getSetting' => array(),
-                    'getSubmodulesStatus' => array('perm_site_admin'),
-                    'getSubmoduleQuickUpdateForm' => array('perm_site_admin'),
+                    'getSubmodulesStatus' => array(),
+                    'getSubmoduleQuickUpdateForm' => array(),
                     'getWorkers' => array(),
                     'getVersion' => array('*'),
-                    'import' => ('perm_site_admin'),
-                    'index' => array('OR' => array('perm_sync', 'perm_admin')),
+                    'import' => array(),
+                    'index' => array(),
                     'ondemandAction' => array(),
-                    'postTest' => array('perm_sync'),
+                    'postTest' => array(),
                     'previewEvent' => array(),
                     'previewIndex' => array(),
                     'pull' => array(),
@@ -371,7 +371,7 @@ class ACLComponent extends Component
                     'statusZeroMQServer' => array(),
                     'stopWorker' => array(),
                     'stopZeroMQServer' => array(),
-                    'testConnection' => array('perm_sync'),
+                    'testConnection' => array(),
                     'update' => array(),
                     'updateJSON' => array(),
                     'updateProgress' => array(),
@@ -518,6 +518,7 @@ class ACLComponent extends Component
                     'initiatePasswordReset' => array('perm_admin'),
                     'login' => array('*'),
                     'logout' => array('*'),
+                    'resetAllSyncAuthKeys' => array(),
                     'resetauthkey' => array('*'),
                     'request_API' => array('*'),
                     'routeafterlogin' => array('*'),
@@ -553,6 +554,78 @@ class ACLComponent extends Component
             )
     );
 
+    private function __checkLoggedActions($user, $controller, $action)
+    {
+        $loggedActions = array(
+            'servers' => array(
+                'index' => array(
+                    'role' => array(
+                        'NOT' => array(
+                            'perm_site_admin'
+                        )
+                    ),
+                    'message' => __('This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)')
+                )
+            )
+        );
+        foreach ($loggedActions as $k => $v) {
+            $loggedActions[$k] = array_change_key_case($v);
+        }
+        $message = '';
+        if (!empty($loggedActions[$controller])) {
+            if (!empty($loggedActions[$controller][$action])) {
+                $message = $loggedActions[$controller][$action]['message'];
+                $hit = false;
+                if (empty($loggedActions[$controller][$action]['role'])) {
+                    $hit = true;
+                } else {
+                    $role_req = $loggedActions[$controller][$action]['role'];
+                    if (empty($role_req['OR']) && empty($role_req['AND']) && empty($role_req['NOT'])) {
+                        $role_req = array('OR' => $role_req);
+                    }
+                    if (!empty($role_req['NOT'])) {
+                        foreach ($role_req['NOT'] as $k => $v) {
+                            if (!$user['Role'][$v]) {
+                                $hit = true;
+                                continue;
+                            }
+                        }
+                    }
+                    if (!$hit && !empty($role_req['AND'])) {
+                        $subhit = true;
+                        foreach ($role_req['AND'] as $k => $v) {
+                            $subhit = $subhit && $user['Role'][$v];
+                        }
+                        if ($subhit) {
+                            $hit = true;
+                        }
+                    }
+                    if (!$hit && !empty($role_req['OR'])) {
+                        foreach ($role_req['OR'] as $k => $v) {
+                            if ($user['Role'][$v]) {
+                                $hit = true;
+                                continue;
+                            }
+                        }
+                    }
+                    if ($hit) {
+                        $this->Log = ClassRegistry::init('Log');
+                        $this->Log->create();
+                        $this->Log->save(array(
+                                'org' => 'SYSTEM',
+                                'model' => 'User',
+                                'model_id' => $user['id'],
+                                'email' => $user['email'],
+                                'action' => 'security',
+                                'user_id' => $user['id'],
+                                'title' => __('User triggered security alert by attempting to access /%s/%s. Reason why this endpoint is of interest: %s', $controller, $action, $message),
+                        ));
+                    }
+                }
+            }
+        }
+    }
+
     // The check works like this:
     // If the user is a site admin, return true
     // If the requested action has an OR-d list, iterate through the list. If any of the permissions are set for the user, return true
@@ -567,6 +640,7 @@ class ACLComponent extends Component
         foreach ($aclList as $k => $v) {
             $aclList[$k] = array_change_key_case($v);
         }
+        $this->__checkLoggedActions($user, $controller, $action);
         if ($user['Role']['perm_site_admin']) {
             return true;
         }

programming_language: PHP
filename: ServersController.php
change_type: ModificationType.MODIFY
diff: @@ -44,12 +44,6 @@ class ServersController extends AppController
 
     public function index()
     {
-        if (!$this->_isSiteAdmin()) {
-            if (!$this->userRole['perm_sync'] && !$this->userRole['perm_admin']) {
-                $this->redirect(array('controller' => 'events', 'action' => 'index'));
-            }
-            $this->paginate['conditions'] = array('Server.org_id LIKE' => $this->Auth->user('org_id'));
-        }
         if ($this->_isRest()) {
             $params = array(
                 'recursive' => -1,
@@ -2089,4 +2083,28 @@ misp.direct_call(relative_path, body)
             }
         }
     }
+
+    public function resetRemoteAuthKey($id)
+    {
+        if (!$this->request->is('post')) {
+            throw new MethodNotAllowedException(__('This endpoint expects POST requests.'));
+        }
+        $result = $this->Server->resetRemoteAuthkey($id);
+        if ($result !== true) {
+            if (!$this->_isRest()) {
+                $this->Flash->error($result);
+                $this->redirect(array('action' => 'index'));
+            } else {
+                return $this->RestResponse->saveFailResponse('Servers', 'resetRemoteAuthKey', $id, $message, $this->response->type());
+            }
+        } else {
+            $message = __('API key updated.');
+            if (!$this->_isRest()) {
+                $this->Flash->success($message);
+                $this->redirect(array('action' => 'index'));
+            } else {
+                return $this->RestResponse->saveSuccessResponse('Servers', 'resetRemoteAuthKey', $message, $this->response->type());
+            }
+        }
+    }
 }

programming_language: PHP
filename: UsersController.php
change_type: ModificationType.MODIFY
diff: @@ -211,7 +211,7 @@ class UsersController extends AppController
                 // Save the data
                 if ($this->User->save($user)) {
                     $message = __('Password Changed.');
-                    $this->__extralog("change_pw");
+                    $this->User->extralog($this->Auth->user(), "change_pw", null, null, $user);
                     if ($this->_isRest()) {
                         return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message);
                     }
@@ -869,7 +869,11 @@ class UsersController extends AppController
                         $c++;
                     }
                     $fieldsResultStr = substr($fieldsResultStr, 2);
-                    $this->__extralog("edit", "user", $fieldsResultStr);
+                    $user = $this->User->find('first', array(
+                        'recursive' => -1,
+                        'conditions' => array('User.id' => $this->id)
+                    ));
+                    $this->User->extralog($this->Auth->user(), "edit", "user", $fieldsResultStr, $user);
                     if ($this->_isRest()) {
                         $user = $this->User->find('first', array(
                                 'conditions' => array('User.id' => $this->User->id),
@@ -954,7 +958,7 @@ class UsersController extends AppController
         }
         $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email'];
         if ($this->User->delete($id)) {
-            $this->__extralog("delete", $fieldsDescrStr, '');
+            $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, '');
             if ($this->_isRest()) {
                 return $this->RestResponse->saveSuccessResponse('User', 'admin_delete', $id, $this->response->type(), 'User deleted.');
             } else {
@@ -1010,7 +1014,7 @@ class UsersController extends AppController
             }
         }
         if ($this->Auth->login()) {
-            $this->__extralog("login");
+            $this->User->extralog($this->Auth->user(), "login");
             $this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
             $this->User->id = $this->Auth->user('id');
             $user = $this->User->find('first', array(
@@ -1125,7 +1129,7 @@ class UsersController extends AppController
     public function logout()
     {
         if ($this->Session->check('Auth.User')) {
-            $this->__extralog("logout");
+            $this->User->extralog($this->Auth->user(), "logout");
         }
         $this->Flash->info(__('Good-Bye'));
         $user = $this->User->find('first', array(
@@ -1140,7 +1144,7 @@ class UsersController extends AppController
         $this->redirect($this->Auth->logout());
     }
 
-    public function resetauthkey($id = null)
+    public function resetauthkey($id = null, $alert = false)
     {
         if (!$this->_isAdmin() && Configure::read('MISP.disableUserSelfManagement')) {
             throw new MethodNotAllowedException('User self-management has been disabled on this instance.');
@@ -1149,24 +1153,12 @@ class UsersController extends AppController
             $id = $this->Auth->user('id');
         }
         if (!$this->userRole['perm_auth']) {
-            throw new MethodNotAllowedException('Invalid action.');
+            throw new MethodNotAllowedException(__('Invalid action.'));
+        }
+        $newkey = $this->User->resetauthkey($this->Auth->user(), $id, $alert);
+        if ($newkey === false) {
+            throw new MethodNotAllowedException(__('Invalid user.'));
         }
-        $this->User->id = $id;
-        if (!$id || !$this->User->exists($id)) {
-            throw new MethodNotAllowedException('Invalid user.');
-        }
-        $user = $this->User->read();
-        $oldKey = $this->User->data['User']['authkey'];
-        if (!$this->_isSiteAdmin() && !($this->_isAdmin() && $this->Auth->user('org_id') == $this->User->data['User']['org_id']) && ($this->Auth->user('id') != $id)) {
-            throw new MethodNotAllowedException('Invalid user.');
-        }
-        $newkey = $this->User->generateAuthKey();
-        $this->User->saveField('authkey', $newkey);
-        $this->__extralog(
-                'reset_auth_key',
-                'Authentication key for user ' . $user['User']['id'] . ' (' . $user['User']['email'] . ')',
-                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')'
-        );
         if (!$this->_isRest()) {
             $this->Flash->success(__('New authkey generated.', true));
             $this->_refreshAuth();
@@ -1176,6 +1168,25 @@ class UsersController extends AppController
         }
     }
 
+    public function resetAllSyncAuthKeys()
+    {
+        if (!$this->request->is('post')) {
+            throw new MethodNotAllowedException(__('This functionality is only accessible via POST requests.'));
+        }
+        $results = $this->User->resetAllSyncAuthKeysRouter($this->Auth->user());
+        if ($results === true) {
+            $message = __('Job initiated.');
+        } else {
+            $message = __('%s authkeys reset, %s could not be reset.', $results['success'], $results['fails']);
+        }
+        if (!$this->_isRest()) {
+            $this->Flash->info($message);
+            $this->redirect($this->referer());
+        } else {
+            return $this->RestResponse->saveSuccessResponse('User', 'resetAllSyncAuthKeys', false, $this->response->type(), $message);
+        }
+    }
+
     public function histogram($selected = null)
     {
         //if (!$this->request->is('ajax') && !$this->_isRest()) throw new MethodNotAllowedException('This function can only be accessed via AJAX or the API.');
@@ -1297,44 +1308,6 @@ class UsersController extends AppController
         return $this->response;
     }
 
-    private function __extralog($action = null, $description = null, $fieldsResult = null)
-    {
-        // new data
-        $model = 'User';
-        $modelId = $this->Auth->user('id');
-        if ($action == 'login') {
-            $description = "User (" . $this->Auth->user('id') . "): " . $this->data['User']['email'];
-        } elseif ($action == 'logout') {
-            $description = "User (" . $this->Auth->user('id') . "): " . $this->Auth->user('email');
-        } elseif ($action == 'edit') {
-            $description = "User (" . $this->User->id . "): " . $this->data['User']['email'];
-        } elseif ($action == 'change_pw') {
-            $description = "User (" . $this->User->id . "): " . $this->Auth->user('email');
-            $fieldsResult = "Password changed.";
-        }
-
-        // query
-        $this->Log = ClassRegistry::init('Log');
-        $this->Log->create();
-        $this->Log->save(array(
-            'org' => $this->Auth->user('Organisation')['name'],
-            'model' => $model,
-            'model_id' => $modelId,
-            'email' => $this->Auth->user('email'),
-            'action' => $action,
-            'title' => $description,
-            'change' => isset($fieldsResult) ? $fieldsResult : ''));
-
-        // write to syslogd as well
-        App::import('Lib', 'SysLog.SysLog');
-        $syslog = new SysLog();
-        if (isset($fieldsResult) && $fieldsResult) {
-            $syslog->write('notice', $description . ' -- ' . $action . ' -- ' . $fieldsResult);
-        } else {
-            $syslog->write('notice', $description . ' -- ' . $action);
-        }
-    }
-
     // Used for fields_before and fields for audit
     public function arrayCopy(array $array)
     {

programming_language: PHP
filename: Log.php
change_type: ModificationType.MODIFY
diff: @@ -48,6 +48,7 @@ class Log extends AppModel
                             'request',
                             'request_delegation',
                             'reset_auth_key',
+                            'security',
                             'serverSettingsEdit',
                             'tag',
                             'undelete',

programming_language: PHP
filename: Server.php
change_type: ModificationType.MODIFY
diff: @@ -2289,10 +2289,10 @@ class Server extends AppModel
         if (!$existingEvent) {
             // add data for newly imported events
             $result = $eventModel->_add($event, true, $user, $server['Server']['org_id'], $passAlong, true, $jobId);
-            if ($result === true) {
+            if ($result) {
                 $successes[] = $eventId;
             } else {
-                $fails[$eventId] = __('Failed (partially?) because of errors: ') . $result;
+                $fails[$eventId] = __('Failed (partially?) because of validation errors: ') . json_encode($eventModel->validationErrors, true);
             }
         } else {
             if (!$existingEvent['Event']['locked'] && !$server['Server']['internal']) {
@@ -2316,6 +2316,7 @@ class Server extends AppModel
                 $eventId,
                 $server
         );
+        ;
         if (!empty($event)) {
             if ($this->__checkIfEventIsBlockedBeforePull($event)) {
                 return false;
@@ -2328,7 +2329,7 @@ class Server extends AppModel
             }
         } else {
             // error
-            $fails[$eventId] = __('failed downloading the event') . ': ' . json_encode($event);
+            $fails[$eventId] = __('failed downloading the event');
         }
         return true;
     }
@@ -5187,4 +5188,65 @@ class Server extends AppModel
         }
         return $results;
     }
+
+    public function resetRemoteAuthKey($id)
+    {
+        $server = $this->find('first', array(
+            'recursive' => -1,
+            'conditions' => array('Server.id' => $id)
+        ));
+        if (empty($server)) {
+            return __('Invalid server');
+        }
+        $HttpSocket = $this->setupHttpSocket($server);
+        $request = $this->setupSyncRequest($server);
+        $uri = $server['Server']['url'] . '/users/resetauthkey/me';
+        try {
+            $response = $HttpSocket->post($uri, '{}', $request);
+        } catch (Exception $e) {
+            $this->Log = ClassRegistry::init('Log');
+            $this->Log->create();
+            $message = 'Could not reset the remote authentication key.';
+            $this->Log->save(array(
+                    'org' => 'SYSTEM',
+                    'model' => 'Server',
+                    'model_id' => $id,
+                    'email' => 'SYSTEM',
+                    'action' => 'error',
+                    'user_id' => 0,
+                    'title' => 'Error: ' . $message,
+            ));
+            return $message;
+        }
+        if ($response->isOk()) {
+            try {
+                $response = json_decode($response->body, true);
+            } catch (Exception $e) {
+                $this->Log = ClassRegistry::init('Log');
+                $this->Log->create();
+                $message = 'Invalid response received from the remote instance.';
+                $this->Log->save(array(
+                        'org' => 'SYSTEM',
+                        'model' => 'Server',
+                        'model_id' => $id,
+                        'email' => 'SYSTEM',
+                        'action' => 'error',
+                        'user_id' => 0,
+                        'title' => 'Error: ' . $message,
+                ));
+                return $message;
+            }
+            if (!empty($response['message'])) {
+                $authkey = $response['message'];
+            }
+            if (substr($authkey, 0, 17) === 'Authkey updated: ') {
+                $authkey = substr($authkey, 17, 57);
+            }
+            $server['Server']['authkey'] = $authkey;
+            $this->save($server);
+            return true;
+        } else {
+            return __('Could not reset the remote authentication key.');
+        }
+    }
 }

programming_language: PHP
filename: User.php
change_type: ModificationType.MODIFY
diff: @@ -1253,4 +1253,174 @@ class User extends AppModel
         $this->save($admin);
         return $authKey;
     }
+
+    public function resetAllSyncAuthKeysRouter($user, $jobId = false)
+    {
+        if (Configure::read('MISP.background_jobs')) {
+            $job = ClassRegistry::init('Job');
+            $job->create();
+            $eventModel = ClassRegistry::init('Event');
+            $data = array(
+                    'worker' => $eventModel->__getPrioWorkerIfPossible(),
+                    'job_type' => __('reset_all_sync_api_keys'),
+                    'job_input' => __('Reseting all API keys'),
+                    'status' => 0,
+                    'retries' => 0,
+                    'org_id' => $user['org_id'],
+                    'org' => $user['Organisation']['name'],
+                    'message' => 'Issuing new API keys to all sync users.',
+            );
+            $job->save($data);
+            $jobId = $job->id;
+            $process_id = CakeResque::enqueue(
+                    'prio',
+                    'AdminShell',
+                    array('resetSyncAuthkeys', $user['id'], $jobId),
+                    true
+            );
+            $job->saveField('process_id', $process_id);
+            return true;
+        } else {
+            return $this->resetAllSyncAuthKeys($user);
+        }
+    }
+
+    public function resetAllSyncAuthKeys($user, $jobId = false)
+    {
+        $affected_users = $this->find('all', array(
+            'recursive' => -1,
+            'contain' => array('Role'),
+            'conditions' => array(
+                'OR' => array(
+                    'Role.perm_sync' => 1,
+                    'Role.perm_admin' => 1
+                ),
+                'Role.perm_site_admin' => 0
+            )
+        ));
+        $results = array('success' => 0, 'fails' => 0);
+        $user_count = count($affected_users);
+        if ($jobId) {
+            $job = ClassRegistry::init('Job');
+            $existingJob = $job->find('first', array(
+                'conditions' => array('Job.id' => $jobId),
+                'recursive' => -1
+            ));
+            if (empty($existingJob)) {
+                $jobId = false;
+            }
+        }
+        foreach ($affected_users as $k => $affected_user) {
+            try {
+                $reset_result = $this->resetauthkey($user, $affected_user['User']['id'], true);
+                if ($reset_result) {
+                    $results['success'] += 1;
+                } else {
+                    $results['fails'] += 1;
+                }
+            } catch (Exception $e) {
+                $results['fails'] += 1;
+            }
+            if ($jobId) {
+                if ($k % 100 == 0) {
+                    $job->id =  $jobId;
+                    $job->saveField('progress', 100 * (($k + 1) / count($user_count)));
+                    $job->saveField('message', __('Reset in progress - %s/%s.', $k, $user_count));
+                }
+            }
+        }
+        if ($jobId) {
+            $message = __('%s authkeys reset, %s could not be reset', $results['success'], $results['fails']);
+            $job->saveField('progress', 100);
+            $job->saveField('message', $message);
+            $job->saveField('status', 4);
+        }
+        return $results;
+    }
+
+    public function resetauthkey($user, $id, $alert = false)
+    {
+        $this->id = $id;
+        if (!$id || !$this->exists($id)) {
+            return false;
+        }
+        $updatedUser = $this->read();
+        $oldKey = $this->data['User']['authkey'];
+        if (empty($user['Role']['perm_site_admin']) && !($user['Role']['perm_admin'] && $user['org_id'] == $updatedUser['User']['org_id']) && ($user['id'] != $id)) {
+            return false;
+        }
+        $newkey = $this->generateAuthKey();
+        $this->saveField('authkey', $newkey);
+        $this->extralog(
+                $user,
+                'reset_auth_key',
+                sprintf(
+                    __('Authentication key for user %s (%s) updated.'),
+                    $updatedUser['User']['id'],
+                    $updatedUser['User']['email']
+                ),
+                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')',
+                $updatedUser
+        );
+        if ($alert) {
+            $baseurl = Configure::read('MISP.external_baseurl');
+            if (empty($baseurl)) {
+                $baseurl = Configure::read('MISP.baseurl');
+            }
+            $body = __(
+                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key is: %s\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
+                $baseurl,
+                $newkey
+            );
+            $bodyNoEnc = __(
+                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key can be retrieved by logging in using this sync user's account.\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
+                $baseurl,
+                $newkey
+            );
+            $this->sendEmail(
+                $updatedUser,
+                $body,
+                $bodyNoEnc,
+                __('API key reset by administrator')
+            );
+        }
+        return $newkey;
+    }
+
+    public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null)
+    {
+        // new data
+        $model = 'User';
+        $modelId = $user['id'];
+        if (!empty($modifiedUser)) {
+            $modelId = $modifiedUser['User']['id'];
+        }
+        if ($action == 'login') {
+            $description = "User (" . $user['id'] . "): " . $user['email'];
+        } elseif ($action == 'logout') {
+            $description = "User (" . $user['id'] . "): " . $user['email'];
+        } elseif ($action == 'edit') {
+            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
+        } elseif ($action == 'change_pw') {
+            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
+            $fieldsResult = "Password changed.";
+        }
+
+        // query
+        $this->Log = ClassRegistry::init('Log');
+        $this->Log->create();
+        $this->Log->save(array(
+            'org' => $user['Organisation']['name'],
+            'model' => $model,
+            'model_id' => $modelId,
+            'email' => $user['email'],
+            'action' => $action,
+            'title' => $description,
+            'change' => isset($fieldsResult) ? $fieldsResult : ''));
+
+        // write to syslogd as well
+        App::import('Lib', 'SysLog.SysLog');
+        $syslog = new SysLog();
+        $syslog->write('notice', $description . ' -- ' . $action . (empty($fieldResult) ? '' : '-- ' . $fieldResult));
+    }
 }

programming_language: PHP
filename: index.ctp
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@
             <th><?php echo $this->Paginator->sort('id');?></th>
             <th><?php echo $this->Paginator->sort('name');?></th>
             <th><?php echo __('Connection test');?></th>
+            <th><?php echo __('Reset API key');?></th>
             <th><?php echo $this->Paginator->sort('internal');?></th>
             <th><?php echo $this->Paginator->sort('push');?></th>
             <th><?php echo $this->Paginator->sort('pull');?></th>
@@ -70,6 +71,20 @@ foreach ($servers as $server):
             ?>
         </td>
         <td id="connection_test_<?php echo $server['Server']['id'];?>"><span role="button" tabindex="0" aria-label="<?php echo __('Test the connection to the remote instance');?>" title="<?php echo __('Test the connection to the remote instance');?>" class="btn btn-primary" style="line-height:10px; padding: 4px 4px;" onClick="testConnection('<?php echo $server['Server']['id'];?>');"><?php echo __('Run');?></span></td>
+        <td id="reset_api_key_<?php echo $server['Server']['id'];?>">
+            <?php
+                echo $this->Form->postLink(
+                    __('Reset'),
+                    $baseurl . '/servers/resetRemoteAuthKey/' . $server['Server']['id'],
+                    array(
+                        'style' => 'line-height:10px; padding: 4px 4px;',
+                        'title' => __('Remotely reset API key'),
+                        'aria-label' => __('Remotely reset API key'),
+                        'class' => 'btn btn-primary'
+                    )
+                );
+            ?>
+        </td>
 
 <td><span class="<?php echo ($server['Server']['internal']? 'icon-ok' : 'icon-remove'); ?>" role="img" aria-label="<?php echo ($server['Server']['internal']? __('Yes') : __('No')); ?>" title="<?php echo ($server['Server']['internal']? __('Internal instance that ignores distribution level degradation *WARNING: Only use this setting if you have several internal instances and the sync link is to an internal extension of the current MISP community*') : __('Normal sync link to an external MISP instance. Distribution degradation will follow the normal rules.')); ?>"></span></td>
         <td><span class="<?php echo ($server['Server']['push']? 'icon-ok' : 'icon-remove'); ?>" role="img" aria-label="<?php echo ($server['Server']['push']? __('Yes') : __('No')); ?>"></span><span class="short <?php if (!$server['Server']['push'] || empty($ruleDescription['push'])) echo "hidden"; ?>" data-toggle="popover" title="Distribution List" data-content="<?php echo $ruleDescription['push']; ?>"> (<?php echo __('Rules');?>)</span></td>

programming_language: PHP
filename: admin_index.ctp
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,25 @@
 <div class="users index">
     <h2><?php echo __('Users');?></h2>
+    <?php
+        if ($isSiteAdmin) {
+            echo sprintf(
+                '<span>%s</span>',
+                __(
+                    'Click %s to reset the API keys of all sync and org admin users in one shot. This will also automatically inform them of their new API keys.',
+                    $this->Form->postLink(
+                        __('here'),
+                        $baseurl . '/users/resetAllSyncAuthKeys',
+                        array(
+                            'title' => __('Reset all sync user API keys'),
+                            'aria-label' => __('Reset all sync user API keys'),
+                            'class' => 'bold'
+                        ),
+                        __('Are you sure you wish to reset the API keys of all users with sync privileges?')
+                    )
+                )
+            );
+        }
+    ?>
     <div class="pagination">
         <ul>
         <?php

programming_language: PHP