diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php
index 55cf99b5f..4b258080c 100755
--- a/app/Controller/AppController.php
+++ b/app/Controller/AppController.php
@@ -79,6 +79,7 @@ class AppController extends Controller
),
'Security',
'ACL',
+ 'CompressedRequestHandler',
'RestResponse',
'Flash',
'Toolbox',
diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php
index 73c481740..8b1364a0c 100644
--- a/app/Controller/Component/ACLComponent.php
+++ b/app/Controller/Component/ACLComponent.php
@@ -262,7 +262,9 @@ class ACLComponent extends Component
'viewEventAttributes' => array('*'),
'viewGraph' => array('*'),
'viewGalaxyMatrix' => array('*'),
- 'xml' => array('*')
+ 'xml' => array('*'),
+ 'addEventLock' => ['perm_auth'],
+ 'removeEventLock' => ['perm_auth'],
),
'favouriteTags' => array(
'toggle' => array('*'),
@@ -495,7 +497,7 @@ class ACLComponent extends Component
'getSubmodulesStatus' => array(),
'getSubmoduleQuickUpdateForm' => array(),
'getWorkers' => array(),
- 'getVersion' => array('*'),
+ 'getVersion' => array('perm_auth'),
'idTranslator' => ['OR' => [
'host_org_user',
'perm_site_admin',
diff --git a/app/Controller/Component/CompressedRequestHandlerComponent.php b/app/Controller/Component/CompressedRequestHandlerComponent.php
new file mode 100644
index 000000000..969303257
--- /dev/null
+++ b/app/Controller/Component/CompressedRequestHandlerComponent.php
@@ -0,0 +1,145 @@
+request->setInput($this->decodeBrotliEncodedContent($controller));
+ } else if ($contentEncoding === 'gzip') {
+ $controller->request->setInput($this->decodeGzipEncodedContent($controller));
+ } else {
+ throw new MethodNotAllowedException("Unsupported content encoding '$contentEncoding'.");
+ }
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function supportedEncodings()
+ {
+ $supportedEncodings = [];
+ if (function_exists('gzdecode') || function_exists('inflate_init')) {
+ $supportedEncodings[] = 'gzip';
+ }
+ if (function_exists('brotli_uncompress') || function_exists('brotli_uncompress_init')) {
+ $supportedEncodings[] = 'br';
+ }
+ return $supportedEncodings;
+ }
+
+ /**
+ * @return string
+ * @throws Exception
+ * @see CakeRequest::_readInput()
+ */
+ private function decodeGzipEncodedContent(Controller $controller)
+ {
+ if (function_exists('inflate_init')) {
+ // Decompress data on the fly if supported
+ $resource = inflate_init(ZLIB_ENCODING_GZIP);
+ if ($resource === false) {
+ throw new Exception('GZIP incremental uncompress init failed.');
+ }
+ $uncompressed = '';
+ foreach ($this->streamInput() as $data) {
+ $uncompressedChunk = inflate_add($resource, $data);
+ if ($uncompressedChunk === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ $uncompressed .= $uncompressedChunk;
+ if (strlen($uncompressed) > self::MAX_SIZE) {
+ throw new Exception("Uncompressed data are bigger than is limit.");
+ }
+ }
+ $uncompressedChunk = inflate_add($resource, '', ZLIB_FINISH);
+ if ($uncompressedChunk === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ return $uncompressed . $uncompressedChunk;
+
+ } else if (function_exists('gzdecode')) {
+ $decoded = gzdecode($controller->request->input(), self::MAX_SIZE);
+ if ($decoded === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ if (strlen($decoded) >= self::MAX_SIZE) {
+ throw new Exception("Uncompressed data are bigger than is limit.");
+ }
+ return $decoded;
+ } else {
+ throw new MethodNotAllowedException("This server doesn't support GZIP compressed requests.");
+ }
+ }
+
+ /**
+ * @param Controller $controller
+ * @return string
+ * @throws Exception
+ * @see CakeRequest::_readInput()
+ */
+ private function decodeBrotliEncodedContent(Controller $controller)
+ {
+ if (function_exists('brotli_uncompress_init')) {
+ // Decompress data on the fly if supported
+ $resource = brotli_uncompress_init();
+ if ($resource === false) {
+ throw new Exception('Brotli incremental uncompress init failed.');
+ }
+ $uncompressed = '';
+ foreach ($this->streamInput() as $data) {
+ $uncompressedChunk = brotli_uncompress_add($resource, $data, BROTLI_PROCESS);
+ if ($uncompressedChunk === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ $uncompressed .= $uncompressedChunk;
+ if (strlen($uncompressed) > self::MAX_SIZE) {
+ throw new Exception("Uncompressed data are bigger than is limit.");
+ }
+ }
+ $uncompressedChunk = brotli_uncompress_add($resource, '', BROTLI_FINISH);
+ if ($uncompressedChunk === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ return $uncompressed . $uncompressedChunk;
+
+ } else if (function_exists('brotli_uncompress')) {
+ $decoded = brotli_uncompress($controller->request->input(), self::MAX_SIZE);
+ if ($decoded === false) {
+ throw new MethodNotAllowedException('Invalid compressed data.');
+ }
+ if (strlen($decoded) >= self::MAX_SIZE) {
+ throw new Exception("Uncompressed data are bigger than is limit.");
+ }
+ return $decoded;
+ } else {
+ throw new MethodNotAllowedException("This server doesn't support brotli compressed requests.");
+ }
+ }
+
+ /**
+ * @param int $chunkSize
+ * @return Generator
+ * @throws Exception
+ */
+ private function streamInput($chunkSize = 8192)
+ {
+ $fh = fopen('php://input', 'rb');
+ if ($fh === false) {
+ throw new Exception("Could not open PHP input for reading.");
+ }
+ while (!feof($fh)) {
+ $data = fread($fh, $chunkSize);
+ if ($data === false) {
+ throw new Exception("Could not read PHP input.");
+ }
+ yield $data;
+ }
+ fclose($fh);
+ }
+}
diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php
index 1f11acf7a..0c5377b08 100644
--- a/app/Controller/EventsController.php
+++ b/app/Controller/EventsController.php
@@ -5312,9 +5312,43 @@ class EventsController extends AppController
}
}
+ public function addEventLock($id)
+ {
+ if (!$this->request->is('post')) {
+ throw new MethodNotAllowedException('This endpoint requires a POST request.');
+ }
+
+ $event = $this->Event->fetchSimpleEvent($this->Auth->User(), $id);
+ if (empty($event)) {
+ throw new MethodNotAllowedException(__('Invalid Event'));
+ }
+ if (!$this->__canModifyEvent($event)) {
+ throw new UnauthorizedException(__('You do not have permission to do that.'));
+ }
+
+ $this->loadModel('EventLock');
+ $lockId = $this->EventLock->insertLockApi($event['Event']['id'], $this->Auth->user());
+ return $this->RestResponse->viewData(['lock_id' => $lockId], $this->response->type());
+ }
+
+ public function removeEventLock($id, $lockId)
+ {
+ if (!$this->request->is('post')) {
+ throw new MethodNotAllowedException('This endpoint requires a POST request.');
+ }
+
+ $event = $this->Event->fetchSimpleEvent($this->Auth->User(), $id);
+ if (empty($event)) {
+ throw new MethodNotAllowedException(__('Invalid Event'));
+ }
+
+ $this->loadModel('EventLock');
+ $deleted = $this->EventLock->deleteApiLock($event['Event']['id'], $lockId, $this->Auth->user());
+ return $this->RestResponse->viewData(['deleted' => $deleted], $this->response->type());
+ }
+
public function checkLocks($id)
{
- $this->loadModel('EventLock');
$event = $this->Event->find('first', array(
'recursive' => -1,
'conditions' => array('Event.id' => $id),
@@ -5322,29 +5356,34 @@ class EventsController extends AppController
));
$locks = array();
if (!empty($event) && ($event['Event']['orgc_id'] == $this->Auth->user('org_id') || $this->_isSiteAdmin())) {
+ $this->loadModel('EventLock');
$locks = $this->EventLock->checkLock($this->Auth->user(), $id);
}
if (!empty($locks)) {
$temp = $locks;
$locks = array();
foreach ($temp as $t) {
- if ($t['User']['id'] !== $this->Auth->user('id')) {
+ if ($t['type'] === 'user' && $t['User']['id'] !== $this->Auth->user('id')) {
if (!$this->_isSiteAdmin() && $t['User']['org_id'] != $this->Auth->user('org_id')) {
- continue;
+ $locks[] = __('another user');
+ } else {
+ $locks[] = $t['User']['email'];
}
- $locks[] = $t['User']['email'];
+ } else if ($t['type'] === 'job') {
+ $locks[] = __('background job');
+ } else if ($t['type'] === 'api') {
+ $locks[] = __('external tool');
}
}
}
- // TODO: i18n
- if (!empty($locks)) {
- $message = sprintf('Warning: Your view on this event might not be up to date as it is currently being edited by: %s', implode(', ', $locks));
- $this->set('message', $message);
- $this->layout = false;
- $this->render('/Events/ajax/event_lock');
- } else {
+ if (empty($locks)) {
return $this->RestResponse->viewData('', $this->response->type(), false, true);
}
+
+ $message = __('Warning: Your view on this event might not be up to date as it is currently being edited by: %s', implode(', ', $locks));
+ $this->set('message', $message);
+ $this->layout = false;
+ $this->render('/Events/ajax/event_lock');
}
public function getEditStrategy($id)
diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php
index 8609a3b00..b1c9c8972 100644
--- a/app/Controller/ServersController.php
+++ b/app/Controller/ServersController.php
@@ -2,6 +2,7 @@
App::uses('AppController', 'Controller');
App::uses('Xml', 'Utility');
App::uses('AttachmentTool', 'Tools');
+App::uses('SecurityAudit', 'Tools');
/**
* @property Server $Server
@@ -938,253 +939,254 @@ class ServersController extends AppController
public function serverSettings($tab=false)
{
- if (!$this->_isSiteAdmin()) {
- throw new MethodNotAllowedException();
+ if (!$this->request->is('get')) {
+ throw new MethodNotAllowedException('Just GET method is allowed.');
}
- if ($this->request->is('Get')) {
- $tabs = array(
- 'MISP' => array('count' => 0, 'errors' => 0, 'severity' => 5),
- 'Encryption' => array('count' => 0, 'errors' => 0, 'severity' => 5),
- 'Proxy' => array('count' => 0, 'errors' => 0, 'severity' => 5),
- 'Security' => array('count' => 0, 'errors' => 0, 'severity' => 5),
- 'Plugin' => array('count' => 0, 'errors' => 0, 'severity' => 5)
- );
- $writeableErrors = array(0 => __('OK'), 1 => __('not found'), 2 => __('is not writeable'));
- $readableErrors = array(0 => __('OK'), 1 => __('not readable'));
- $gpgErrors = array(0 => __('OK'), 1 => __('FAIL: settings not set'), 2 => __('FAIL: Failed to load GnuPG'), 3 => __('FAIL: Issues with the key/passphrase'), 4 => __('FAIL: sign failed'));
- $proxyErrors = array(0 => __('OK'), 1 => __('not configured (so not tested)'), 2 => __('Getting URL via proxy failed'));
- $zmqErrors = array(0 => __('OK'), 1 => __('not enabled (so not tested)'), 2 => __('Python ZeroMQ library not installed correctly.'), 3 => __('ZeroMQ script not running.'));
- $stixOperational = array(0 => __('Some of the libraries related to STIX are not installed. Make sure that all libraries listed below are correctly installed.'), 1 => __('OK'));
- $stixVersion = array(0 => __('Incorrect STIX version installed, found $current, expecting $expected'), 1 => __('OK'));
- $stix2Version = array(0 => __('Incorrect STIX2 version installed, found $current, expecting $expected'), 1 => __('OK'));
- $cyboxVersion = array(0 => __('Incorrect CyBox version installed, found $current, expecting $expected'), 1 => __('OK'));
- $mixboxVersion = array(0 => __('Incorrect mixbox version installed, found $current, expecting $expected'), 1 => __('OK'));
- $maecVersion = array(0 => __('Incorrect maec version installed, found $current, expecting $expected'), 1 => __('OK'));
- $pymispVersion = array(0 => __('Incorrect PyMISP version installed, found $current, expecting $expected'), 1 => __('OK'));
- $sessionErrors = array(0 => __('OK'), 1 => __('High'), 2 => __('Alternative setting used'), 3 => __('Test failed'));
- $moduleErrors = array(0 => __('OK'), 1 => __('System not enabled'), 2 => __('No modules found'));
+ $tabs = array(
+ 'MISP' => array('count' => 0, 'errors' => 0, 'severity' => 5),
+ 'Encryption' => array('count' => 0, 'errors' => 0, 'severity' => 5),
+ 'Proxy' => array('count' => 0, 'errors' => 0, 'severity' => 5),
+ 'Security' => array('count' => 0, 'errors' => 0, 'severity' => 5),
+ 'Plugin' => array('count' => 0, 'errors' => 0, 'severity' => 5)
+ );
+ $writeableErrors = array(0 => __('OK'), 1 => __('not found'), 2 => __('is not writeable'));
+ $readableErrors = array(0 => __('OK'), 1 => __('not readable'));
+ $gpgErrors = array(0 => __('OK'), 1 => __('FAIL: settings not set'), 2 => __('FAIL: Failed to load GnuPG'), 3 => __('FAIL: Issues with the key/passphrase'), 4 => __('FAIL: sign failed'));
+ $proxyErrors = array(0 => __('OK'), 1 => __('not configured (so not tested)'), 2 => __('Getting URL via proxy failed'));
+ $zmqErrors = array(0 => __('OK'), 1 => __('not enabled (so not tested)'), 2 => __('Python ZeroMQ library not installed correctly.'), 3 => __('ZeroMQ script not running.'));
+ $stixOperational = array(0 => __('Some of the libraries related to STIX are not installed. Make sure that all libraries listed below are correctly installed.'), 1 => __('OK'));
+ $stixVersion = array(0 => __('Incorrect STIX version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $stix2Version = array(0 => __('Incorrect STIX2 version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $cyboxVersion = array(0 => __('Incorrect CyBox version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $mixboxVersion = array(0 => __('Incorrect mixbox version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $maecVersion = array(0 => __('Incorrect maec version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $pymispVersion = array(0 => __('Incorrect PyMISP version installed, found $current, expecting $expected'), 1 => __('OK'));
+ $sessionErrors = array(0 => __('OK'), 1 => __('High'), 2 => __('Alternative setting used'), 3 => __('Test failed'));
+ $moduleErrors = array(0 => __('OK'), 1 => __('System not enabled'), 2 => __('No modules found'));
- $finalSettings = $this->Server->serverSettingsRead();
- $issues = array(
- 'errors' => array(
- 0 => array(
- 'value' => 0,
- 'description' => __('MISP will not operate correctly or will be unsecure until these issues are resolved.')
- ),
- 1 => array(
- 'value' => 0,
- 'description' => __('Some of the features of MISP cannot be utilised until these issues are resolved.')
- ),
- 2 => array(
- 'value' => 0,
- 'description' => __('There are some optional tweaks that could be done to improve the looks of your MISP instance.')
- ),
+ $finalSettings = $this->Server->serverSettingsRead();
+ $issues = array(
+ 'errors' => array(
+ 0 => array(
+ 'value' => 0,
+ 'description' => __('MISP will not operate correctly or will be unsecure until these issues are resolved.')
),
- 'deprecated' => array(),
- 'overallHealth' => 3,
- );
- $dumpResults = array();
- $tempArray = array();
- foreach ($finalSettings as $k => $result) {
- if ($result['level'] == 3) {
- $issues['deprecated']++;
+ 1 => array(
+ 'value' => 0,
+ 'description' => __('Some of the features of MISP cannot be utilised until these issues are resolved.')
+ ),
+ 2 => array(
+ 'value' => 0,
+ 'description' => __('There are some optional tweaks that could be done to improve the looks of your MISP instance.')
+ ),
+ ),
+ 'deprecated' => array(),
+ 'overallHealth' => 3,
+ );
+ $dumpResults = array();
+ $tempArray = array();
+ foreach ($finalSettings as $k => $result) {
+ if ($result['level'] == 3) {
+ $issues['deprecated']++;
+ }
+ $tabs[$result['tab']]['count']++;
+ if (isset($result['error']) && $result['level'] < 3) {
+ $issues['errors'][$result['level']]['value']++;
+ if ($result['level'] < $issues['overallHealth']) {
+ $issues['overallHealth'] = $result['level'];
}
- $tabs[$result['tab']]['count']++;
- if (isset($result['error']) && $result['level'] < 3) {
- $issues['errors'][$result['level']]['value']++;
- if ($result['level'] < $issues['overallHealth']) {
- $issues['overallHealth'] = $result['level'];
- }
- $tabs[$result['tab']]['errors']++;
- if ($result['level'] < $tabs[$result['tab']]['severity']) {
- $tabs[$result['tab']]['severity'] = $result['level'];
- }
- }
- if (isset($result['optionsSource']) && is_callable($result['optionsSource'])) {
- $result['options'] = $result['optionsSource']();
- }
- $dumpResults[] = $result;
- if ($result['tab'] == $tab) {
- if (isset($result['subGroup'])) {
- $tempArray[$result['subGroup']][] = $result;
- } else {
- $tempArray['general'][] = $result;
- }
+ $tabs[$result['tab']]['errors']++;
+ if ($result['level'] < $tabs[$result['tab']]['severity']) {
+ $tabs[$result['tab']]['severity'] = $result['level'];
}
}
- $finalSettings = $tempArray;
- // Diagnostics portion
- $diagnostic_errors = 0;
- App::uses('File', 'Utility');
- App::uses('Folder', 'Utility');
- if ($tab === 'files') {
- $files = $this->__manageFiles();
- $this->set('files', $files);
+ if (isset($result['optionsSource']) && is_callable($result['optionsSource'])) {
+ $result['options'] = $result['optionsSource']();
}
- // Only run this check on the diagnostics tab
- if ($tab === 'diagnostics' || $tab === 'download' || $this->_isRest()) {
- $php_ini = php_ini_loaded_file();
- $this->set('php_ini', $php_ini);
-
- $attachmentTool = new AttachmentTool();
- try {
- $advanced_attachments = $attachmentTool->checkAdvancedExtractionStatus($this->Server->getPythonVersion());
- } catch (Exception $e) {
- $this->log($e->getMessage(), LOG_NOTICE);
- $advanced_attachments = false;
+ $dumpResults[] = $result;
+ if ($result['tab'] == $tab) {
+ if (isset($result['subGroup'])) {
+ $tempArray[$result['subGroup']][] = $result;
+ } else {
+ $tempArray['general'][] = $result;
}
-
- $this->set('advanced_attachments', $advanced_attachments);
- // check if the current version of MISP is outdated or not
- $version = $this->__checkVersion();
- $this->set('version', $version);
- $gitStatus = $this->Server->getCurrentGitStatus();
- $this->set('branch', $gitStatus['branch']);
- $this->set('commit', $gitStatus['commit']);
- $this->set('latestCommit', $gitStatus['latestCommit']);
-
- $phpSettings = array(
- 'max_execution_time' => array(
- 'explanation' => 'The maximum duration that a script can run (does not affect the background workers). A too low number will break long running scripts like comprehensive API exports',
- 'recommended' => 300,
- 'unit' => 'seconds',
- ),
- 'memory_limit' => array(
- 'explanation' => 'The maximum memory that PHP can consume. It is recommended to raise this number since certain exports can generate a fair bit of memory usage',
- 'recommended' => 2048,
- 'unit' => 'MB'
- ),
- 'upload_max_filesize' => array(
- 'explanation' => 'The maximum size that an uploaded file can be. It is recommended to raise this number to allow for the upload of larger samples',
- 'recommended' => 50,
- 'unit' => 'MB'
- ),
- 'post_max_size' => array(
- 'explanation' => 'The maximum size of a POSTed message, this has to be at least the same size as the upload_max_filesize setting',
- 'recommended' => 50,
- 'unit' => 'MB'
- )
- );
-
- foreach ($phpSettings as $setting => $settingArray) {
- $phpSettings[$setting]['value'] = $this->Server->getIniSetting($setting);
- if ($phpSettings[$setting]['value'] && $settingArray['unit'] && $settingArray['unit'] === 'MB') {
- // convert basic unit to M
- $phpSettings[$setting]['value'] = (int) floor($phpSettings[$setting]['value'] / 1024 / 1024);
- }
- }
- $this->set('phpSettings', $phpSettings);
-
- if ($version && (!$version['upToDate'] || $version['upToDate'] == 'older')) {
- $diagnostic_errors++;
- }
-
- // check if the STIX and Cybox libraries are working and the correct version using the test script stixtest.py
- $stix = $this->Server->stixDiagnostics($diagnostic_errors, $stixVersion, $cyboxVersion, $mixboxVersion, $maecVersion, $stix2Version, $pymispVersion);
-
- $yaraStatus = $this->Server->yaraDiagnostics($diagnostic_errors);
-
- // if GnuPG is set up in the settings, try to encrypt a test message
- $gpgStatus = $this->Server->gpgDiagnostics($diagnostic_errors);
-
- // if the message queue pub/sub is enabled, check whether the extension works
- $zmqStatus = $this->Server->zmqDiagnostics($diagnostic_errors);
-
- // if Proxy is set up in the settings, try to connect to a test URL
- $proxyStatus = $this->Server->proxyDiagnostics($diagnostic_errors);
-
- // get the DB diagnostics
- $dbDiagnostics = $this->Server->dbSpaceUsage();
- $dbSchemaDiagnostics = $this->Server->dbSchemaDiagnostic();
-
- $redisInfo = $this->Server->redisInfo();
-
- $moduleTypes = array('Enrichment', 'Import', 'Export', 'Cortex');
- foreach ($moduleTypes as $type) {
- $moduleStatus[$type] = $this->Server->moduleDiagnostics($diagnostic_errors, $type);
- }
-
- // check the size of the session table
- $sessionCount = 0;
- $sessionStatus = $this->Server->sessionDiagnostics($diagnostic_errors, $sessionCount);
- $this->set('sessionCount', $sessionCount);
-
- $this->loadModel('AttachmentScan');
- try {
- $attachmentScan = ['status' => true, 'software' => $this->AttachmentScan->diagnostic()];
- } catch (Exception $e) {
- $attachmentScan = ['status' => false, 'error' => $e->getMessage()];
- }
-
- $view = compact('gpgStatus', 'sessionErrors', 'proxyStatus', 'sessionStatus', 'zmqStatus', 'stixVersion', 'cyboxVersion', 'mixboxVersion', 'maecVersion', 'stix2Version', 'pymispVersion', 'moduleStatus', 'yaraStatus', 'gpgErrors', 'proxyErrors', 'zmqErrors', 'stixOperational', 'stix', 'moduleErrors', 'moduleTypes', 'dbDiagnostics', 'dbSchemaDiagnostics', 'redisInfo', 'attachmentScan');
- } else {
- $view = [];
}
- // check whether the files are writeable
- $writeableDirs = $this->Server->writeableDirsDiagnostics($diagnostic_errors);
- $writeableFiles = $this->Server->writeableFilesDiagnostics($diagnostic_errors);
- $readableFiles = $this->Server->readableFilesDiagnostics($diagnostic_errors);
- $extensions = $this->Server->extensionDiagnostics();
-
- // check if the encoding is not set to utf8
- $dbEncodingStatus = $this->Server->databaseEncodingDiagnostics($diagnostic_errors);
-
- $view = array_merge($view, compact('diagnostic_errors', 'tabs', 'tab', 'issues', 'finalSettings', 'writeableErrors', 'readableErrors', 'writeableDirs', 'writeableFiles', 'readableFiles', 'extensions', 'dbEncodingStatus'));
- $this->set($view);
-
- $workerIssueCount = 4;
- $worker_array = array();
- if (Configure::read('MISP.background_jobs')) {
- $workerIssueCount = 0;
- $worker_array = $this->Server->workerDiagnostics($workerIssueCount);
- }
- $this->set('worker_array', $worker_array);
- if ($tab == 'download' || $this->_isRest()) {
- foreach ($dumpResults as $key => $dr) {
- unset($dumpResults[$key]['description']);
- }
- $dump = array(
- 'version' => $version,
- 'phpSettings' => $phpSettings,
- 'gpgStatus' => $gpgErrors[$gpgStatus['status']],
- 'proxyStatus' => $proxyErrors[$proxyStatus],
- 'zmqStatus' => $zmqStatus,
- 'stix' => $stix,
- 'moduleStatus' => $moduleStatus,
- 'writeableDirs' => $writeableDirs,
- 'writeableFiles' => $writeableFiles,
- 'readableFiles' => $readableFiles,
- 'dbDiagnostics' => $dbDiagnostics,
- 'dbSchemaDiagnostics' => $dbSchemaDiagnostics,
- 'redisInfo' => $redisInfo,
- 'finalSettings' => $dumpResults,
- 'extensions' => $extensions,
- 'workers' => $worker_array
- );
- foreach ($dump['finalSettings'] as $k => $v) {
- if (!empty($v['redacted'])) {
- $dump['finalSettings'][$k]['value'] = '*****';
- }
- }
- $this->response->body(json_encode($dump, JSON_PRETTY_PRINT));
- $this->response->type('json');
- $this->response->download('MISP.report.json');
- return $this->response;
- }
-
- $priorities = array(0 => 'Critical', 1 => 'Recommended', 2 => 'Optional', 3 => 'Deprecated');
- $this->set('priorities', $priorities);
- $this->set('workerIssueCount', $workerIssueCount);
- $priorityErrorColours = array(0 => 'red', 1 => 'yellow', 2 => 'green');
- $this->set('priorityErrorColours', $priorityErrorColours);
- $this->set('phpversion', phpversion());
- $this->set('phpmin', $this->phpmin);
- $this->set('phprec', $this->phprec);
- $this->set('phptoonew', $this->phptoonew);
- $this->set('pythonmin', $this->pythonmin);
- $this->set('pythonrec', $this->pythonrec);
- $this->set('pymisp', $this->pymisp);
}
+ $finalSettings = $tempArray;
+ // Diagnostics portion
+ $diagnostic_errors = 0;
+ App::uses('File', 'Utility');
+ App::uses('Folder', 'Utility');
+ if ($tab === 'files') {
+ $files = $this->Server->grabFiles();
+ $this->set('files', $files);
+ }
+ // Only run this check on the diagnostics tab
+ if ($tab === 'diagnostics' || $tab === 'download' || $this->_isRest()) {
+ $php_ini = php_ini_loaded_file();
+ $this->set('php_ini', $php_ini);
+
+ $attachmentTool = new AttachmentTool();
+ try {
+ $advanced_attachments = $attachmentTool->checkAdvancedExtractionStatus($this->Server->getPythonVersion());
+ } catch (Exception $e) {
+ $this->log($e->getMessage(), LOG_NOTICE);
+ $advanced_attachments = false;
+ }
+
+ $this->set('advanced_attachments', $advanced_attachments);
+ // check if the current version of MISP is outdated or not
+ $version = $this->__checkVersion();
+ $this->set('version', $version);
+ $gitStatus = $this->Server->getCurrentGitStatus();
+ $this->set('branch', $gitStatus['branch']);
+ $this->set('commit', $gitStatus['commit']);
+ $this->set('latestCommit', $gitStatus['latestCommit']);
+
+ $phpSettings = array(
+ 'max_execution_time' => array(
+ 'explanation' => 'The maximum duration that a script can run (does not affect the background workers). A too low number will break long running scripts like comprehensive API exports',
+ 'recommended' => 300,
+ 'unit' => 'seconds',
+ ),
+ 'memory_limit' => array(
+ 'explanation' => 'The maximum memory that PHP can consume. It is recommended to raise this number since certain exports can generate a fair bit of memory usage',
+ 'recommended' => 2048,
+ 'unit' => 'MB'
+ ),
+ 'upload_max_filesize' => array(
+ 'explanation' => 'The maximum size that an uploaded file can be. It is recommended to raise this number to allow for the upload of larger samples',
+ 'recommended' => 50,
+ 'unit' => 'MB'
+ ),
+ 'post_max_size' => array(
+ 'explanation' => 'The maximum size of a POSTed message, this has to be at least the same size as the upload_max_filesize setting',
+ 'recommended' => 50,
+ 'unit' => 'MB'
+ )
+ );
+
+ foreach ($phpSettings as $setting => $settingArray) {
+ $phpSettings[$setting]['value'] = $this->Server->getIniSetting($setting);
+ if ($phpSettings[$setting]['value'] && $settingArray['unit'] && $settingArray['unit'] === 'MB') {
+ // convert basic unit to M
+ $phpSettings[$setting]['value'] = (int) floor($phpSettings[$setting]['value'] / 1024 / 1024);
+ }
+ }
+ $this->set('phpSettings', $phpSettings);
+
+ if ($version && (!$version['upToDate'] || $version['upToDate'] == 'older')) {
+ $diagnostic_errors++;
+ }
+
+ // check if the STIX and Cybox libraries are working and the correct version using the test script stixtest.py
+ $stix = $this->Server->stixDiagnostics($diagnostic_errors, $stixVersion, $cyboxVersion, $mixboxVersion, $maecVersion, $stix2Version, $pymispVersion);
+
+ $yaraStatus = $this->Server->yaraDiagnostics($diagnostic_errors);
+
+ // if GnuPG is set up in the settings, try to encrypt a test message
+ $gpgStatus = $this->Server->gpgDiagnostics($diagnostic_errors);
+
+ // if the message queue pub/sub is enabled, check whether the extension works
+ $zmqStatus = $this->Server->zmqDiagnostics($diagnostic_errors);
+
+ // if Proxy is set up in the settings, try to connect to a test URL
+ $proxyStatus = $this->Server->proxyDiagnostics($diagnostic_errors);
+
+ // get the DB diagnostics
+ $dbDiagnostics = $this->Server->dbSpaceUsage();
+ $dbSchemaDiagnostics = $this->Server->dbSchemaDiagnostic();
+
+ $redisInfo = $this->Server->redisInfo();
+
+ $moduleTypes = array('Enrichment', 'Import', 'Export', 'Cortex');
+ foreach ($moduleTypes as $type) {
+ $moduleStatus[$type] = $this->Server->moduleDiagnostics($diagnostic_errors, $type);
+ }
+
+ // check the size of the session table
+ $sessionCount = 0;
+ $sessionStatus = $this->Server->sessionDiagnostics($diagnostic_errors, $sessionCount);
+ $this->set('sessionCount', $sessionCount);
+
+ $this->loadModel('AttachmentScan');
+ try {
+ $attachmentScan = ['status' => true, 'software' => $this->AttachmentScan->diagnostic()];
+ } catch (Exception $e) {
+ $attachmentScan = ['status' => false, 'error' => $e->getMessage()];
+ }
+
+ $securityAudit = (new SecurityAudit())->run($this->Server);
+
+ $view = compact('gpgStatus', 'sessionErrors', 'proxyStatus', 'sessionStatus', 'zmqStatus', 'stixVersion', 'cyboxVersion', 'mixboxVersion', 'maecVersion', 'stix2Version', 'pymispVersion', 'moduleStatus', 'yaraStatus', 'gpgErrors', 'proxyErrors', 'zmqErrors', 'stixOperational', 'stix', 'moduleErrors', 'moduleTypes', 'dbDiagnostics', 'dbSchemaDiagnostics', 'redisInfo', 'attachmentScan', 'securityAudit');
+ } else {
+ $view = [];
+ }
+
+ // check whether the files are writeable
+ $writeableDirs = $this->Server->writeableDirsDiagnostics($diagnostic_errors);
+ $writeableFiles = $this->Server->writeableFilesDiagnostics($diagnostic_errors);
+ $readableFiles = $this->Server->readableFilesDiagnostics($diagnostic_errors);
+ $extensions = $this->Server->extensionDiagnostics();
+
+ // check if the encoding is not set to utf8
+ $dbEncodingStatus = $this->Server->databaseEncodingDiagnostics($diagnostic_errors);
+
+ $view = array_merge($view, compact('diagnostic_errors', 'tabs', 'tab', 'issues', 'finalSettings', 'writeableErrors', 'readableErrors', 'writeableDirs', 'writeableFiles', 'readableFiles', 'extensions', 'dbEncodingStatus'));
+ $this->set($view);
+
+ $workerIssueCount = 4;
+ $worker_array = array();
+ if (Configure::read('MISP.background_jobs')) {
+ $workerIssueCount = 0;
+ $worker_array = $this->Server->workerDiagnostics($workerIssueCount);
+ }
+ $this->set('worker_array', $worker_array);
+ if ($tab === 'download' || $this->_isRest()) {
+ foreach ($dumpResults as $key => $dr) {
+ unset($dumpResults[$key]['description']);
+ }
+ $dump = array(
+ 'version' => $version,
+ 'phpSettings' => $phpSettings,
+ 'gpgStatus' => $gpgErrors[$gpgStatus['status']],
+ 'proxyStatus' => $proxyErrors[$proxyStatus],
+ 'zmqStatus' => $zmqStatus,
+ 'stix' => $stix,
+ 'moduleStatus' => $moduleStatus,
+ 'writeableDirs' => $writeableDirs,
+ 'writeableFiles' => $writeableFiles,
+ 'readableFiles' => $readableFiles,
+ 'dbDiagnostics' => $dbDiagnostics,
+ 'dbSchemaDiagnostics' => $dbSchemaDiagnostics,
+ 'redisInfo' => $redisInfo,
+ 'finalSettings' => $dumpResults,
+ 'extensions' => $extensions,
+ 'workers' => $worker_array
+ );
+ foreach ($dump['finalSettings'] as $k => $v) {
+ if (!empty($v['redacted'])) {
+ $dump['finalSettings'][$k]['value'] = '*****';
+ }
+ }
+ $this->response->body(json_encode($dump, JSON_PRETTY_PRINT));
+ $this->response->type('json');
+ $this->response->download('MISP.report.json');
+ return $this->response;
+ }
+
+ $priorities = array(0 => 'Critical', 1 => 'Recommended', 2 => 'Optional', 3 => 'Deprecated');
+ $this->set('priorities', $priorities);
+ $this->set('workerIssueCount', $workerIssueCount);
+ $priorityErrorColours = array(0 => 'red', 1 => 'yellow', 2 => 'green');
+ $this->set('priorityErrorColours', $priorityErrorColours);
+ $this->set('phpversion', phpversion());
+ $this->set('phpmin', $this->phpmin);
+ $this->set('phprec', $this->phprec);
+ $this->set('phptoonew', $this->phptoonew);
+ $this->set('pythonmin', $this->pythonmin);
+ $this->set('pythonrec', $this->pythonrec);
+ $this->set('pymisp', $this->pymisp);
}
public function startWorker($type)
@@ -1250,9 +1252,6 @@ class ServersController extends AppController
private function __checkVersion()
{
- if (!$this->_isSiteAdmin()) {
- throw new MethodNotAllowedException();
- }
App::uses('SyncTool', 'Tools');
$syncTool = new SyncTool();
try {
@@ -1365,10 +1364,8 @@ class ServersController extends AppController
$this->set('title_for_layout', __('Event ID translator'));
}
- public function getSubmodulesStatus() {
- if (!$this->_isSiteAdmin()) {
- throw new MethodNotAllowedException();
- }
+ public function getSubmodulesStatus()
+ {
$this->set('submodules', $this->Server->getSubmodulesGitStatus());
$this->render('ajax/submoduleStatus');
}
@@ -1387,9 +1384,6 @@ class ServersController extends AppController
public function serverSettingsEdit($setting_name, $id = false, $forceSave = false)
{
- if (!$this->_isSiteAdmin()) {
- throw new MethodNotAllowedException();
- }
if (!isset($setting_name)) {
throw new MethodNotAllowedException();
}
@@ -1521,15 +1515,6 @@ class ServersController extends AppController
$this->redirect(array('controller' => 'servers', 'action' => 'serverSettings', 'workers'));
}
- private function __manageFiles()
- {
- if (!$this->_isSiteAdmin()) {
- throw new MethodNotAllowedException();
- }
- $files = $this->Server->grabFiles();
- return $files;
- }
-
public function deleteFile($type, $filename)
{
if (!$this->_isSiteAdmin()) {
@@ -1703,23 +1688,17 @@ class ServersController extends AppController
$result['status'] = 8;
return new CakeResponse(array('body'=> json_encode($result), 'type' => 'json'));
}
- return new CakeResponse(
- array(
- 'body'=> json_encode(
- array(
- 'status' => 1,
- 'local_version' => implode('.', $local_version),
- 'version' => implode('.', $version),
- 'mismatch' => $mismatch,
- 'newer' => $newer,
- 'post' => isset($post) ? $post['status'] : 'too old',
- 'response_encoding' => isset($post['content-encoding']) ? $post['content-encoding'] : null,
- 'client_certificate' => $result['client_certificate'],
- )
- ),
- 'type' => 'json'
- )
- );
+ return $this->RestResponse->viewData([
+ 'status' => 1,
+ 'local_version' => implode('.', $local_version),
+ 'version' => implode('.', $version),
+ 'mismatch' => $mismatch,
+ 'newer' => $newer,
+ 'post' => isset($post) ? $post['status'] : 'too old',
+ 'response_encoding' => isset($post['content-encoding']) ? $post['content-encoding'] : null,
+ 'request_encoding' => isset($result['info']['request_encoding']) ? $result['info']['request_encoding'] : null,
+ 'client_certificate' => $result['client_certificate'],
+ ], 'json');
} else {
$result['status'] = 3;
}
@@ -1799,17 +1778,15 @@ class ServersController extends AppController
public function getVersion()
{
- if (!$this->userRole['perm_auth']) {
- throw new MethodNotAllowedException('This action requires API access.');
- }
$versionArray = $this->Server->checkMISPVersion();
- $this->set('response', array(
+ $response = [
'version' => $versionArray['major'] . '.' . $versionArray['minor'] . '.' . $versionArray['hotfix'],
'perm_sync' => $this->userRole['perm_sync'],
'perm_sighting' => $this->userRole['perm_sighting'],
'perm_galaxy_editor' => $this->userRole['perm_galaxy_editor'],
- ));
- $this->set('_serialize', 'response');
+ 'request_encoding' => $this->CompressedRequestHandler->supportedEncodings(),
+ ];
+ return $this->RestResponse->viewData($response, $this->response->type());
}
public function getPyMISPVersion()
diff --git a/app/Controller/TaxonomiesController.php b/app/Controller/TaxonomiesController.php
index 6b9be18d8..95f611b09 100644
--- a/app/Controller/TaxonomiesController.php
+++ b/app/Controller/TaxonomiesController.php
@@ -101,14 +101,15 @@ class TaxonomiesController extends AppController
$params = $customPagination->applyRulesOnArray($taxonomy['entries'], $params, 'taxonomies');
if ($this->_isRest()) {
return $this->RestResponse->viewData($taxonomy, $this->response->type());
- } else {
- $this->set('entries', $taxonomy['entries']);
- $this->set('urlparams', $urlparams);
- $this->set('passedArgs', json_encode($passedArgs));
- $this->set('passedArgsArray', $passedArgs);
- $this->set('taxonomy', $taxonomy['Taxonomy']);
- $this->set('id', $id);
}
+
+ $this->set('entries', $taxonomy['entries']);
+ $this->set('urlparams', $urlparams);
+ $this->set('passedArgs', json_encode($passedArgs));
+ $this->set('passedArgsArray', $passedArgs);
+ $this->set('taxonomy', $taxonomy['Taxonomy']);
+ $this->set('id', $id);
+ $this->set('title_for_layout', __('%s Taxonomy Library', h(strtoupper($taxonomy['Taxonomy']['namespace']))));
}
public function enable($id)
diff --git a/app/Lib/Tools/SecurityAudit.php b/app/Lib/Tools/SecurityAudit.php
new file mode 100644
index 000000000..f97aaeed7
--- /dev/null
+++ b/app/Lib/Tools/SecurityAudit.php
@@ -0,0 +1,505 @@
+config['password'];
+ if (empty($databasePassword)) {
+ $output['Database'][] = ['error', __('Database password not set.')];
+ } else if (strlen($databasePassword) < self::STRONG_PASSWORD_LENGTH) {
+ $output['Database'][] = ['warning', __('Database password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
+ }
+
+ $passwordPolicyLength = Configure::read('Security.password_policy_length') ?: $server->serverSettings['Security']['password_policy_length']['value'];
+ if ($passwordPolicyLength < 8) {
+ $output['Password'][] = ['error', __('Minimum password length is set to %s, that too short.', $passwordPolicyLength)];
+ } elseif ($passwordPolicyLength < 12) {
+ $output['Password'][] = ['warning', __('Minimum password length is set to %s, consider raising to at least 12 characters.', $passwordPolicyLength)];
+ }
+
+ if (empty(Configure::read('Security.require_password_confirmation'))) {
+ $output['Password'][] = [
+ 'warning',
+ __('Password confirmation is not enabled. %s', $server->serverSettings['Security']['require_password_confirmation']['description']),
+ ];
+ }
+ if (!empty(Configure::read('Security.auth')) && !Configure::read('Security.auth_enforced')) {
+ $output['Login'][] = [
+ 'hint',
+ __('External authentication is enabled, but local accounts will still work. You can disable logging with local accounts by setting `Security.auth_enforced` to `true`.'),
+ ];
+ }
+
+ if (empty(Configure::read('Security.disable_browser_cache'))) {
+ $output['Browser'][] = [
+ 'warning',
+ __('Browser cache is enabled. Attacker can obtain sensitive data from user cache. You can disable cache by setting `Security.disable_browser_cache` to `false`.'),
+ ];
+ }
+ if (empty(Configure::read('Security.check_sec_fetch_site_header'))) {
+ $output['Browser'][] = [
+ 'warning',
+ __('MISP server is not checking `Sec-Fetch` HTTP headers. This is protection against CSRF for moder browsers. You can enable this checks by setting `Security.check_sec_fetch_site_header` to `true`.'),
+ ];
+ }
+ if (Configure::read('Security.disable_form_security')) {
+ $output['Browser'][] = ['error', __('Disabling form security is never a good idea.')];
+ }
+
+ if (empty(Configure::read('Security.advanced_authkeys'))) {
+ $output['Auth Key'][] = ['warning', __('Consider enabling Advanced Auth Keys, that provides higher security.')];
+ }
+ if (Configure::read('Security.allow_unsafe_apikey_named_param')) {
+ $output['Auth Key'][] = ['error', __('It is possible to pass API key in URL, so the key can be logged by proxies.')];
+ }
+ if (empty(Configure::read('Security.do_not_log_authkeys'))) {
+ $output['Auth Key'][] = ['warning', __('Auth Key logging is not disabled. Auth Keys in cleartext can be visible in Audit log.')];
+ }
+
+ $salt = Configure::read('Security.salt');
+ if (empty($salt)) {
+ $output['Security salt'][] = ['error', __('Salt is not set.')];
+ } else if (strlen($salt) < 32) {
+ $output['Security salt'][] = ['warning', __('Salt is too short, should contains at least 32 characters.')];
+ } else if ($salt === "Rooraenietu8Eeyoemail($output);
+
+ if (!Configure::read('Security.hide_organisation_index_from_users')) {
+ $output['MISP'][] = [
+ 'hint',
+ __('Any user can see list of all organisations. You can disable that by setting `Security.hide_organisation_index_from_users` to `true`. %s', $server->serverSettings['Security']['hide_organisation_index_from_users']['description']),
+ ];
+ }
+ if (!Configure::read('Security.hide_organisations_in_sharing_groups')) {
+ $output['MISP'][] = [
+ 'hint',
+ __('Any user can see list of all organisations in sharing group that user can see. You can disable that by setting `Security.hide_organisations_in_sharing_groups` to `true`. %s', $server->serverSettings['Security']['hide_organisations_in_sharing_groups']['description']),
+ ];
+ }
+
+ $this->feeds($output);
+ $this->remoteServers($output);
+
+ try {
+ $cakeVersion = $this->getCakeVersion();
+ if (version_compare($cakeVersion, '2.10.21', '<')) {
+ $output['Dependencies'][] = ['warning', __('CakePHP version %s is outdated.', $cakeVersion)];
+ }
+ } catch (RuntimeException $e) {}
+
+ if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+ $output['PHP'][] = [
+ 'warning',
+ __('PHP version %s is not supported anymore. It can be still supported by your distribution. ', PHP_VERSION),
+ 'https://www.php.net/supported-versions.php'
+ ];
+ } else if (version_compare(PHP_VERSION, '7.4.0', '<')) {
+ $output['PHP'][] = [
+ 'hint',
+ __('PHP version 7.3 will be not supported after 6 Dec 2021. Event after that date can be still supported by your distribution.'),
+ 'https://www.php.net/supported-versions.php'
+ ];
+ }
+ if (ini_get('session.use_strict_mode') != 1) {
+ $output['PHP'][] = [
+ 'warning',
+ __('Session strict mode is disable.'),
+ 'https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode',
+ ];
+ }
+ if (empty(ini_get('session.cookie_httponly'))) {
+ $output['PHP'][] = ['error', __('Session cookie is not set as HTTP only. Session cookie can be access from JavaScript.')];
+ }
+ if (!in_array(strtolower(ini_get('session.cookie_samesite')), ['strict', 'lax'])) {
+ $output['PHP'][] = [
+ 'error',
+ __('Session cookie SameSite parameter is not defined or set to None.'),
+ 'https://developer.mozilla.org/en-us/docs/Web/HTTP/Headers/Set-Cookie/SameSite',
+ ];
+ }
+ $sidLength = ini_get('session.sid_length');
+ if ($sidLength !== false && $sidLength < 32) {
+ $output['PHP'][] = [
+ 'warning',
+ __('Session ID length is set to %s, at least 32 is recommended.', $sidLength),
+ 'https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length',
+ ];
+ }
+ $sidBits = ini_get('session.sid_bits_per_character');
+ if ($sidBits !== false && $sidBits <= 4) {
+ $output['PHP'][] = [
+ 'warning',
+ __('Session ID bit per character is set to %s, at least 5 is recommended.', $sidBits),
+ 'https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character',
+ ];
+ }
+
+ $this->system($server, $output);
+
+ return $output;
+ }
+
+ private function feeds(array &$output)
+ {
+ /** @var Feed $feed */
+ $feed = ClassRegistry::init('Feed');
+ $enabledFeeds = $feed->find('list', [
+ 'conditions' => [
+ 'input_source' => 'network',
+ 'OR' => [
+ 'enabled' => true,
+ 'caching_enabled' => true,
+ ]
+ ],
+ 'fields' => ['name', 'url'],
+ ]);
+ foreach ($enabledFeeds as $feedName => $feedUrl) {
+ if (substr($feedUrl, 0, strlen('http://')) === 'http://') {
+ $output['Feeds'][] = ['warning', __('Feed %s uses insecure (HTTP) connection.', $feedName)];
+ }
+ }
+ }
+
+ private function remoteServers(array &$output)
+ {
+ /** @var Server $server */
+ $server = ClassRegistry::init('Server');
+ $enabledServers = $server->find('all', [
+ 'conditions' => ['OR' => [
+ 'push' => true,
+ 'pull' => true,
+ 'push_sightings' => true,
+ 'caching_enabled' => true,
+ ]],
+ 'fields' => ['id', 'name', 'url', 'self_signed', 'cert_file', 'client_cert_file'],
+ ]);
+ foreach ($enabledServers as $enabledServer) {
+ if (substr($enabledServer['Server']['url'], 0, strlen('http://')) === 'http://') {
+ $output['Remote servers'][] = ['warning', __('Server %s uses insecure (HTTP) connection.', $enabledServer['Server']['name'])];
+ } else if ($enabledServer['Server']['self_signed']) {
+ $output['Remote servers'][] = ['warning', __('Server %s uses self signed certificate. This is considered insecure.', $enabledServer['Server']['name'])];
+ }
+
+ try {
+ $parsed = SyncTool::getServerClientCertificateInfo($enabledServer);
+ if (isset($parsed['public_key_size_ok']) && !$parsed['public_key_size_ok']) {
+ $algo = $parsed['public_key_type'] . " " . $parsed['public_key_size'];
+ $output['Remote servers'][] = ['warning', __('Server %s uses weak client certificate (%s).', $enabledServer['Server']['name'], $algo)];
+ }
+ } catch (Exception $e) {}
+
+ try {
+ $parsed = SyncTool::getServerCaCertificateInfo($enabledServer);
+ if (isset($parsed['public_key_size_ok']) && !$parsed['public_key_size_ok']) {
+ $algo = $parsed['public_key_type'] . " " . $parsed['public_key_size'];
+ $output['Remote servers'][] = ['warning', __('Server %s uses weak CA certificate (%s).', $enabledServer['Server']['name'], $algo)];
+ }
+ } catch (Exception $e) {}
+ }
+ }
+
+ private function email(array &$output)
+ {
+ $canSignPgp = Configure::read('GnuPG.sign');
+ $canSignSmime = Configure::read('SMIME.enabled') &&
+ !empty(Configure::read('SMIME.cert_public_sign')) &&
+ !empty(Configure::read('SMIME.key_sign'));
+
+ if (!$canSignPgp && !$canSignSmime) {
+ $output['Email'][] = [
+ 'warning',
+ __('Email signing (PGP or S/MIME) is not enabled.')
+ ];
+ }
+
+ if ($canSignPgp) {
+ $gpgKeyPassword = Configure::read('GnuPG.password');
+ if (empty($gpgKeyPassword)) {
+ $output['Email'][] = ['error', __('PGP private key password is empty.')];
+ } else if (strlen($gpgKeyPassword) < self::STRONG_PASSWORD_LENGTH) {
+ $output['Email'][] = ['warning', __('PGP private key password is too short, should be at least %s chars long.', self::STRONG_PASSWORD_LENGTH)];
+ }
+ }
+
+ if (!Configure::read('GnuPG.bodyonlyencrypted')) {
+ $output['Email'][] = [
+ 'hint',
+ __('Full email body with all event information will be send even without encryption.')
+ ];
+ }
+
+ if ($canSignPgp && !Configure::read('GnuPG.obscure_subject')) {
+ $output['Email'][] = [
+ 'hint',
+ __('Even for encrypted emails, email subject will be send unencrypted. You can change that behaviour by setting `GnuPG.obscure_subject` to `true`.'),
+ ];
+ }
+
+ App::uses('CakeEmail', 'Network/Email');
+ $email = new CakeEmail();
+ $emailConfig = $email->config();
+ if ($emailConfig['transport'] === 'Smtp' && $emailConfig['port'] == 25 && !$emailConfig['tls']) {
+ $output['Email'][] = [
+ 'warning',
+ __('STARTTLS is not enabled.'),
+ 'https://en.wikipedia.org/wiki/Opportunistic_TLS',
+ ];
+ }
+ }
+
+ private function system(Server $server, array &$output)
+ {
+ $kernelBuildTime = $this->getKernelBuild();
+ if ($kernelBuildTime) {
+ $diff = (new DateTime())->diff($kernelBuildTime);
+ $diffDays = $diff->format('a');
+ if ($diffDays > 300) {
+ $output['System'][] = [
+ 'warning',
+ __('Kernel build time is since %s days ago. This usually means that the system kernel is not updated.', $diffDays),
+ ];
+ }
+ }
+
+ // uptime
+ try {
+ $since = $this->execute(['uptime', '-s']);
+ $since = new DateTime($since);
+ $diff = (new DateTime())->diff($since);
+ $diffDays = $diff->format('a');
+ if ($diffDays > 100) {
+ $output['System'][] = [
+ 'warning',
+ __('Uptime of this server is %s days. This usually means that the system kernel is outdated.', $diffDays),
+ ];
+ }
+ } catch (Exception $e) {
+ }
+
+ // Python version
+ try {
+ $pythonVersion = $this->execute([$server->getPythonVersion(), '-V']);
+ $parts = explode(' ', $pythonVersion);
+ if ($parts[0] !== 'Python') {
+ throw new Exception("Invalid python version response: $pythonVersion");
+ }
+
+ if (version_compare($parts[1], '3.6', '<')) {
+ $output['System'][] = [
+ 'warning',
+ __('You are using Python %s. This version is not supported anymore, but it can be still supported by your distribution.'),
+ 'https://endoflife.date/python',
+ ];
+ } else if (version_compare($parts[1], '3.7', '<')) {
+ $output['System'][] = [
+ 'hint',
+ __('You are using Python %s. This version will be not supported after 23 Dec 2021, but it can be still supported by your distribution.'),
+ 'https://endoflife.date/python',
+ ];
+ }
+ } catch (Exception $e) {
+ }
+
+ $ubuntuVersion = $this->getUbuntuVersion();
+ if ($ubuntuVersion) {
+ if (in_array($ubuntuVersion, ['14.04', '19.10'])) {
+ $output['System'][] = [
+ 'warning',
+ __('You are using Ubuntu %s. This version doesn\'t have security support anymore.', $ubuntuVersion),
+ 'https://endoflife.date/ubuntu',
+ ];
+ } else if (in_array($ubuntuVersion, ['16.04'])) {
+ $output['System'][] = [
+ 'hint',
+ __('You are using Ubuntu %s. This version will be not supported after 02 Apr 2021.', $ubuntuVersion),
+ 'https://endoflife.date/ubuntu',
+ ];
+ }
+ }
+ }
+
+ private function getKernelBuild()
+ {
+ if (!php_uname('s') !== 'Linux') {
+ return false;
+ }
+
+ $version = php_uname('v');
+ if (substr($version, 0, 7) !== '#1 SMP ') {
+ return false;
+ }
+ $buildTime = substr($version, 7);
+ return new DateTime($buildTime);
+ }
+
+ private function getUbuntuVersion()
+ {
+ if (!php_uname('s') !== 'Linux') {
+ return false;
+ }
+ if (!is_readable('/etc/os-release')) {
+ return false;
+ }
+ $content = file_get_contents('/etc/os-release');
+ if ($content === false) {
+ return false;
+ }
+ $parsed = parse_ini_string($content);
+ if ($parsed === false) {
+ return false;
+ }
+ if (!isset($parsed['NAME'])) {
+ return false;
+ }
+ if ($parsed['NAME'] !== 'Ubuntu') {
+ return false;
+ }
+ if (!isset($parsed['VERSION_ID'])) {
+ return false;
+ }
+ return $parsed['VERSION_ID'];
+ }
+
+ /**
+ * @return string
+ */
+ private function getCakeVersion()
+ {
+ $filePath = APP . 'Lib/cakephp/lib/Cake/VERSION.txt';
+ $version = file_get_contents($filePath);
+ if (!$version) {
+ throw new RuntimeException("Could not open CakePHP version file '$filePath'.");
+ }
+ foreach (explode("\n", $version) as $line) {
+ if ($line[0] === '/') {
+ continue;
+ }
+ return trim($line);
+ }
+ throw new RuntimeException("CakePHP version not found in file '$filePath'.");
+ }
+
+ private function execute(array $command)
+ {
+ $descriptorspec = [
+ 1 => ["pipe", "w"], // stdout
+ 2 => ["pipe", "w"], // stderr
+ ];
+
+ $command = implode(' ', $command);
+ $process = proc_open($command, $descriptorspec, $pipes);
+ if (!$process) {
+ throw new Exception("Command '$command' could be started.");
+ }
+
+ $stdout = stream_get_contents($pipes[1]);
+ if ($stdout === false) {
+ throw new Exception("Could not get STDOUT of command.");
+ }
+ fclose($pipes[1]);
+
+ $stderr = stream_get_contents($pipes[2]);
+ fclose($pipes[2]);
+
+ $returnCode = proc_close($process);
+ if ($returnCode !== 0) {
+ throw new Exception("Command '$command' return error code $returnCode. STDERR: '$stderr', STDOUT: '$stdout'");
+ }
+
+ return $stdout;
+ }
+}
diff --git a/app/Lib/Tools/SyncTool.php b/app/Lib/Tools/SyncTool.php
index 4f3d87974..b59a192b6 100644
--- a/app/Lib/Tools/SyncTool.php
+++ b/app/Lib/Tools/SyncTool.php
@@ -92,6 +92,35 @@ class SyncTool
return self::getClientCertificateInfo($certificateContent);
}
+ /**
+ * @param array $server
+ * @return array|void
+ * @throws Exception
+ */
+ public static function getServerCaCertificateInfo(array $server)
+ {
+ if (!$server['Server']['cert_file']) {
+ return;
+ }
+
+ $caCertificate = new File(APP . "files" . DS . "certs" . DS . $server['Server']['id'] . '.pem');
+ if (!$caCertificate->exists()) {
+ throw new Exception("Certificate file '{$caCertificate->pwd()}' doesn't exists.");
+ }
+
+ $certificateContent = $caCertificate->read();
+ if ($certificateContent === false) {
+ throw new Exception("Could not read '{$caCertificate->pwd()}' file with certificate.");
+ }
+
+ $certificate = openssl_x509_read($certificateContent);
+ if (!$certificate) {
+ throw new Exception("Couldn't read certificate: " . openssl_error_string());
+ }
+
+ return self::parseCertificate($certificate);
+ }
+
/**
* @param string $certificateContent PEM encoded certificate and private key.
* @return array
@@ -101,11 +130,11 @@ class SyncTool
{
$certificate = openssl_x509_read($certificateContent);
if (!$certificate) {
- throw new Exception("Could't parse certificate: " . openssl_error_string());
+ throw new Exception("Couldn't read certificate: " . openssl_error_string());
}
$privateKey = openssl_pkey_get_private($certificateContent);
if (!$privateKey) {
- throw new Exception("Could't get private key from certificate: " . openssl_error_string());
+ throw new Exception("Couldn't get private key from certificate: " . openssl_error_string());
}
$verify = openssl_x509_check_private_key($certificate, $privateKey);
if (!$verify) {
@@ -123,7 +152,7 @@ class SyncTool
{
$parsed = openssl_x509_parse($certificate);
if (!$parsed) {
- throw new Exception("Could't get parse X.509 certificate: " . openssl_error_string());
+ throw new Exception("Couldn't get parse X.509 certificate: " . openssl_error_string());
}
$currentTime = new DateTime();
$output = [
diff --git a/app/Model/Attribute.php b/app/Model/Attribute.php
index a94a38fd9..7c9076944 100644
--- a/app/Model/Attribute.php
+++ b/app/Model/Attribute.php
@@ -4073,6 +4073,10 @@ class Attribute extends AppModel
$attribute['distribution'] = 5;
}
}
+ if (isset($attribute['Sighting']) && !empty($attribute['Sighting'])) {
+ $this->Sighting = ClassRegistry::init('Sighting');
+ $this->Sighting->captureSightings($attribute['Sighting'], $attribute['id'], $eventId, $user);
+ }
$fieldList = $this->editableFields;
if (empty($existingAttribute)) {
$addableFieldList = array('event_id', 'type', 'uuid');
diff --git a/app/Model/Event.php b/app/Model/Event.php
index e728ae28e..296ceec5d 100755
--- a/app/Model/Event.php
+++ b/app/Model/Event.php
@@ -3640,9 +3640,6 @@ class Event extends AppModel
// Low level function to add an Event based on an Event $data array
public function _add(array &$data, $fromXml, array $user, $org_id = 0, $passAlong = null, $fromPull = false, $jobId = null, &$created_id = 0, &$validationErrors = array())
{
- if ($jobId) {
- App::uses('AuthComponent', 'Controller/Component');
- }
if (Configure::read('MISP.enableEventBlocklisting') !== false && isset($data['Event']['uuid'])) {
$this->EventBlocklist = ClassRegistry::init('EventBlocklist');
if ($this->EventBlocklist->isBlocked($data['Event']['uuid'])) {
@@ -3821,6 +3818,12 @@ class Event extends AppModel
);
$saveResult = $this->save(array('Event' => $data['Event']), array('fieldList' => $fieldList['Event']));
if ($saveResult) {
+ if ($jobId) {
+ /** @var EventLock $eventLock */
+ $eventLock = ClassRegistry::init('EventLock');
+ $eventLock->insertLockBackgroundJob($this->id, $jobId);
+ }
+
if ($passAlong) {
if ($server['Server']['publish_without_email'] == 0) {
$st = "enabled";
@@ -3950,6 +3953,10 @@ class Event extends AppModel
}
}
}
+ if ($jobId) {
+ $eventLock->deleteBackgroundJobLock($this->id, $jobId);
+ }
+
return true;
} else {
$validationErrors['Event'] = $this->validationErrors;
@@ -4074,6 +4081,11 @@ class Event extends AppModel
$saveResult = $this->save(array('Event' => $data['Event']), array('fieldList' => $fieldList));
$this->Log = ClassRegistry::init('Log');
if ($saveResult) {
+ if ($jobId) {
+ /** @var EventLock $eventLock */
+ $eventLock = ClassRegistry::init('EventLock');
+ $eventLock->insertLockBackgroundJob($data['Event']['id'], $jobId);
+ }
$validationErrors = array();
if (isset($data['Event']['Attribute'])) {
$data['Event']['Attribute'] = array_values($data['Event']['Attribute']);
@@ -4201,6 +4213,9 @@ class Event extends AppModel
}
$this->publish($existingEvent['Event']['id']);
}
+ if ($jobId) {
+ $eventLock->deleteBackgroundJobLock($data['Event']['id'], $jobId);
+ }
return true;
}
return $this->validationErrors;
@@ -4830,32 +4845,25 @@ class Event extends AppModel
return false;
}
- public function removeOlder(&$eventArray, $scope = 'events')
+ public function removeOlder(array &$events, $scope = 'events')
{
- if ($scope === 'sightings' ) {
- $field = 'sighting_timestamp';
- } else {
- $field = 'timestamp';
- }
- $uuidsToCheck = array();
- foreach ($eventArray as $k => &$event) {
- $uuidsToCheck[$event['uuid']] = $k;
- }
- $localEvents = array();
- $temp = $this->find('all', array('recursive' => -1, 'fields' => array('Event.uuid', 'Event.' . $field, 'Event.locked')));
- foreach ($temp as $e) {
- $localEvents[$e['Event']['uuid']] = array($field => $e['Event'][$field], 'locked' => $e['Event']['locked']);
- }
- foreach ($uuidsToCheck as $uuid => $eventArrayId) {
+ $field = $scope === 'sightings' ? 'sighting_timestamp' : 'timestamp';
+ $localEvents = $this->find('all', [
+ 'recursive' => -1,
+ 'fields' => ['Event.uuid', 'Event.' . $field, 'Event.locked'],
+ ]);
+ $localEvents = array_column(array_column($localEvents, 'Event'), null, 'uuid');
+ foreach ($events as $k => $event) {
// remove all events for the sighting sync if the remote is not aware of the new field yet
- if (!isset($eventArray[$eventArrayId][$field])) {
- unset($eventArray[$eventArrayId]);
+ if (!isset($event[$field])) {
+ unset($events[$k]);
} else {
+ $uuid = $event['uuid'];
if (isset($localEvents[$uuid])
- && ($localEvents[$uuid][$field] >= $eventArray[$eventArrayId][$field]
+ && ($localEvents[$uuid][$field] >= $event[$field]
|| ($scope === 'events' && !$localEvents[$uuid]['locked'])))
{
- unset($eventArray[$eventArrayId]);
+ unset($events[$k]);
}
}
}
@@ -6122,6 +6130,10 @@ class Event extends AppModel
$ontheflyattributes = array();
$i = 0;
if ($jobId) {
+ /** @var EventLock $eventLock */
+ $eventLock = ClassRegistry::init('EventLock');
+ $eventLock->insertLockBackgroundJob($event['Event']['id'], $jobId);
+
$this->Job = ClassRegistry::init('Job');
$total = count($attributeSources);
}
@@ -6229,6 +6241,7 @@ class Event extends AppModel
$message = $saved . ' ' . $messageScopeSaved . ' created' . $emailResult . '.';
}
if ($jobId) {
+ $eventLock->deleteBackgroundJobLock($event['Event']['id'], $jobId);
$this->Job->saveStatus($jobId, true, __('Processing complete. %s', $message));
}
if (!empty($returnRawResults)) {
diff --git a/app/Model/EventLock.php b/app/Model/EventLock.php
index a0fd2807a..790c2b801 100644
--- a/app/Model/EventLock.php
+++ b/app/Model/EventLock.php
@@ -1,65 +1,132 @@
array(
- 'className' => 'User',
- 'foreignKey' => 'user_id',
- )
- );
-
-
- public $validate = array(
- );
-
- public function beforeValidate($options = array())
+ /**
+ * @param array $user
+ * @param int $eventId
+ * @return bool True if insert was successful.
+ */
+ public function insertLock(array $user, $eventId)
{
- parent::beforeValidate();
- return true;
+ return $this->insertLockToRedis("misp:event_lock:$eventId:user:{$user['id']}", [
+ 'type' => 'user',
+ 'timestamp' => time(),
+ 'User' => [
+ 'id' => $user['id'],
+ 'org_id' => $user['org_id'],
+ 'email' => $user['email'],
+ ]
+ ]);
}
- public function insertLock($user, $eventId)
+ /**
+ * @param int $eventId
+ * @param int $jobId
+ * @return bool True if insert was successful.
+ */
+ public function insertLockBackgroundJob($eventId, $jobId)
{
- $date = new DateTime();
- $lock = array(
- 'timestamp' => $date->getTimestamp(),
+ return $this->insertLockToRedis("misp:event_lock:$eventId:job:$jobId", [
+ 'type' => 'job',
+ 'timestamp' => time(),
+ 'job_id' => $jobId,
+ ]);
+ }
+
+ /**
+ * @param int $eventId
+ * @return int|null Lock ID
+ */
+ public function insertLockApi($eventId, array $user)
+ {
+ $rand = mt_rand();
+ if ($this->insertLockToRedis("misp:event_lock:$eventId:api:{$user['id']}:$rand", [
+ 'type' => 'api',
'user_id' => $user['id'],
- 'event_id' => $eventId
- );
- $this->deleteAll(array('user_id' => $user['id']));
- $this->create();
- return $this->save($lock);
+ 'timestamp' => time(),
+ ])) {
+ return $rand;
+ }
+ return null;
}
- public function checkLock($user, $eventId)
+ /**
+ * @param int $eventId
+ * @param int $jobId
+ * @return null
+ */
+ public function deleteBackgroundJobLock($eventId, $jobId)
{
- $this->cleanupLock($user, $eventId);
- $locks = $this->find('all', array(
- 'recursive' => -1,
- 'contain' => array('User.email', 'User.org_id', 'User.id'),
- 'conditions' => array(
- 'event_id' => $eventId
- )
- ));
- return $locks;
+ try {
+ $redis = $this->setupRedisWithException();
+ } catch (Exception $e) {
+ return false;
+ }
+
+ $deleted = $redis->del("misp:event_lock:$eventId:job:$jobId");
+ return $deleted > 0;
}
- // If a lock has been active for 15 minutes, delete it
- public function cleanupLock()
+ /**
+ * @param string $key
+ * @param array $data
+ * @return bool
+ */
+ private function insertLockToRedis($key, array $data)
{
- $date = new DateTime();
- $timestamp = $date->getTimestamp();
- $timestamp -= 900;
- $this->deleteAll(array('timestamp <' => $timestamp));
- return true;
+ try {
+ $redis = $this->setupRedisWithException();
+ } catch (Exception $e) {
+ return false;
+ }
+
+ return $redis->setex($key, self::DEFAULT_TTL, json_encode($data));
+ }
+
+ /**
+ * @param int $eventId
+ * @param int $lockId
+ * @return bool
+ */
+ public function deleteApiLock($eventId, $lockId, array $user)
+ {
+ try {
+ $redis = $this->setupRedisWithException();
+ } catch (Exception $e) {
+ return false;
+ }
+
+ $deleted = $redis->del("misp:event_lock:$eventId:api:{$user['id']}:$lockId");
+ return $deleted > 0;
+ }
+
+ /**
+ * @param array $user
+ * @param int $eventId
+ * @return array[]
+ * @throws JsonException
+ */
+ public function checkLock(array $user, $eventId)
+ {
+ try {
+ $redis = $this->setupRedisWithException();
+ } catch (Exception $e) {
+ return [];
+ }
+
+ $keys = $redis->keys("misp:event_lock:$eventId:*");
+ if (empty($keys)) {
+ return [];
+ }
+
+ return array_map(function ($value) {
+ return $this->jsonDecode($value);
+ }, $redis->mget($keys));
}
}
diff --git a/app/Model/Feed.php b/app/Model/Feed.php
index 8bbfb36a2..4d9cdb8af 100644
--- a/app/Model/Feed.php
+++ b/app/Model/Feed.php
@@ -997,7 +997,7 @@ class Feed extends AppModel
* @return bool
* @throws Exception
*/
- public function saveFreetextFeedData($feed, $data, $user, $jobId = false)
+ public function saveFreetextFeedData(array $feed, array $data, array $user, $jobId = false)
{
$this->Event = ClassRegistry::init('Event');
@@ -1040,25 +1040,14 @@ class Feed extends AppModel
}
}
if ($feed['Feed']['fixed_event']) {
- $existsAttributes = $this->Event->Attribute->find('all', array(
+ $existsAttributesValueToId = $this->Event->Attribute->find('list', array(
'conditions' => array(
'Attribute.deleted' => 0,
'Attribute.event_id' => $event['Event']['id']
),
'recursive' => -1,
- 'fields' => array('id', 'value1', 'value2')
+ 'fields' => array('value', 'id')
));
- $existsAttributesValueToId = array();
- foreach ($existsAttributes as $t) {
- if (!empty($t['Attribute']['value2'])) {
- $value = $t['Attribute']['value1'] . '|' . $t['Attribute']['value2'];
- } else {
- $value = $t['Attribute']['value1'];
- }
- // Since event values are unique, it is OK to put value into key
- $existsAttributesValueToId[$value] = $t['Attribute']['id'];
- }
- unset($existsAttributes);
// Create event diff. After this cycle, `$data` will contains just attributes that do not exists in current
// event and in `$existsAttributesValueToId` will contains just attributes that do not exists in current feed.
@@ -1098,7 +1087,6 @@ class Feed extends AppModel
return true;
}
- $data = array_values($data);
$uniqueValues = array();
foreach ($data as $key => $value) {
if (isset($uniqueValues[$value['value']])) {
diff --git a/app/Model/Post.php b/app/Model/Post.php
index ea3315a57..aba8994c5 100644
--- a/app/Model/Post.php
+++ b/app/Model/Post.php
@@ -1,8 +1,10 @@
User->findById($user_id);
@@ -38,7 +40,7 @@ class Post extends AppModel
'status' => 0,
'retries' => 0,
'org_id' => $user['User']['org_id'],
- 'message' => 'Sending..',
+ 'message' => 'Sending...',
);
$job->save($data);
$jobId = $job->id;
@@ -51,50 +53,90 @@ class Post extends AppModel
$job->saveField('process_id', $process_id);
return true;
} else {
- $result = $this->sendPostsEmail($user_id, $post_id, $event_id, $title, $message);
- return $result;
+ return $this->sendPostsEmail($user_id, $post_id, $event_id, $title, $message);
}
}
- public function sendPostsEmail($user_id, $post_id, $event_id, $title, $message)
+ /**
+ * @param int $userId
+ * @param int $postId
+ * @param int|null $eventId
+ * @param string $title
+ * @param string $message
+ * @return bool
+ * @throws Exception
+ */
+ public function sendPostsEmail($userId, $postId, $eventId, $title, $message)
{
- // fetch the post
- $post = $this->read(null, $post_id);
- $this->User = ClassRegistry::init('User');
+ $post = $this->find('first', [
+ 'recursive' => -1,
+ 'conditions' => ['id' => $postId],
+ 'fields' => ['id', 'thread_id'],
+ ]);
+ if (empty($post)) {
+ throw new Exception("Post with ID $postId not found.");
+ }
+
+ $userFields = ['id', 'email', 'gpgkey', 'certif_public', 'disabled'];
// If the post belongs to an event, E-mail all users in the org that have contactalert set
- if ($event_id) {
+ if ($eventId) {
$this->Event = ClassRegistry::init('Event');
- ;
- $event = $this->Event->read(null, $event_id);
- //Insert extra field here: alertOrg or something, then foreach all the org members
- //limit this array to users with contactalerts turned on!
- $orgMembers = array();
- $this->User->recursive = -1;
- $temp = $this->User->findAllByOrgId($event['Event']['org_id'], array('email', 'gpgkey', 'certif_public', 'contactalert', 'id'));
- foreach ($temp as $tempElement) {
- if ($tempElement['User']['id'] != $user_id && ($tempElement['User']['contactalert'] || $tempElement['User']['id'] == $event['Event']['user_id'])) {
- array_push($orgMembers, $tempElement);
- }
+ $event = $this->Event->find('first', [
+ 'recursive' => -1,
+ 'conditions' => ['id' => $eventId],
+ 'fields' => ['id', 'org_id', 'user_id'],
+ ]);
+ if (empty($event)) {
+ throw new Exception("Event with ID $eventId not found.");
}
+ // Insert extra field here: alertOrg or something, then foreach all the org members
+ // limit this array to users with contactalerts turned on!
+ $orgMembers = $this->User->find('all', [
+ 'recursive' => -1,
+ 'conditions' => [
+ 'org_id' => $event['Event']['org_id'],
+ 'disabled' => 0,
+ 'NOT' => ['id' => $userId], // do not send to post creator
+ 'OR' => [ // send just to users with contactalert or to event creator
+ 'contactalert' => 1,
+ 'id' => $event['Event']['user_id'],
+ ],
+ ],
+ 'fields' => $userFields,
+ ]);
} else {
// Not an event: E-mail the user that started the thread
$thread = $this->Thread->read(null, $post['Post']['thread_id']);
- if ($thread['Thread']['user_id'] == $user_id) {
+ if ($thread['Thread']['user_id'] == $userId) {
$orgMembers = array();
} else {
- $orgMembers = $this->User->findAllById($thread['Thread']['user_id'], array('email', 'gpgkey', 'certif_public', 'contactalert', 'id'));
+ $orgMembers = $this->User->find('all', [
+ 'recursive' => -1,
+ 'fields' => $userFields,
+ 'conditions' => [
+ 'id' => $thread['Thread']['user_id'],
+ 'disabled' => 0,
+ ]
+ ]);
}
}
// Add all users who posted in this thread
- $temp = $this->findAllByThreadId($post['Post']['thread_id'], array('user_id'));
- foreach ($temp as $tempElement) {
- $user = $this->User->findById($tempElement['Post']['user_id'], array('email', 'gpgkey', 'certif_public', 'contactalert', 'id'));
- if (!empty($user) && $user['User']['id'] != $user_id && !in_array($user, $orgMembers)) {
- array_push($orgMembers, $user);
- }
- }
+ $excludeUsers = Hash::extract($orgMembers, '{n}.User.id');
+ $excludeUsers[] = $userId;
+ $temp = $this->find('all', [
+ 'recursive' => -1,
+ 'fields' => ['Post.id'],
+ 'conditions' => [
+ 'Post.thread_id' => $post['Post']['thread_id'],
+ 'User.disabled' => 0,
+ 'NOT' => ['User.id' => $excludeUsers]
+ ],
+ 'contain' => ['User' => ['fields' => $userFields]],
+ 'group' => ['User.id'], // remove duplicates
+ ]);
+ $orgMembers = array_merge($orgMembers, $temp);
// The mail body, h() is NOT needed as we are sending plain-text mails.
$body = "";
@@ -118,11 +160,14 @@ class Post extends AppModel
$bodyDetail .= "The following message was added: \n";
$bodyDetail .= "\n";
$bodyDetail .= $message . "\n";
+
$tplColorString = Configure::read('MISP.email_subject_TLP_string') ?: "tlp:amber";
$subject = "[" . Configure::read('MISP.org') . " MISP] New post in discussion " . $post['Post']['thread_id'] . " - " . strtoupper($tplColorString);
foreach ($orgMembers as $recipient) {
$this->User->sendEmail($recipient, $bodyDetail, $body, $subject);
}
+
+ return true;
}
public function findPageNr($id, $context = 'thread', $post_id = false)
diff --git a/app/Model/Server.php b/app/Model/Server.php
index d868cc129..0f0cde2ad 100644
--- a/app/Model/Server.php
+++ b/app/Model/Server.php
@@ -3767,17 +3767,14 @@ class Server extends AppModel
return true;
}
- public function getLatestGitRemote()
- {
- return exec('timeout 3 git ls-remote https://github.com/MISP/MISP | head -1 | sed "s/HEAD//"');
- }
-
public function getCurrentGitStatus()
{
+ $latestCommit = exec('timeout 3 git ls-remote https://github.com/MISP/MISP | head -1 | sed "s/HEAD//"');
+
$status = array();
$status['commit'] = exec('git rev-parse HEAD');
$status['branch'] = $this->getCurrentBranch();
- $status['latestCommit'] = $this->getLatestGitremote();
+ $status['latestCommit'] = $latestCommit;
return $status;
}
diff --git a/app/Model/ServerTag.php b/app/Model/ServerTag.php
deleted file mode 100644
index 06e001647..000000000
--- a/app/Model/ServerTag.php
+++ /dev/null
@@ -1,43 +0,0 @@
- array(
- 'valueNotEmpty' => array(
- 'rule' => array('valueNotEmpty'),
- ),
- ),
- 'tag_id' => array(
- 'valueNotEmpty' => array(
- 'rule' => array('valueNotEmpty'),
- ),
- ),
- );
-
- public $belongsTo = array(
- 'Server',
- 'Tag'
- );
-
- public function attachTagToServer($server_id, $tag_id)
- {
- $existingAssociation = $this->find('first', array(
- 'recursive' => -1,
- 'conditions' => array(
- 'tag_id' => $tag_id,
- 'server_id' => $server_id
- )
- ));
- if (empty($existingAssociation)) {
- $this->create();
- if (!$this->save(array('server_id' => $server_id, 'tag_id' => $tag_id))) {
- return false;
- }
- }
- return true;
- }
-}
diff --git a/app/Model/Sighting.php b/app/Model/Sighting.php
index 03c833c55..534cbda92 100644
--- a/app/Model/Sighting.php
+++ b/app/Model/Sighting.php
@@ -154,6 +154,8 @@ class Sighting extends AppModel
} else {
$orgId = $this->Organisation->captureOrg($sighting['Organisation'], $user);
}
+ } else if (isset($user['org_id'])) {
+ $orgId = $user['org_id'];
}
unset($sighting['id']);
diff --git a/app/Model/User.php b/app/Model/User.php
index 4395d88c0..e8f70527d 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -7,6 +7,8 @@ App::uses('SendEmail', 'Tools');
/**
* @property Log $Log
+ * @property Organisation $Organisation
+ * @property Role $Role
*/
class User extends AppModel
{
diff --git a/app/Plugin/OidcAuth/Controller/Component/Auth/OidcAuthenticate.php b/app/Plugin/OidcAuth/Controller/Component/Auth/OidcAuthenticate.php
new file mode 100644
index 000000000..2b8cb2cb4
--- /dev/null
+++ b/app/Plugin/OidcAuth/Controller/Component/Auth/OidcAuthenticate.php
@@ -0,0 +1,244 @@
+prepareClient();
+
+ if (!$oidc->authenticate()) {
+ throw new Exception("OIDC authentication was not successful.");
+ }
+
+ $mispUsername = $oidc->requestUserInfo('email');
+ $this->log($mispUsername, "Trying login");
+
+ $verifiedClaims = $oidc->getVerifiedClaims();
+ $organisationProperty = $this->getConfig('organisation_property', 'organization');
+ if (property_exists($verifiedClaims, $organisationProperty)) {
+ $organisationName = $verifiedClaims->{$organisationProperty};
+ } else {
+ $organisationName = $this->getConfig('default_org');
+ }
+
+ $roles = [];
+ $roleProperty = $this->getConfig('roles_property', 'roles');
+ if (property_exists($verifiedClaims, $roleProperty)) {
+ $roles = $verifiedClaims->{$roleProperty};
+ }
+
+ $this->settings['fields'] = ['username' => 'email'];
+ $user = $this->_findUser($mispUsername);
+
+ $organisationId = $this->checkOrganization($organisationName, $user, $mispUsername);
+ if (!$organisationId) {
+ return false;
+ }
+
+ $roleId = $this->getUserRole($roles, $mispUsername);
+ if ($roleId === null) {
+ $this->log($mispUsername, 'No role was assigned.');
+ return false;
+ }
+
+ if ($user) {
+ $this->log($mispUsername, 'Found in database.');
+
+ if ($user['org_id'] != $organisationId) {
+ $user['org_id'] = $organisationId;
+ $this->userModel()->updateField($user, 'org_id', $organisationId);
+ $this->log($mispUsername, "User organisation changed from ${user['org_id']} to $organisationId.");
+ }
+
+ if ($user['role_id'] != $roleId) {
+ $user['role_id'] = $roleId;
+ $this->userModel()->updateField($user, 'role_id', $roleId);
+ $this->log($mispUsername, "User role changed from ${user['role_id']} to $roleId.");
+ }
+
+ $this->log($mispUsername, 'Logged in.');
+ return $user;
+ }
+
+ $this->log($mispUsername, 'Not found in database.');
+
+ $userData = [
+ 'email' => $mispUsername,
+ 'org_id' => $organisationId,
+ 'newsread' => time(),
+ 'role_id' => $roleId,
+ 'change_pw' => 0,
+ 'date_created' => time(),
+ ];
+
+ if (!$this->userModel()->save($userData)) {
+ throw new RuntimeException("Could not save user `$mispUsername` to database.");
+ }
+
+ $this->log($mispUsername, "Saved in database with ID {$this->userModel()->id}");
+ $this->log($mispUsername, 'Logged in.');
+ return $this->_findUser($mispUsername);
+ }
+
+ /**
+ * @return OpenIDConnectClient
+ */
+ private function prepareClient()
+ {
+ $providerUrl = $this->getConfig('provider_url');
+ if (!filter_var($providerUrl, FILTER_VALIDATE_URL)) {
+ throw new RuntimeException("Config option `OidcAuth.provider_url` must be valid URL.");
+ }
+
+ // OpenIDConnectClient will append well-know path, so if well-know path is already part of the url, remove it
+ $wellKnownPosition = strpos($providerUrl, '/.well-known/');
+ if ($wellKnownPosition !== false) {
+ $providerUrl = substr($providerUrl, 0, $wellKnownPosition);
+ }
+
+ $clientId = $this->getConfig('client_id');
+ $clientSecret = $this->getConfig('client_secret');
+
+ $oidc = new OpenIDConnectClient($providerUrl, $clientId, $clientSecret);
+ $oidc->setRedirectURL(Configure::read('MISP.baseurl') . '/users/login');
+ return $oidc;
+ }
+
+ /**
+ * @param string $org
+ * @param array|null $user
+ * @param string $mispUsername
+ * @return int
+ * @throws Exception
+ */
+ private function checkOrganization($org, $user, $mispUsername)
+ {
+ if (empty($org)) {
+ $this->log($mispUsername, "Organisation name not provided.");
+ return false;
+ }
+
+ $orgIsUuid = Validation::uuid($org);
+
+ $orgAux = $this->userModel()->Organisation->find('first', [
+ 'fields' => ['Organisation.id'],
+ 'conditions' => $orgIsUuid ? ['uuid' => mb_strtolower($org)] : ['name' => $org],
+ ]);
+ if (empty($orgAux)) {
+ if ($orgIsUuid) {
+ $this->log($mispUsername, "Could not found organisation with UUID `$org`.");
+ return false;
+ }
+
+ $orgUserId = 1; // By default created by the admin
+ if ($user) {
+ $orgUserId = $user['id'];
+ }
+ $orgId = $this->userModel()->Organisation->createOrgFromName($org, $orgUserId, true);
+ $this->log($mispUsername, "User organisation `$org` created with ID $orgId.");
+ } else {
+ $orgId = $orgAux['Organisation']['id'];
+ $this->log($mispUsername, "User organisation `$org` found with ID $orgId.");
+ }
+ return $orgId;
+ }
+
+ /**
+ * @param array $roles Role list provided by OIDC
+ * @param string $mispUsername
+ * @return int|null Role ID or null if no role matches
+ */
+ private function getUserRole(array $roles, $mispUsername)
+ {
+ $roleMapper = $this->getConfig('role_mapper');
+ if (!is_array($roleMapper)) {
+ throw new RuntimeException("Config option `OidcAuth.role_mapper` must be array.");
+ }
+
+ $roleNameToId = $this->userModel()->Role->find('list', [
+ 'fields' => ['Role.name', 'Role.id'],
+ ]);
+ $roleNameToId = array_change_key_case($roleNameToId); // normalize role names to lowercase
+
+ $userRole = null;
+ foreach ($roles as $role) {
+ if (isset($roleMapper[$role])) {
+ $roleId = $roleMapper[$role];
+ if (!is_numeric($roleId)) {
+ $roleId = mb_strtolower($roleId);
+ if (isset($roleNameToId[$roleId])) {
+ $roleId = $roleNameToId[$roleId];
+ } else {
+ $this->log($mispUsername, "MISP Role with name `$roleId` not found, skipping.");
+ continue;
+ }
+ }
+ if ($userRole === null || $roleId < $userRole) { // role with lower ID wins
+ $userRole = $roleId;
+ }
+ }
+ }
+
+ return $userRole;
+ }
+
+ /**
+ * @param string $config
+ * @param mixed|null $default
+ * @return mixed
+ */
+ private function getConfig($config, $default = null)
+ {
+ $value = Configure::read("OidcAuth.$config");
+ if (empty($value)) {
+ if ($default === null) {
+ throw new RuntimeException("Config option `OidcAuth.$config` is not set.");
+ }
+ return $default;
+ }
+ return $value;
+ }
+
+ /**
+ * @param string $username
+ * @param string $message
+ */
+ private function log($username, $message)
+ {
+ CakeLog::info("OIDC: User `$username` – $message");
+ }
+
+ /**
+ * @return User
+ */
+ private function userModel()
+ {
+ if (isset($this->userModel)) {
+ return $this->userModel;
+ }
+
+ $this->userModel = ClassRegistry::init($this->settings['userModel']);
+ return $this->userModel;
+ }
+}
diff --git a/app/View/Elements/eventattributetoolbar.ctp b/app/View/Elements/eventattributetoolbar.ctp
index 22e8c5552..736a68391 100644
--- a/app/View/Elements/eventattributetoolbar.ctp
+++ b/app/View/Elements/eventattributetoolbar.ctp
@@ -51,7 +51,7 @@
'id' => 'create-button',
'title' => $possibleAction === 'attribute' ? __('Add attribute') : __('Add proposal'),
'fa-icon' => 'plus',
- //'onClick' => 'clickCreateButton',
+ 'class' => 'last',
'onClick' => 'openGenericModal',
'onClickParams' => array('/' . $possibleAction . 's/add/' . h($event['Event']['id']))
),
diff --git a/app/View/Elements/healthElements/diagnostics.ctp b/app/View/Elements/healthElements/diagnostics.ctp
index 855bd1815..404b249c2 100644
--- a/app/View/Elements/healthElements/diagnostics.ctp
+++ b/app/View/Elements/healthElements/diagnostics.ctp
@@ -115,6 +115,38 @@
?>
+ = __('Security Audit') ?>
+
+
+
+
+ = __('Area') ?> |
+ = __('Level') ?> |
+ = __('Message') ?> |
+
+
+
+ $errors): foreach ($errors as $error): list($level, $message) = $error; ?>
+
+ = h($field) ?> |
+
+
+
+
+
+
+
+
+ |
+ = h($message) ?> = __('More info') ?> |
+
+
+
+
+
+
-
-
-
-
-
- Form->create('Server', array('id' => 'removeTag_' . h($tag['Tag']['id']), 'url' => $baseurl . '/servers/removeTag/' . h($server['Server']['id']) . '/' . h($tag['Tag']['id']), 'style' => 'margin:0px;'));
- ?>
-
x
- Form->end();
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/View/Events/view.ctp b/app/View/Events/view.ctp
index a6a76138c..90c19a505 100644
--- a/app/View/Events/view.ctp
+++ b/app/View/Events/view.ctp
@@ -540,7 +540,7 @@
-
-
diff --git a/app/View/Layouts/default.ctp b/app/View/Layouts/default.ctp
index 6a05c310c..3b79002e4 100644
--- a/app/View/Layouts/default.ctp
+++ b/app/View/Layouts/default.ctp
@@ -15,7 +15,6 @@
'jquery-ui',
'chosen.min',
'main',
- 'jquery-jvectormap-2.0.5',
array('print', array('media' => 'print'))
);
if (Configure::read('MISP.custom_css')) {
@@ -121,10 +120,6 @@
- if ($('.alert').text().indexOf("$flashErrorMessage") >= 0) {
- var flashMessageLink = 'here';
- $('.alert').html(($('.alert').html().replace("$flashErrorMessage", flashMessageLink)));
- }
});
diff --git a/app/View/Layouts/flash.ctp b/app/View/Layouts/flash.ctp
deleted file mode 100644
index 566086d0f..000000000
--- a/app/View/Layouts/flash.ctp
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-