diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e53ebb9d9..294e448ee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -263,4 +263,6 @@ jobs: run: | tail -n +1 `pwd`/app/tmp/logs/* tail -n +1 /var/log/apache2/*.log - + + sudo -u $USER app/Console/cake Log export /tmp/logs.json.gz --without-changes + zcat /tmp/logs.json.gz diff --git a/INSTALL/MYSQL.sql b/INSTALL/MYSQL.sql index 44da990b4..9afec4086 100644 --- a/INSTALL/MYSQL.sql +++ b/INSTALL/MYSQL.sql @@ -1523,16 +1523,16 @@ INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `pe VALUES (2, 'Org Admin', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (3, 'User', NOW(), NOW(), 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1); +VALUES (3, 'User', NOW(), NOW(), 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (4, 'Publisher', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0); +VALUES (4, 'Publisher', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (5, 'Sync user', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0); +VALUES (5, 'Sync user', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (6, 'Read Only', NOW(), NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); +VALUES (6, 'Read Only', NOW(), NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); -- -------------------------------------------------------- @@ -1661,4 +1661,6 @@ INSERT IGNORE INTO `org_blocklists` (`org_uuid`, `created`, `org_name`, `comment ('58d38339-7b24-4386-b4b4-4c0f950d210f', NOW(), 'Setec Astrononomy', 'default example'), ('58d38326-eda8-443a-9fa8-4e12950d210f', NOW(), 'Acme Finance', 'default example'); -INSERT IGNORE INTO `admin_settings` (`setting`, `value`) VALUES ('fix_login', NOW()); +INSERT IGNORE INTO `admin_settings` (`setting`, `value`) VALUES +('fix_login', NOW()), +('default_role', 3); \ No newline at end of file diff --git a/PyMISP b/PyMISP index 24c528138..cca5287b2 160000 --- a/PyMISP +++ b/PyMISP @@ -1 +1 @@ -Subproject commit 24c52813876dd88a92b9fcc4b6c2cd259d80d733 +Subproject commit cca5287b2b11a1951789680c75e10636dde98add diff --git a/VERSION.json b/VERSION.json index 956e0f9e5..7b36fa445 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1 +1 @@ -{"major":2, "minor":4, "hotfix":166} +{"major":2, "minor":4, "hotfix":167} diff --git a/app/Console/Command/LogShell.php b/app/Console/Command/LogShell.php index 0da55c6f1..d4bf5f239 100644 --- a/app/Console/Command/LogShell.php +++ b/app/Console/Command/LogShell.php @@ -24,21 +24,33 @@ class LogShell extends AppShell ]); $parser->addSubcommand('export', [ 'help' => __('Export application logs to compressed file in JSON Lines format (one JSON encoded line per entry).'), - 'parser' => array( - 'arguments' => array( + 'parser' => [ + 'arguments' => [ 'file' => ['help' => __('Path to output file'), 'required' => true], - ), - ), + ], + 'options' => [ + 'without-changes' => ['boolean' => true, 'help' => __('Do not include add, edit or delete actions.')], + ], + ], ]); $parser->addSubcommand('recompress', [ 'help' => __('Recompress compressed data in logs.'), ]); + $parser->addSubcommand('accessLogRetention', [ + 'help' => __('Delete logs that are older than specified duration.'), + 'parser' => array( + 'arguments' => array( + 'duration' => ['help' => __('Duration in days'), 'required' => true], + ), + ), + ]); return $parser; } public function export() { list($path) = $this->args; + $withoutChanges = $this->param('without-changes'); if (file_exists($path)) { $this->error("File $path already exists"); @@ -49,21 +61,24 @@ class LogShell extends AppShell $this->error("Could not open $path for writing"); } - $rows = $this->Log->query("SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'logs';"); /** @var ProgressShellHelper $progress */ $progress = $this->helper('progress'); $progress->init([ - 'total' => $rows[0]['TABLES']['TABLE_ROWS'], // just estimate, but fast + 'total' => $this->Log->tableRows(), // just estimate, but fast 'width' => 50, ]); $lastId = 0; while (true) { + $conditions = ['Log.id >' => $lastId]; // much faster than offset + if ($withoutChanges) { + $conditions['NOT'] = ['Log.action' => ['add', 'edit', 'delete']]; + } $logs = $this->Log->find('all', [ - 'conditions' => ['id >' => $lastId], // much faster than offset + 'conditions' => $conditions, 'recursive' => -1, 'limit' => 100000, - 'order' => ['id ASC'], + 'order' => ['Log.id ASC'], ]); if (empty($logs)) { break; @@ -184,4 +199,15 @@ class LogShell extends AppShell { $this->AuditLog->recompress(); } + + public function accessLogRetention() + { + list($duration) = $this->args; + if ($duration <= 0 || !is_numeric($duration)) { + $this->error("Invalid duration specified."); + } + $duration = new DateTime("-$duration days"); + $deleted = $this->AccessLog->deleteOldLogs($duration); + $this->out(__n("Deleted %s entry", "Deleted %s entries", $deleted, $deleted)); + } } diff --git a/app/Controller/AccessLogsController.php b/app/Controller/AccessLogsController.php index 549a66e34..936c68697 100644 --- a/app/Controller/AccessLogsController.php +++ b/app/Controller/AccessLogsController.php @@ -61,6 +61,9 @@ class AccessLogsController extends AppController if (empty(Configure::read('MISP.log_skip_access_logs_in_application_logs'))) { $this->Flash->warning(__('Access logs are logged in both application logs and access logs. Make sure you reconfigure your log monitoring tools and update MISP.log_skip_access_logs_in_application_logs.')); } + + $this->AccessLog->virtualFields['has_query_log'] = 'query_log IS NOT NULL'; + $this->paginate['fields'][] = 'has_query_log'; $this->paginate['conditions'] = $conditions; $list = $this->paginate(); @@ -102,6 +105,23 @@ class AccessLogsController extends AppController $this->set('request', $data); } + public function admin_queryLog($id) + { + $request = $this->AccessLog->find('first', [ + 'conditions' => ['AccessLog.id' => $id], + 'fields' => ['AccessLog.query_log'], + ]); + if (empty($request)) { + throw new NotFoundException(__('Access log not found')); + } + + if (empty($request['AccessLog']['query_log'])) { + throw new NotFoundException(__('Query log is empty')); + } + + $this->set('queryLog', $request['AccessLog']['query_log']); + } + /** * @param array $params * @return array diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 96dc46a68..f969cbe95 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -34,7 +34,7 @@ class AppController extends Controller public $helpers = array('OrgImg', 'FontAwesome', 'UserName'); private $__queryVersion = '146'; - public $pyMispVersion = '2.4.166'; + public $pyMispVersion = '2.4.167'; public $phpmin = '7.2'; public $phprec = '7.4'; public $phptoonew = '8.0'; @@ -43,7 +43,6 @@ class AppController extends Controller private $isApiAuthed = false; public $baseurl = ''; - public $sql_dump = false; public $restResponsePayload = null; @@ -102,7 +101,9 @@ class AppController extends Controller { $controller = $this->request->params['controller']; $action = $this->request->params['action']; - + if (empty($this->Session->read('creation_timestamp'))) { + $this->Session->write('creation_timestamp', time()); + } if (Configure::read('MISP.system_setting_db')) { App::uses('SystemSetting', 'Model'); SystemSetting::setGlobalSetting(); @@ -136,17 +137,12 @@ class AppController extends Controller $this->response->header('X-XSS-Protection', '1; mode=block'); } - if (!empty($this->request->params['named']['sql'])) { - $this->sql_dump = intval($this->request->params['named']['sql']); - } - $this->_setupDatabaseConnection(); $this->set('debugMode', Configure::read('debug') >= 1 ? 'debugOn' : 'debugOff'); $isAjax = $this->request->is('ajax'); $this->set('ajax', $isAjax); $this->set('queryVersion', $this->__queryVersion); - $this->User = ClassRegistry::init('User'); $language = Configure::read('MISP.language'); if (!empty($language) && $language !== 'eng') { @@ -155,6 +151,21 @@ class AppController extends Controller Configure::write('Config.language', 'eng'); } + $this->User = ClassRegistry::init('User'); + if ($this->Auth->user()) { + if ($this->User->checkForSessionDestruction($this->Auth->user('id'))) { + $this->Auth->logout(); + $this->Session->destroy(); + $message = __('User deauthenticated on administrator request. Please reauthenticate.'); + if ($this->_isRest()) { + throw new ForbiddenException($message); + } else { + $this->Flash->warning($message); + $this->_redirectToLogin(); + } + } + } + // For fresh installation (salt empty) generate a new salt if (!Configure::read('Security.salt')) { $this->User->Server->serverSettingsSaveValue('Security.salt', $this->User->generateRandomPassword(32)); @@ -225,8 +236,9 @@ class AppController extends Controller if ($this->_isRest() || $this->_isAutomation()) { // disable CSRF for REST access $this->Security->csrfCheck = false; - if ($this->__loginByAuthKey() === false || $this->Auth->user() === null) { - if ($this->__loginByAuthKey() === null) { + $loginByAuthKeyResult = $this->__loginByAuthKey(); + if ($loginByAuthKeyResult === false || $this->Auth->user() === null) { + if ($loginByAuthKeyResult === null) { $this->loadModel('Log'); $this->Log->createLogEntry('SYSTEM', 'auth_fail', 'User', 0, "Failed API authentication. No authkey was provided."); } @@ -447,6 +459,9 @@ class AppController extends Controller } $this->Session->destroy(); } + } else { + $this->loadModel('Log'); + $this->Log->createLogEntry('SYSTEM', 'auth_fail', 'User', 0, "Failed authentication using an API key of incorrect length."); } return false; } @@ -674,7 +689,7 @@ class AppController extends Controller $shouldBeLogged = $userMonitoringEnabled || Configure::read('MISP.log_paranoid') || - (Configure::read('MISP.log_paranoid_api') && $user['logged_by_authkey']); + (Configure::read('MISP.log_paranoid_api') && isset($user['logged_by_authkey']) && $user['logged_by_authkey']); if ($shouldBeLogged) { $includeRequestBody = !empty(Configure::read('MISP.log_paranoid_include_post_body')) || $userMonitoringEnabled; @@ -684,8 +699,8 @@ class AppController extends Controller } if ( - (empty(Configure::read('MISP.log_skip_access_logs_in_application_logs'))) && - Configure::read('MISP.log_paranoid') || $userMonitoringEnabled + empty(Configure::read('MISP.log_skip_access_logs_in_application_logs')) && + $shouldBeLogged ) { $change = 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->request->here; if ( diff --git a/app/Controller/AttributesController.php b/app/Controller/AttributesController.php index 98353e80b..b87ed66f6 100644 --- a/app/Controller/AttributesController.php +++ b/app/Controller/AttributesController.php @@ -1961,7 +1961,7 @@ class AttributesController extends AppController public function fetchViewValue($id, $field = null) { $user = $this->_closeSession(); - $validFields = ['value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'timestamp', 'first_seen', 'last_seen']; + $validFields = ['value', 'comment', 'type', 'category', 'distribution', 'timestamp', 'first_seen', 'last_seen']; if (!isset($field) || !in_array($field, $validFields, true)) { throw new MethodNotAllowedException(__('Invalid field requested.')); } @@ -1989,9 +1989,7 @@ class AttributesController extends AppController $attribute = $attribute[0]; $result = $attribute['Attribute'][$field]; if ($field === 'distribution') { - $result = $this->Attribute->shortDist[$result]; - } elseif ($field === 'to_ids') { - $result = $result == 0 ? 'No' : 'Yes'; + $this->set('shortDist', $this->Attribute->shortDist); } elseif ($field === 'value') { $this->loadModel('Warninglist'); $attribute['Attribute'] = $this->Warninglist->checkForWarning($attribute['Attribute']); @@ -2006,23 +2004,27 @@ class AttributesController extends AppController public function fetchEditForm($id, $field = null) { - $validFields = array('value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'first_seen', 'last_seen'); - if (!isset($field) || !in_array($field, $validFields)) { - throw new MethodNotAllowedException(__('Invalid field requested.')); - } if (!$this->request->is('ajax')) { throw new MethodNotAllowedException(__('This function can only be accessed via AJAX.')); } - $fields = array('id', 'distribution', 'event_id'); - if ($field == 'category' || $field == 'type') { - $fields[] = 'type'; - $fields[] = 'category'; + + $validFields = array('value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'first_seen', 'last_seen'); + if (!isset($field) || !in_array($field, $validFields, true)) { + throw new NotFoundException(__('Invalid field requested.')); + } + $fieldsToFetch = array('id', 'event_id'); + if ($field === 'category' || $field === 'type') { + $fieldsToFetch[] = 'type'; + $fieldsToFetch[] = 'category'; + if ($field === 'type') { + $fieldsToFetch[] = 'value'; + } } else { - $fields[] = $field; + $fieldsToFetch[] = $field; } $params = array( 'conditions' => array('Attribute.id' => $id), - 'fields' => $fields, + 'fields' => $fieldsToFetch, 'flatten' => 1, 'contain' => array( 'Event' => array( @@ -2044,15 +2046,28 @@ class AttributesController extends AppController unset($distributionLevels[4]); $this->set('distributionLevels', $distributionLevels); } elseif ($field === 'category') { - $typeCategory = array(); + $possibleCategories = []; foreach ($this->Attribute->categoryDefinitions as $k => $category) { - foreach ($category['types'] as $type) { - $typeCategory[$type][] = $k; + if (in_array($attribute['Attribute']['type'], $category['types'], true)) { + $possibleCategories[] = $k; } } - $this->set('typeCategory', $typeCategory); + $this->set('possibleCategories', $possibleCategories); } elseif ($field === 'type') { - $this->set('categoryDefinitions', $this->Attribute->categoryDefinitions); + $possibleTypes = $this->Attribute->categoryDefinitions[$attribute['Attribute']['category']]['types']; + $validTypes = AttributeValidationTool::validTypesForValue($possibleTypes, $this->Attribute->getCompositeTypes(), $attribute['Attribute']['value']); + $options = []; + foreach ($possibleTypes as $possibleType) { + if ($this->Attribute->typeIsAttachment($possibleType)) { + continue; // skip attachment types + } + $options[] = [ + 'name' => $possibleType, + 'value' => $possibleType, + 'disabled' => !in_array($possibleType, $validTypes, true), + ]; + } + $this->set('options', $options); } $this->set('object', $attribute['Attribute']); $fieldURL = ucfirst($field); @@ -2798,10 +2813,7 @@ class AttributesController extends AppController 'fields' => ['Attribute.deleted', 'Attribute.event_id', 'Attribute.id', 'Attribute.object_id', 'Event.orgc_id', 'Event.user_id'], 'contain' => ['Event'], ]); - if (empty($attribute)) { - throw new NotFoundException(__('Invalid attribute')); - } - if ($attribute['Attribute']['deleted']) { + if (empty($attribute) || $attribute['Attribute']['deleted']) { throw new NotFoundException(__('Invalid attribute')); } if (empty($tag_id)) { @@ -2831,19 +2843,19 @@ class AttributesController extends AppController if (!$this->__canModifyTag($attribute, !empty($attributeTag['AttributeTag']['local']))) { return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => 'You do not have permission to do that.')), 'status' => 200, 'type' => 'json')); } - if (empty($attributeTag)) { return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => 'Invalid attribute - tag combination.')), 'status' => 200, 'type' => 'json')); } - $tag = $this->Attribute->AttributeTag->Tag->find('first', array( - 'conditions' => array('Tag.id' => $tag_id), - 'recursive' => -1, - 'fields' => array('Tag.name') - )); if ($this->Attribute->AttributeTag->delete($attributeTag['AttributeTag']['id'])) { if (empty($attributeTag['AttributeTag']['local'])) { $this->Attribute->touch($attribute); } + + $tag = $this->Attribute->AttributeTag->Tag->find('first', array( + 'conditions' => array('Tag.id' => $tag_id), + 'recursive' => -1, + 'fields' => array('Tag.name') + )); $log = ClassRegistry::init('Log'); $log->createLogEntry($this->Auth->user(), 'tag', 'Attribute', $id, 'Removed tag (' . $tag_id . ') "' . $tag['Tag']['name'] . '" from attribute (' . $id . ')', 'Attribute (' . $id . ') untagged of Tag (' . $tag_id . ')'); return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => 'Tag removed.', 'check_publish' => empty($attributeTag['AttributeTag']['local']))), 'status' => 200, 'type'=> 'json')); diff --git a/app/Controller/CommunitiesController.php b/app/Controller/CommunitiesController.php index 162d7162a..5b7a651bf 100644 --- a/app/Controller/CommunitiesController.php +++ b/app/Controller/CommunitiesController.php @@ -1,15 +1,13 @@ 60, 'maxLimit' => 9999 diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php index cab59d922..c78f98c15 100644 --- a/app/Controller/Component/ACLComponent.php +++ b/app/Controller/Component/ACLComponent.php @@ -384,7 +384,8 @@ class ACLComponent extends Component 'event_index' => array('*'), 'returnDates' => array('*'), 'testForStolenAttributes' => array(), - 'pruneUpdateLogs' => array() + 'pruneUpdateLogs' => array(), + 'index' => array('perm_audit') ), 'auditLogs' => [ 'admin_index' => ['perm_audit'], @@ -395,6 +396,7 @@ class ACLComponent extends Component 'accessLogs' => [ 'admin_index' => [], 'admin_request' => [], + 'admin_queryLog' => [], ], 'modules' => array( 'index' => array('perm_auth'), @@ -434,6 +436,7 @@ class ACLComponent extends Component 'groupAttributesIntoObject' => array('perm_add'), 'revise_object' => array('perm_add'), 'view' => array('*'), + 'createFromFreetext' => ['perm_add'], ), 'objectReferences' => array( 'add' => array('perm_add'), @@ -451,9 +454,9 @@ class ACLComponent extends Component 'objectChoice' => array('*'), 'objectMetaChoice' => array('perm_add'), 'view' => array('*'), - 'viewElements' => array('*'), 'index' => array('*'), - 'update' => array() + 'update' => array(), + 'possibleObjectTemplates' => ['*'], ), 'objectTemplateElements' => array( 'viewElements' => array('*') @@ -480,9 +483,9 @@ class ACLComponent extends Component 'display' => array('*'), ), 'posts' => array( - 'add' => array('not_read_only_authkey'), - 'delete' => array('not_read_only_authkey'), - 'edit' => array('not_read_only_authkey'), + 'add' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], + 'delete' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], + 'edit' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], 'pushMessageToZMQ' => array() ), 'regexp' => array( @@ -674,6 +677,7 @@ class ACLComponent extends Component 'taxonomyMassHide' => array('perm_tagger'), 'taxonomyMassUnhide' => array('perm_tagger'), 'toggleRequired' => array(), + 'toggleHighlighted' => array(), 'update' => array(), 'import' => [], 'export' => ['*'], @@ -713,14 +717,15 @@ class ACLComponent extends Component 'view' => array('*'), ), 'threads' => array( - 'index' => array('*'), - 'view' => array('*'), - 'viewEvent' => array('*'), + 'index' => array('discussion_enabled'), + 'view' => array('discussion_enabled'), + 'viewEvent' => array('discussion_enabled'), ), 'users' => array( 'acceptRegistrations' => array(), 'admin_add' => ['AND' => ['perm_admin', 'add_user_enabled']], 'admin_delete' => array('perm_admin'), + 'admin_destroy' => array(), 'admin_edit' => array('perm_admin'), 'admin_email' => array('perm_admin'), 'admin_filterUserIndex' => array('perm_admin'), @@ -774,7 +779,7 @@ class ACLComponent extends Component 'eventIndexColumnToggle' => ['*'], ), 'warninglists' => array( - 'checkValue' => array('perm_auth'), + 'checkValue' => ['*'], 'delete' => ['perm_warninglist'], 'enableWarninglist' => ['perm_warninglist'], 'getToggleField' => ['perm_warninglist'], @@ -864,6 +869,9 @@ class ACLComponent extends Component $this->dynamicChecks['delegation_enabled'] = function (array $user) { return (bool)Configure::read('MISP.delegation'); }; + $this->dynamicChecks['discussion_enabled'] = function (array $user) { + return !Configure::read('MISP.discussion_disable'); + }; // Returns true if current user is not using advanced auth key or if authkey is not read only $this->dynamicChecks['not_read_only_authkey'] = function (array $user) { return !isset($user['authkey_read_only']) || !$user['authkey_read_only']; diff --git a/app/Controller/Component/BetterSecurityComponent.php b/app/Controller/Component/BetterSecurityComponent.php index df3540a4d..a6eb76a0e 100644 --- a/app/Controller/Component/BetterSecurityComponent.php +++ b/app/Controller/Component/BetterSecurityComponent.php @@ -13,6 +13,15 @@ class BetterSecurityComponent extends SecurityComponent */ public $doNotGenerateToken = false; + public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) + { + $action = $controller->request->params['action']; + $unlockedActions = JsonTool::encode($this->unlockedActions); + $isRest = $controller->IndexFilter->isRest() ? '1' : '0'; + $this->log("Blackhole exception when accessing $controller->here (isRest: $isRest, action: $action, unlockedActions: $unlockedActions): {$exception->getMessage()}"); // log blackhole exception + return parent::blackHole($controller, $error, $exception); + } + public function generateToken(CakeRequest $request) { if (isset($request->params['requested']) && $request->params['requested'] === 1) { diff --git a/app/Controller/Component/IndexFilterComponent.php b/app/Controller/Component/IndexFilterComponent.php index fb0d62553..8d78351aa 100644 --- a/app/Controller/Component/IndexFilterComponent.php +++ b/app/Controller/Component/IndexFilterComponent.php @@ -44,15 +44,21 @@ class IndexFilterComponent extends Component } } } + + $data = $this->__massageData($data, $request, $paramArray); + + $this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs)); + return $data; + } + + private function __massageData($data, $request, $paramArray) + { + $data = array_filter($data, function($paramName) use ($paramArray) { + return !empty($paramArray[$paramName]); + }, ARRAY_FILTER_USE_KEY); + if (!empty($paramArray)) { foreach ($paramArray as $p) { - if ( - isset($options['ordered_url_params'][$p]) && - (!in_array(strtolower((string)$options['ordered_url_params'][$p]), array('null', '0', false, 'false', null))) - ) { - $data[$p] = $options['ordered_url_params'][$p]; - $data[$p] = str_replace(';', ':', $data[$p]); - } if (isset($request->params['named'][$p])) { $data[$p] = str_replace(';', ':', $request->params['named'][$p]); } @@ -67,28 +73,8 @@ class IndexFilterComponent extends Component } } unset($v); - if (!empty($options['additional_delimiters'])) { - if (!is_array($options['additional_delimiters'])) { - $options['additional_delimiters'] = array($options['additional_delimiters']); - } - foreach ($data as $k => $v) { - $found = false; - foreach ($options['additional_delimiters'] as $delim) { - if (strpos($v, $delim) !== false) { - $found = true; - break; - } - } - if ($found) { - $data[$k] = explode($options['additional_delimiters'][0], str_replace($options['additional_delimiters'], $options['additional_delimiters'][0], $v)); - foreach ($data[$k] as $k2 => $value) { - $data[$k][$k2] = trim($data[$k][$k2]); - } - } - } - } - $this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs)); return $data; + } public function isRest() diff --git a/app/Controller/Component/RestResponseComponent.php b/app/Controller/Component/RestResponseComponent.php index 28d4a60e0..e8d8c29e4 100644 --- a/app/Controller/Component/RestResponseComponent.php +++ b/app/Controller/Component/RestResponseComponent.php @@ -608,37 +608,34 @@ class RestResponseComponent extends Component $type = 'csv'; } else { $type = $format; - $dumpSql = !empty($this->Controller->sql_dump) && Configure::read('debug') > 1; + + $dumpSql = intval($this->Controller->request->params['named']['sql'] ?? 0); + if ($dumpSql && Configure::read('debug') < 2) { + $dumpSql = 0; // disable dumping SQL if debugging is off + } + if (!$raw) { if (is_string($response)) { $response = array('message' => $response); } if ($dumpSql) { - $this->Log = ClassRegistry::init('Log'); - if ($this->Controller->sql_dump === 2) { - $response = array('sql_dump' => $this->Log->getDataSource()->getLog(false, false)); + if ($dumpSql === 2) { + $response = ['sql_dump' => $this->getSqlLog()]; } else { - $response['sql_dump'] = $this->Log->getDataSource()->getLog(false, false); + $response['sql_dump'] = $this->getSqlLog(); } } - $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; - if (!$this->isAutomaticTool()) { - $flags |= JSON_PRETTY_PRINT; // Do not pretty print response for automatic tools - } - if (defined('JSON_THROW_ON_ERROR')) { - $flags |= JSON_THROW_ON_ERROR; // Throw exception on error if supported - } - $response = json_encode($response, $flags); + $prettyPrint = !$this->isAutomaticTool(); // Do not pretty print response for automatic tools + $response = JsonTool::encode($response, $prettyPrint); } else { if ($dumpSql) { - $this->Log = ClassRegistry::init('Log'); - if ($this->Controller->sql_dump === 2) { - $response = json_encode(array('sql_dump' => $this->Log->getDataSource()->getLog(false, false))); + if ($dumpSql === 2) { + $response = JsonTool::encode(['sql_dump' => $this->getSqlLog()]); } else { $response = substr_replace( $response, - sprintf(', "sql_dump": %s}', json_encode($this->Log->getDataSource()->getLog(false, false))), + sprintf(', "sql_dump": %s}', JsonTool::encode($this->getSqlLog())), -2 ); } @@ -2104,4 +2101,12 @@ class RestResponseComponent extends Component } return '/' . $admin_routing . $controller . '/' . $action; } + + /** + * @return array + */ + private function getSqlLog() + { + return $this->Controller->User->getDataSource()->getLog(false, false); + } } diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index b7cd292a0..45f9a22cf 100644 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -990,7 +990,7 @@ class EventsController extends AppController $possibleColumns[] = 'proposals'; } - if (Configure::read('MISP.showDiscussionsCountOnIndex')) { + if (Configure::read('MISP.showDiscussionsCountOnIndex') && !Configure::read('MISP.discussion_disable')) { $possibleColumns[] = 'discussion'; } @@ -1022,6 +1022,7 @@ class EventsController extends AppController if (in_array('tags', $columns, true) || in_array('clusters', $columns, true)) { $events = $this->Event->attachTagsToEvents($events); $events = $this->GalaxyCluster->attachClustersToEventIndex($user, $events, true); + $events = $this->__attachHighlightedTagsToEvents($events); } if (in_array('correlations', $columns, true)) { @@ -1036,7 +1037,7 @@ class EventsController extends AppController $events = $this->Event->attachProposalsCountToEvents($user, $events); } - if (in_array('discussion', $columns, true)) { + if (in_array('discussion', $columns, true) && !Configure::read('MISP.discussion_disable')) { $events = $this->Event->attachDiscussionsCountToEvents($user, $events); } @@ -1302,6 +1303,7 @@ class EventsController extends AppController } // remove galaxies tags + $containsProposals = !empty($event['ShadowAttribute']);; $this->loadModel('Taxonomy'); foreach ($event['Object'] as $k => $object) { if (isset($object['Attribute'])) { @@ -1312,6 +1314,9 @@ class EventsController extends AppController $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($attribute['AttributeTag']); $event['Object'][$k]['Attribute'][$k2]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } } } @@ -1323,6 +1328,9 @@ class EventsController extends AppController $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($attribute['AttributeTag']); $attribute['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } if (empty($this->passedArgs['sort'])) { $filters['sort'] = 'timestamp'; @@ -1337,6 +1345,7 @@ class EventsController extends AppController } $this->params->params['paging'] = array($this->modelClass => $params); $this->set('event', $event); + $this->set('includeOrgColumn', (isset($conditions['extended']) || $containsProposals)); $this->set('includeSightingdb', (!empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable'))); $this->set('deleted', isset($filters['deleted']) && $filters['deleted'] != 0); $this->set('attributeFilter', isset($filters['attributeFilter']) ? $filters['attributeFilter'] : 'all'); @@ -1386,32 +1395,11 @@ class EventsController extends AppController $emptyEvent = (empty($event['Object']) && empty($event['Attribute'])); $this->set('emptyEvent', $emptyEvent); - $attributeCount = isset($event['Attribute']) ? count($event['Attribute']) : 0; - $objectCount = isset($event['Object']) ? count($event['Object']) : 0; - $oldest_timestamp = false; + // set the data for the contributors / history field $contributors = $this->Event->ShadowAttribute->getEventContributors($event['Event']['id']); $this->set('contributors', $contributors); - if ($this->__canPublishEvent($event, $user)) { - $proposalStatus = false; - if (isset($event['ShadowAttribute']) && !empty($event['ShadowAttribute'])) { - $proposalStatus = true; - } - if (!$proposalStatus && !empty($event['Attribute'])) { - foreach ($event['Attribute'] as $temp) { - if (!empty($temp['ShadowAttribute'])) { - $proposalStatus = true; - break; - } - } - } - $mess = $this->Session->read('Message'); - if ($proposalStatus && empty($mess)) { - $this->Flash->info('This event has active proposals for you to accept or discard.'); - } - } - // set the pivot data $this->helpers[] = 'Pivot'; if ($continue) { @@ -1435,7 +1423,7 @@ class EventsController extends AppController } } foreach ($relatedEventCorrelationCount as $key => $relation) { - $relatedEventCorrelationCount[$key] = count($relatedEventCorrelationCount[$key]); + $relatedEventCorrelationCount[$key] = count($relation); } $this->Event->removeGalaxyClusterTags($event); @@ -1449,11 +1437,15 @@ class EventsController extends AppController } $this->set('tagConflicts', $tagConflicts); + $attributeCount = isset($event['Attribute']) ? count($event['Attribute']) : 0; + $objectCount = isset($event['Object']) ? count($event['Object']) : 0; + $oldestTimestamp = PHP_INT_MAX; + $containsProposals = !empty($event['ShadowAttribute']); $modDate = date("Y-m-d", $event['Event']['timestamp']); $modificationMap = array($modDate => 1); foreach ($event['Attribute'] as $k => $attribute) { - if ($oldest_timestamp === false || $oldest_timestamp > $attribute['timestamp']) { - $oldest_timestamp = $attribute['timestamp']; + if ($oldestTimestamp > $attribute['timestamp']) { + $oldestTimestamp = $attribute['timestamp']; } $modDate = date("Y-m-d", $attribute['timestamp']); $modificationMap[$modDate] = !isset($modificationMap[$modDate]) ? 1 : $modificationMap[$modDate] + 1; @@ -1470,10 +1462,10 @@ class EventsController extends AppController } $event['Attribute'][$k]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } - $attributeTagsName = $this->Event->Attribute->AttributeTag->extractAttributeTagsNameFromEvent($event); - $this->set('attributeTags', array_values($attributeTagsName['tags'])); - $this->set('attributeClusters', array_values($attributeTagsName['clusters'])); foreach ($event['Object'] as $k => $object) { $modDate = date("Y-m-d", $object['timestamp']); @@ -1481,8 +1473,8 @@ class EventsController extends AppController if (!empty($object['Attribute'])) { $attributeCount += count($object['Attribute']); foreach ($object['Attribute'] as $k2 => $attribute) { - if ($oldest_timestamp === false || $oldest_timestamp > $attribute['timestamp']) { - $oldest_timestamp = $attribute['timestamp']; + if ($oldestTimestamp > $attribute['timestamp']) { + $oldestTimestamp = $attribute['timestamp']; } $modDate = date("Y-m-d", $attribute['timestamp']); @@ -1500,9 +1492,24 @@ class EventsController extends AppController } $event['Object'][$k]['Attribute'][$k2]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } } } + + if ($containsProposals && $this->__canPublishEvent($event, $user)) { + $mess = $this->Session->read('Message'); + if (empty($mess)) { + $this->Flash->info(__('This event has active proposals for you to accept or discard.')); + } + } + + $attributeTagsName = $this->Event->Attribute->AttributeTag->extractAttributeTagsNameFromEvent($event); + $this->set('attributeTags', array_values($attributeTagsName['tags'])); + $this->set('attributeClusters', array_values($attributeTagsName['clusters'])); + $this->set('warningTagConflicts', $warningTagConflicts); $filters['sort'] = 'timestamp'; $filters['direction'] = 'desc'; @@ -1584,9 +1591,10 @@ class EventsController extends AppController if (!empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable')) { $this->set('sightingdbs', $this->Sightingdb->getSightingdbList($user)); } + $this->set('includeOrgColumn', $this->viewVars['extended'] || $containsProposals); $this->set('includeSightingdb', !empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable')); $this->set('relatedEventCorrelationCount', $relatedEventCorrelationCount); - $this->set('oldest_timestamp', $oldest_timestamp); + $this->set('oldest_timestamp', $oldestTimestamp === PHP_INT_MAX ? false : $oldestTimestamp); $this->set('missingTaxonomies', $this->Event->missingTaxonomies($event)); $this->set('currentUri', $attributeUri); $this->set('filters', $filters); @@ -1645,6 +1653,7 @@ class EventsController extends AppController $cortex_modules = $this->Module->getEnabledModules($user, false, 'Cortex'); $this->set('cortex_modules', $cortex_modules); } + $this->set('sightingsDbEnabled', (bool)Configure::read('Plugin.Sightings_sighting_db_enable')); } public function view($id = null, $continue = false, $fromEvent = null) @@ -1805,6 +1814,8 @@ class EventsController extends AppController $this->set('includeRelatedTags', (!empty($namedParams['includeRelatedTags'])) ? 1 : 0); $this->set('includeDecayScore', (!empty($namedParams['includeDecayScore'])) ? 1 : 0); + $this->__setHighlightedTags($event); + if ($this->_isSiteAdmin() && $event['Event']['orgc_id'] !== $this->Auth->user('org_id')) { $this->Flash->info(__('You are currently logged in as a site administrator and about to edit an event not belonging to your organisation. This goes against the sharing model of MISP. Use a normal user account for day to day work.')); } @@ -2273,7 +2284,12 @@ class EventsController extends AppController foreach ($analysisLevels as $key => $value) { $fieldDesc['analysis'][$key] = $this->Event->analysisDescriptions[$key]['formdesc']; } - $this->Flash->info(__('The event created will be visible to the organisations having an account on this platform, but not synchronised to other MISP instances until it is published.')); + + if (Configure::read('MISP.unpublishedprivate')) { + $this->Flash->info(__('The event created will be visible only to your organisation until it is published.')); + } else { + $this->Flash->info(__('The event created will be visible to the organisations having an account on this platform, but not synchronised to other MISP instances until it is published.')); + } $this->set('fieldDesc', $fieldDesc); if (isset($this->params['named']['extends'])) { $this->set('extends_uuid', $this->params['named']['extends']); @@ -2341,13 +2357,14 @@ class EventsController extends AppController $results = $this->Event->addMISPExportFile($this->Auth->user(), $data, $isXml, $takeOwnership, $publish); } catch (Exception $e) { $this->log("Exception during processing MISP file import: {$e->getMessage()}"); - $this->Flash->error(__('Could not process MISP export file. Probably file content is invalid.')); + $this->Flash->error(__('Could not process MISP export file. %s.', $e->getMessage())); $this->redirect(['controller' => 'events', 'action' => 'add_misp_export']); } } $this->set('results', $results); $this->render('add_misp_export_result'); } + $this->set('title_for_layout', __('Import from MISP Export File')); } public function upload_stix($stix_version = '1', $publish = false) @@ -3985,6 +4002,7 @@ class EventsController extends AppController $this->set('mayModify', $this->__canModifyEvent($event)); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $this->set('resultArray', $resultArray); $this->set('importComment', ''); $this->set('title_for_layout', __('Freetext Import Results')); @@ -5258,6 +5276,7 @@ class EventsController extends AppController $this->set('resultArray', $resultArray); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $this->set('importComment', $importComment); $this->render('resolved_attributes'); } @@ -5292,7 +5311,7 @@ class EventsController extends AppController } } - public function importModule($module, $eventId) + public function importModule($moduleName, $eventId) { $event = $this->Event->fetchSimpleEvent($this->Auth->user(), $eventId); if (!$event) { @@ -5302,8 +5321,7 @@ class EventsController extends AppController $eventId = $event['Event']['id']; $this->loadModel('Module'); - $moduleName = $module; - $module = $this->Module->getEnabledModule($module, 'Import'); + $module = $this->Module->getEnabledModule($moduleName, 'Import'); if (!is_array($module)) { throw new MethodNotAllowedException($module); } @@ -5311,10 +5329,11 @@ class EventsController extends AppController $module['mispattributes']['inputSource'] = array('paste'); } if ($this->request->is('post')) { + $requestData = $this->request->data['Event']; $fail = false; $modulePayload = array( - 'module' => $module['name'], - 'event_id' => $eventId, + 'module' => $module['name'], + 'event_id' => $eventId, ); if (isset($module['meta']['config'])) { foreach ($module['meta']['config'] as $conf) { @@ -5322,11 +5341,11 @@ class EventsController extends AppController } } if ($moduleName === 'csvimport') { - if (empty($this->request->data['Event']['config']['header']) && $this->request->data['Event']['config']['has_header'] === '1') { - $this->request->data['Event']['config']['header'] = ' '; + if (empty($requestData['config']['header']) && $requestData['config']['has_header'] === '1') { + $requestData['config']['header'] = ' '; } - if (empty($this->request->data['Event']['config']['special_delimiter'])) { - $this->request->data['Event']['config']['special_delimiter'] = ' '; + if (empty($requestData['config']['special_delimiter'])) { + $requestData['config']['special_delimiter'] = ' '; } } if (isset($module['mispattributes']['userConfig'])) { @@ -5337,18 +5356,19 @@ class EventsController extends AppController $validation = true; } } else { - $validation = call_user_func_array(array($this->Module, $this->Module->configTypes[$config['type']]['validation']), array($this->request->data['Event']['config'][$configName])); + $validationMethod = Module::CONFIG_TYPES[$config['type']]['validation']; + $validation = $this->Module->{$validationMethod}($requestData['config'][$configName]); } if ($validation !== true) { $fail = ucfirst($configName) . ': ' . $validation; } else { if (isset($config['regex']) && !empty($config['regex'])) { - $fail = preg_match($config['regex'], $this->request->data['Event']['config'][$configName]) ? false : ucfirst($configName) . ': ' . 'Invalid setting' . ($config['errorMessage'] ? ' - ' . $config['errorMessage'] : ''); + $fail = preg_match($config['regex'], $requestData['config'][$configName]) ? false : ucfirst($configName) . ': ' . 'Invalid setting' . ($config['errorMessage'] ? ' - ' . $config['errorMessage'] : ''); if (!empty($fail)) { - $modulePayload['config'][$configName] = $this->request->data['Event']['config'][$configName]; + $modulePayload['config'][$configName] = $requestData['config'][$configName]; } } else { - $modulePayload['config'][$configName] = $this->request->data['Event']['config'][$configName]; + $modulePayload['config'][$configName] = $requestData['config'][$configName]; } } } @@ -5356,31 +5376,29 @@ class EventsController extends AppController } if (!$fail) { if (!empty($module['mispattributes']['inputSource'])) { - if (!isset($this->request->data['Event']['source'])) { + if (!isset($requestData['source'])) { if (in_array('paste', $module['mispattributes']['inputSource'])) { - $this->request->data['Event']['source'] = '0'; + $requestData['source'] = '0'; } else { - $this->request->data['Event']['source'] = '1'; + $requestData['source'] = '1'; } } - if ($this->request->data['Event']['source'] == '1') { - if (isset($this->request->data['Event']['data'])) { - $modulePayload['data'] = base64_decode($this->request->data['Event']['data']); - } elseif (!isset($this->request->data['Event']['fileupload']) || empty($this->request->data['Event']['fileupload'])) { - $fail = 'Invalid file upload.'; + if ($requestData['source'] == '1') { + if (isset($requestData['data'])) { + $modulePayload['data'] = base64_decode($requestData['data']); + } elseif (empty($requestData['fileupload'])) { + $fail = __('Invalid file upload.'); } else { - $fileupload = $this->request->data['Event']['fileupload']; - $tmpfile = new File($fileupload['tmp_name']); - if ((isset($fileupload['error']) && $fileupload['error'] == 0) || (!empty($fileupload['tmp_name']) && $fileupload['tmp_name'] != 'none') && is_uploaded_file($tmpfile->path)) { + $fileupload = $requestData['fileupload']; + if ((isset($fileupload['error']) && $fileupload['error'] == 0) || (!empty($fileupload['tmp_name']) && $fileupload['tmp_name'] != 'none') && is_uploaded_file($fileupload['tmp_name'])) { $filename = basename($fileupload['name']); - App::uses('FileAccessTool', 'Tools'); - $modulePayload['data'] = FileAccessTool::readFromFile($fileupload['tmp_name'], $fileupload['size']); + $modulePayload['data'] = FileAccessTool::readAndDelete($fileupload['tmp_name']); } else { - $fail = 'Invalid file upload.'; + $fail = __('Invalid file upload.'); } } } else { - $modulePayload['data'] = $this->request->data['Event']['paste']; + $modulePayload['data'] = $requestData['paste']; } } else { $modulePayload['data'] = ''; @@ -5433,15 +5451,13 @@ class EventsController extends AppController $this->set('resultArray', $resultArray); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $render_name = 'resolved_attributes'; } - $distributions = $this->Event->Attribute->distributionLevels; - $sgs = $this->Event->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', 1); - if (empty($sgs)) { - unset($distributions[4]); - } - $this->set('distributions', $distributions); - $this->set('sgs', $sgs); + + $distributionData = $this->Event->Attribute->fetchDistributionData($this->Auth->user()); + $this->set('distributions', $distributionData['levels']); + $this->set('sgs', $distributionData['sgs']); $this->set('title', __('Import Results')); $this->set('title_for_layout', __('Import Results')); $this->set('importComment', $importComment); @@ -5452,7 +5468,7 @@ class EventsController extends AppController $this->Flash->error($fail); } } - $this->set('configTypes', $this->Module->configTypes); + $this->set('configTypes', Module::CONFIG_TYPES); $this->set('module', $module); $this->set('eventId', $eventId); $this->set('event', $event); @@ -6217,4 +6233,31 @@ class EventsController extends AppController $this->render('/genericTemplates/confirm'); } } + + /** + * @param array $event + * @return void + */ + private function __setHighlightedTags($event) + { + $this->loadModel('Taxonomy'); + $highlightedTags = $this->Taxonomy->getHighlightedTags($this->Taxonomy->getHighlightedTaxonomies(), $event['EventTag']); + $this->set('highlightedTags', $highlightedTags); + } + + /** + * + * @param array $events + * @return array + */ + private function __attachHighlightedTagsToEvents($events) + { + $this->loadModel('Taxonomy'); + $highlightedTaxonomies = $this->Taxonomy->getHighlightedTaxonomies(); + foreach ($events as $k => $event) { + $events[$k]['Event']['highlightedTags'] = $this->Taxonomy->getHighlightedTags($highlightedTaxonomies, $event['EventTag']); + } + + return $events; + } } diff --git a/app/Controller/GalaxiesController.php b/app/Controller/GalaxiesController.php index fc6ef9cd6..edb500e27 100644 --- a/app/Controller/GalaxiesController.php +++ b/app/Controller/GalaxiesController.php @@ -525,7 +525,7 @@ class GalaxiesController extends AppController } } - $result = $this->Galaxy->attachCluster($user, $target_type, $target_id, $cluster_id, $local); + $result = $this->Galaxy->attachCluster($user, $target_type, $target, $cluster_id, $local); return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => $result, 'check_publish' => true)), 'status'=>200, 'type' => 'json')); } diff --git a/app/Controller/GalaxyClustersController.php b/app/Controller/GalaxyClustersController.php index 52d5721e7..7246f5174 100644 --- a/app/Controller/GalaxyClustersController.php +++ b/app/Controller/GalaxyClustersController.php @@ -47,7 +47,7 @@ class GalaxyClustersController extends AppController } if ($filters['context'] == 'default') { - $contextConditions['GalaxyCluster.default'] = true; + $contextConditions['GalaxyCluster.default'] = true; } elseif ($filters['context'] == 'custom') { $contextConditions['GalaxyCluster.default'] = false; } elseif ($filters['context'] == 'org') { @@ -146,9 +146,8 @@ class GalaxyClustersController extends AppController 'GalaxyCluster.default' => 0, ] ]); - $this->loadModel('Attribute'); - $distributionLevels = $this->Attribute->distributionLevels; - unset($distributionLevels[5]); + $this->loadModel('Event'); + $distributionLevels = $this->Event->shortDist; $this->set('distributionLevels', $distributionLevels); $this->set('list', $clusters); $this->set('galaxy_id', $galaxyId); diff --git a/app/Controller/GalaxyElementsController.php b/app/Controller/GalaxyElementsController.php index c529a19a6..c09c80fcf 100644 --- a/app/Controller/GalaxyElementsController.php +++ b/app/Controller/GalaxyElementsController.php @@ -1,6 +1,9 @@ _closeSession(); $filters = $this->IndexFilter->harvestParameters(array('context', 'searchall')); - $aclConditions = $this->GalaxyElement->buildClusterConditions($this->Auth->user(), $clusterId); + $aclConditions = $this->GalaxyElement->buildClusterConditions($user, $clusterId); if (empty($filters['context'])) { $filters['context'] = 'all'; } @@ -44,18 +48,15 @@ class GalaxyElementsController extends AppController 'context' => $filters['context'], 'searchall' => isset($filters['searchall']) ? $filters['searchall'] : '' ])); - $cluster = $this->GalaxyElement->GalaxyCluster->fetchIfAuthorized($this->Auth->user(), $clusterId, array('edit', 'delete'), false, false); + $cluster = $this->GalaxyElement->GalaxyCluster->fetchIfAuthorized($user, $clusterId, array('edit', 'delete'), false, false); $canModify = !empty($cluster['authorized']); - $canModify = true; $this->set('canModify', $canModify); - if ($filters['context'] == 'JSONView') { + if ($filters['context'] === 'JSONView') { $expanded = $this->GalaxyElement->getExpandedJSONFromElements($elements); $this->set('JSONElements', $expanded); } - if ($this->request->is('ajax')) { - $this->layout = false; - $this->render('ajax/index'); - } + $this->layout = false; + $this->render('ajax/index'); } public function delete($elementId) diff --git a/app/Controller/LogsController.php b/app/Controller/LogsController.php index 13ee4c3de..d589bc28e 100644 --- a/app/Controller/LogsController.php +++ b/app/Controller/LogsController.php @@ -1,7 +1,9 @@ request->params['action']) { + if ('admin_search' === $this->request->params['action']) { $this->Security->csrfCheck = false; } } - public function admin_index() + public function index() { $paramArray = array('id', 'title', 'created', 'model', 'model_id', 'action', 'user_id', 'change', 'email', 'org', 'description', 'ip'); $filterData = array( 'request' => $this->request, - 'named_params' => $this->params['named'], + 'named_params' => $this->request->params['named'], 'paramArray' => $paramArray, 'ordered_url_params' => func_get_args() ); $exception = false; $filters = $this->_harvestParameters($filterData, $exception); unset($filterData); + if ($this->_isRest()) { if ($filters === false) { return $exception; @@ -71,8 +74,14 @@ class LogsController extends AppController } } if (!$this->_isSiteAdmin()) { - $orgRestriction = $this->Auth->user('Organisation')['name']; - $conditions['AND']['Log.org'] = $orgRestriction; + if ($this->_isAdmin()) { + // ORG admins can see their own org info + $orgRestriction = $this->Auth->user('Organisation')['name']; + $conditions['Log.org'] = $orgRestriction; + } else { + // users can see their own info + $conditions['Log.user_id'] = $this->Auth->user('id'); + } } $params = array( 'conditions' => $conditions, @@ -86,30 +95,42 @@ class LogsController extends AppController } $log_entries = $this->Log->find('all', $params); return $this->RestResponse->viewData($log_entries, 'json'); - } else { - $this->set('isSearch', 0); - $this->recursive = 0; - $validFilters = $this->Log->logMeta; - if (!$this->_isSiteAdmin()) { - $orgRestriction = $this->Auth->user('Organisation')['name']; - $conditions['Log.org'] = $orgRestriction; - $this->paginate['conditions'] = $conditions; - } else { - $validFilters = array_merge_recursive($validFilters, $this->Log->logMetaAdmin); - } - if (isset($this->params['named']['filter']) && in_array($this->params['named']['filter'], array_keys($validFilters))) { - $this->paginate['conditions']['Log.action'] = $validFilters[$this->params['named']['filter']]['values']; - } - foreach ($filters as $key => $value) { - if ($key === 'created') { - $key = 'created >='; - } - $this->paginate['conditions']["Log.$key"] = $value; - } - $this->set('validFilters', $validFilters); - $this->set('filter', isset($this->params['named']['filter']) ? $this->params['named']['filter'] : false); - $this->set('list', $this->paginate()); } + + $this->set('isSearch', 0); + $this->recursive = 0; + $validFilters = $this->Log->logMeta; + if ($this->_isSiteAdmin()) { + $validFilters = array_merge_recursive($validFilters, $this->Log->logMetaAdmin); + } + else if (!$this->_isSiteAdmin() && $this->_isAdmin()) { + // ORG admins can see their own org info + $orgRestriction = $this->Auth->user('Organisation')['name']; + $conditions['Log.org'] = $orgRestriction; + $this->paginate['conditions'] = $conditions; + } else { + // users can see their own info + $conditions['Log.email'] = $this->Auth->user('email'); + $this->paginate['conditions'] = $conditions; + } + if (isset($this->params['named']['filter']) && in_array($this->params['named']['filter'], array_keys($validFilters))) { + $this->paginate['conditions']['Log.action'] = $validFilters[$this->params['named']['filter']]['values']; + } + foreach ($filters as $key => $value) { + if ($key === 'created') { + $key = 'created >='; + } + $this->paginate['conditions']["Log.$key"] = $value; + } + $this->set('validFilters', $validFilters); + $this->set('filter', isset($this->params['named']['filter']) ? $this->params['named']['filter'] : false); + $this->set('list', $this->paginate()); + } + + public function admin_index() + { + $this->view = 'index'; + return $this->index(); } // Shows a minimalistic history for the currently selected event @@ -313,7 +334,7 @@ class LogsController extends AppController } // set the same view as the index page - $this->render('admin_index'); + $this->render('index'); } } else { // get from Session diff --git a/app/Controller/ObjectTemplatesController.php b/app/Controller/ObjectTemplatesController.php index f1f52f338..f536b978b 100644 --- a/app/Controller/ObjectTemplatesController.php +++ b/app/Controller/ObjectTemplatesController.php @@ -22,9 +22,10 @@ class ObjectTemplatesController extends AppController public function beforeFilter() { parent::beforeFilter(); - if (in_array($this->request->action, ['objectMetaChoice', 'objectChoice'], true)) { + if (in_array($this->request->action, ['objectMetaChoice', 'objectChoice', 'possibleObjectTemplates'], true)) { $this->Security->doNotGenerateToken = true; } + $this->Security->unlockedActions[] = 'possibleObjectTemplates'; } public function objectMetaChoice($eventId) @@ -162,16 +163,6 @@ class ObjectTemplatesController extends AppController $this->redirect($this->referer()); } - public function viewElements($id, $context = 'all') - { - $elements = $this->ObjectTemplate->ObjectTemplateElement->find('all', array( - 'conditions' => array('ObjectTemplateElement.object_template_id' => $id) - )); - $this->set('list', $elements); - $this->layout = false; - $this->render('ajax/view_elements'); - } - public function index($all = false) { $passedArgsArray = array(); @@ -183,11 +174,12 @@ class ObjectTemplatesController extends AppController $this->set('all', true); } if (!empty($this->params['named']['searchall'])) { + $searchTerm = '%' . strtolower($this->request->params['named']['searchall']) . '%'; $this->paginate['conditions']['AND']['OR'] = array( - 'ObjectTemplate.uuid LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'LOWER(ObjectTemplate.name) LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'ObjectTemplate.meta-category LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'LOWER(ObjectTemplate.description) LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%' + 'ObjectTemplate.uuid LIKE' => $searchTerm, + 'LOWER(ObjectTemplate.name) LIKE' => $searchTerm, + 'ObjectTemplate.meta-category LIKE' => $searchTerm, + 'LOWER(ObjectTemplate.description) LIKE' => $searchTerm, ); } if ($this->_isRest()) { @@ -196,11 +188,11 @@ class ObjectTemplatesController extends AppController unset($rules['order']); $objectTemplates = $this->ObjectTemplate->find('all', $rules); return $this->RestResponse->viewData($objectTemplates, $this->response->type()); - } else { - $this->paginate['order'] = array('ObjectTemplate.name' => 'ASC'); - $objectTemplates = $this->paginate(); - $this->set('list', $objectTemplates); } + + $this->paginate['order'] = array('ObjectTemplate.name' => 'ASC'); + $objectTemplates = $this->paginate(); + $this->set('list', $objectTemplates); $this->set('passedArgs', json_encode($passedArgs)); $this->set('passedArgsArray', $passedArgsArray); } @@ -315,4 +307,28 @@ class ObjectTemplatesController extends AppController } return $this->RestResponse->viewData($template, $this->response->type()); } + + public function possibleObjectTemplates() + { + session_abort(); + $this->request->allowMethod(['post']); + + $attributeTypes = $this->request->data['attributeTypes']; + $templates = $this->ObjectTemplate->fetchPossibleTemplatesBasedOnTypes($attributeTypes)['templates']; + + $results = []; + foreach ($templates as $template) { + $template = $template['ObjectTemplate']; + if ($template['compatibility'] === true && empty($template['invalidTypes'])) { + $results[] = [ + 'id' => $template['id'], + 'name' => $template['name'], + 'description' => $template['description'], + 'meta-category' => $template['meta-category'], + ]; + } + } + + return $this->RestResponse->viewData($results, 'json'); + } } diff --git a/app/Controller/ObjectsController.php b/app/Controller/ObjectsController.php index b5faafaa5..e626b2b00 100644 --- a/app/Controller/ObjectsController.php +++ b/app/Controller/ObjectsController.php @@ -60,33 +60,6 @@ class ObjectsController extends AppController $sgs = $this->MispObject->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', false, array_keys($sharing_groups)); $this->set('sharing_groups', $sgs); } - $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'], sprintf('{n}[multiple=true]')); - $multiple_attribute_allowed = array(); - foreach ($multiple_template_elements as $template_element) { - $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; - $multiple_attribute_allowed[$relation_type] = true; - } - $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); - // try to fetch similar objects - $cur_attrs = Hash::extract($this->request->data, 'Attribute.{n}.value'); - $conditions = array( - 'event_id' => $event_id, - 'value1' => $cur_attrs, - 'object_id !=' => '0' - ); - $similar_objects = $this->MispObject->Attribute->find('all', array( - 'conditions' => $conditions, - 'recursive' => -1, - 'fields' => 'object_id, count(object_id) as similarity_amount', - 'group' => 'object_id', - 'order' => 'similarity_amount DESC' - )); - $similar_object_ids = array(); - $similar_object_similarity_amount = array(); - foreach ($similar_objects as $obj) { - $similar_object_ids[] = $obj['Attribute']['object_id']; - $similar_object_similarity_amount[$obj['Attribute']['object_id']] = $obj[0]['similarity_amount']; - } if (isset($this->request->data['Attribute'])) { foreach ($this->request->data['Attribute'] as &$attribute) { @@ -113,33 +86,35 @@ class ObjectsController extends AppController 'cur_object_tmp_uuid' => $curObjectTmpUuid, 'data' => $this->request->data )); - if (!empty($similar_object_ids)) { - $this->set('similar_objects_count', count($similar_object_ids)); - $similar_object_ids = array_slice($similar_object_ids, 0, $similar_objects_display_threshold); // slice to honor the threshold - $similar_objects = $this->MispObject->fetchObjects($this->Auth->user(), array( - 'conditions' => array( - 'Object.id' => $similar_object_ids, - 'Object.template_uuid' => $template['ObjectTemplate']['uuid'] - ) - )); - foreach ($similar_objects as $key => $obj) { - $similar_objects[$key]['Object']['similarity_amount'] = $similar_object_similarity_amount[$obj['Object']['id']]; // sorting function cannot use external variables - } - usort($similar_objects, function ($a, $b) { // fetch Object returns object sorted by IDs, force the sort by the similarity amount - if ($a['Object']['similarity_amount'] == $b['Object']['similarity_amount']) { - return 0; + + if ($action === 'add') { + list($similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval) = $this->MispObject->findSimilarObjects( + $this->Auth->user(), + $event_id, + $this->request->data['Attribute'], + $template, + $similar_objects_display_threshold + ); + if ($similar_objects_count) { + $this->set('similar_objects_count', $similar_objects_count); + $this->set('similar_objects', $similar_objects); + $this->set('similar_objects_display_threshold', $similar_objects_display_threshold); + $this->set('simple_flattened_attribute', $simple_flattened_attribute); + $this->set('simple_flattened_attribute_noval', $simple_flattened_attribute_noval); + + $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'],'{n}[multiple=true]'); + $multiple_attribute_allowed = array(); + foreach ($multiple_template_elements as $template_element) { + $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; + $multiple_attribute_allowed[$relation_type] = true; } - return ($a['Object']['similarity_amount'] > $b['Object']['similarity_amount']) ? -1 : 1; - }); - $this->set('similar_objects', $similar_objects); - $this->set('similar_object_similarity_amount', $similar_object_similarity_amount); - $this->set('similar_objects_display_threshold', $similar_objects_display_threshold); + $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); + } } } - /** - * Create an object using a template + * Create an object using a template * POSTing will take the input and validate it against the template * GETing will return the template */ @@ -343,10 +318,16 @@ class ObjectsController extends AppController $template = $this->MispObject->prepareTemplate($template); $element = array(); foreach ($template['ObjectTemplateElement'] as $templateElement) { - if ($templateElement['object_relation'] == $object_relation) { + if ($templateElement['object_relation'] === $object_relation) { $element = $templateElement; + break; } } + + if (empty($element)) { + throw new NotFoundException(__("Object template do not contains object relation $object_relation")); + } + $distributionData = $this->MispObject->Event->Attribute->fetchDistributionData($this->Auth->user()); $this->layout = false; $this->set('distributionData', $distributionData); @@ -431,7 +412,7 @@ class ObjectsController extends AppController $savedObject = array(); if (!is_numeric($objectToSave)) { $object_validation_errors = array(); - foreach($objectToSave as $field => $field_errors) { + foreach ($objectToSave as $field => $field_errors) { $object_validation_errors[] = sprintf('%s: %s', $field, implode(', ', $field_errors)); } $error_message = __('Object could not be saved.') . PHP_EOL . implode(PHP_EOL, $object_validation_errors); @@ -458,12 +439,10 @@ class ObjectsController extends AppController return $this->RestResponse->saveFailResponse('Objects', 'edit', false, $id, $this->response->type()); } } else { - $message = __('Object attributes saved.'); if ($this->request->is('ajax')) { - $this->autoRender = false; if (is_numeric($objectToSave)) { $this->MispObject->Event->unpublishEvent($event); - return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => $message)), 'status'=>200, 'type' => 'json')); + return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => __('Object attributes saved.'))), 'status'=>200, 'type' => 'json')); } else { return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'errors' => $error_message)), 'status'=>200, 'type' => 'json')); } @@ -597,7 +576,7 @@ class ObjectsController extends AppController $object = $object[0]; $result = $object['Object'][$field]; if ($field === 'distribution') { - $result = $this->MispObject->shortDist[$result]; + $this->set('shortDist', $this->MispObject->Attribute->shortDist); } $this->set('value', $result); $this->set('field', $field); @@ -634,7 +613,7 @@ class ObjectsController extends AppController throw new NotFoundException(__('Invalid object')); } $this->layout = false; - if ($field == 'distribution') { + if ($field === 'distribution') { $distributionLevels = $this->MispObject->shortDist; unset($distributionLevels[4]); $this->set('distributionLevels', $distributionLevels); @@ -732,7 +711,7 @@ class ObjectsController extends AppController 'fields' => array('template_uuid', 'template_version', 'id', 'event_id'), 'flatten' => 1, 'contain' => array( - 'Event' + 'Event' => ['fields' => ['id', 'user_id', 'org_id', 'orgc_id']] ) ); // fetchObjects restrict access based on user @@ -1137,23 +1116,8 @@ class ObjectsController extends AppController $selectedAttributes = $this->_jsonDecode($selectedAttributes); $res = $this->MispObject->validObjectsFromAttributeTypes($this->Auth->user(), $eventId, $selectedAttributes); - $potentialTemplates = $res['templates']; - $attributeTypes = $res['types']; - usort($potentialTemplates, function($a, $b) { - if ($a['ObjectTemplate']['id'] == $b['ObjectTemplate']['id']) { - return 0; - } else if (is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { - return count($a['ObjectTemplate']['compatibility']) > count($b['ObjectTemplate']['compatibility']) ? 1 : -1; - } else if (is_array($a['ObjectTemplate']['compatibility']) && !is_array($b['ObjectTemplate']['compatibility'])) { - return 1; - } else if (!is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { - return -1; - } else { // sort based on invalidTypes count - return count($a['ObjectTemplate']['invalidTypes']) > count($b['ObjectTemplate']['invalidTypes']) ? 1 : -1; - } - }); - $this->set('potential_templates', $potentialTemplates); - $this->set('selected_types', $attributeTypes); + $this->set('potential_templates', $res['templates']); + $this->set('selected_types', $res['types']); $this->set('event_id', $eventId); } @@ -1230,7 +1194,8 @@ class ObjectsController extends AppController if (empty($template)) { throw new NotFoundException(__('Invalid template.')); } - $conformity_result = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $selected_attributes); + $attributeTypes = array_column(array_column($selected_attributes, 'Attribute'), 'type'); + $conformity_result = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributeTypes); $skipped_attributes = 0; foreach ($selected_attributes as $i => $attribute) { if (in_array($attribute['Attribute']['type'], $conformity_result['invalidTypes'], true)) { @@ -1278,6 +1243,151 @@ class ObjectsController extends AppController } } + public function createFromFreetext($eventId) + { + $this->request->allowMethod(['post']); + + $event = $this->MispObject->Event->find('first', array( + 'recursive' => -1, + 'fields' => array('Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.user_id', 'Event.publish_timestamp'), + 'conditions' => array('Event.id' => $eventId) + )); + if (empty($event)) { + throw new NotFoundException(__('Invalid event.')); + } + if (!$this->__canModifyEvent($event)) { + throw new ForbiddenException(__('You do not have permission to do that.')); + } + + $requestData = $this->request->data['Object']; + $selectedTemplateId = $requestData['selectedTemplateId']; + $template = $this->MispObject->ObjectTemplate->find('first', array( + 'recursive' => -1, + 'conditions' => array( + 'ObjectTemplate.id' => $selectedTemplateId, + 'ObjectTemplate.active' => true, + ), + 'contain' => ['ObjectTemplateElement'], + )); + if (empty($template)) { + throw new NotFoundException(__('Invalid template.')); + } + + if (isset($requestData['selectedObjectRelationMapping'])) { + $distribution = $requestData['distribution']; + $sharingGroupId = $requestData['sharing_group_id'] ?? 0; + $comment = $requestData['comment']; + if ($distribution == 4) { + $sg = $this->MispObject->SharingGroup->fetchSG($sharingGroupId, $this->Auth->user()); + if (empty($sg)) { + throw new NotFoundException(__('Invalid sharing group.')); + } + } else { + $sharingGroupId = 0; + } + + $attributes = $this->_jsonDecode($requestData['attributes']); + $selectedObjectRelationMapping = $this->_jsonDecode($requestData['selectedObjectRelationMapping']); + + // Attach object relation to attributes and fix tag format + foreach ($attributes as $k => &$attribute) { + $attribute['object_relation'] = $selectedObjectRelationMapping[$k]; + if (!empty($attribute['tags'])) { + $attribute['Tag'] = []; + foreach (explode(",", $attribute['tags']) as $tagName) { + $attribute['Tag'][] = [ + 'name' => trim($tagName), + ]; + } + unset($attribute['tags']); + } + } + + $object = [ + 'Object' => [ + 'event_id' => $eventId, + 'distribution' => $distribution, + 'sharing_group_id' => $sharingGroupId, + 'comment' => $comment, + 'Attribute' => $attributes, + ], + ]; + + $object = $this->MispObject->fillObjectDataFromTemplate($object, $template); + $result = $this->MispObject->captureObject($object, $eventId, $this->Auth->user(), true, false, $event); + if ($result === true) { + return $this->RestResponse->saveSuccessResponse('Objects', 'Created from Attributes', $result, 'json'); + } else { + $error = __('Failed to create an Object from Attributes. Error: ') . PHP_EOL . h($result); + return $this->RestResponse->saveFailResponse('Objects', 'Created from Attributes', false, $error, 'json'); + } + } else { + $attributes = $this->_jsonDecode($requestData['attributes']); + + $processedAttributes = []; + foreach ($attributes as $attribute) { + if ($attribute['type'] === 'ip-src/ip-dst') { + $types = array('ip-src', 'ip-dst'); + } elseif ($attribute['type'] === 'ip-src|port/ip-dst|port') { + $types = array('ip-src|port', 'ip-dst|port'); + } else { + $types = [$attribute['type']]; + } + foreach ($types as $type) { + $attribute['type'] = $type; + $processedAttributes[] = $attribute; + } + } + + $attributeTypes = array_column($processedAttributes, 'type'); + $conformityResult = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributeTypes); + + if ($conformityResult['valid'] !== true || !empty($conformityResult['invalidTypes'])) { + throw new NotFoundException(__('Invalid template.')); + } + + $objectRelations = []; + foreach ($template['ObjectTemplateElement'] as $templateElement) { + $objectRelations[$templateElement['type']][] = $templateElement; + } + + // Attach first object_relation according to attribute type that will be considered as default + foreach ($processedAttributes as &$attribute) { + $attribute['object_relation'] = $objectRelations[$attribute['type']][0]['object_relation']; + } + + $distributionData = $this->MispObject->Event->Attribute->fetchDistributionData($this->Auth->user()); + $this->set('event', $event); + $this->set('distributionData', $distributionData); + $this->set('distributionLevels', $this->MispObject->Attribute->distributionLevels); + $this->set('template', $template); + $this->set('objectRelations', $objectRelations); + $this->set('attributes', $processedAttributes); + + list($similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval) = $this->MispObject->findSimilarObjects( + $this->Auth->user(), + $eventId, + $processedAttributes, + $template + ); + if ($similar_objects_count) { + $this->set('similar_objects_count', $similar_objects_count); + $this->set('similar_objects', $similar_objects); + $this->set('similar_objects_display_threshold', 15); + $this->set('simple_flattened_attribute', $simple_flattened_attribute); + $this->set('simple_flattened_attribute_noval', $simple_flattened_attribute_noval); + + $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'],'{n}[multiple=true]'); + $multiple_attribute_allowed = array(); + foreach ($multiple_template_elements as $template_element) { + $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; + $multiple_attribute_allowed[$relation_type] = true; + } + $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); + } + } + } + private function __objectIdToConditions($id) { if (is_numeric($id)) { diff --git a/app/Controller/TagsController.php b/app/Controller/TagsController.php index 51eb10822..d1cabe53f 100644 --- a/app/Controller/TagsController.php +++ b/app/Controller/TagsController.php @@ -367,6 +367,9 @@ class TagsController extends AppController // Remove galaxy tags $event = $this->Tag->removeGalaxyClusterTags($user, $event); + $highlightedTags = $this->Taxonomy->getHighlightedTags($this->Taxonomy->getHighlightedTaxonomies(), $event['EventTag']); + $this->set('highlightedTaxonomies', $highlightedTags); + $this->set('tags', $event['EventTag']); $this->set('missingTaxonomies', $this->Tag->EventTag->Event->missingTaxonomies($event)); $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($event['EventTag']); diff --git a/app/Controller/TaxonomiesController.php b/app/Controller/TaxonomiesController.php index afbb6c22c..b47b80617 100644 --- a/app/Controller/TaxonomiesController.php +++ b/app/Controller/TaxonomiesController.php @@ -59,7 +59,7 @@ class TaxonomiesController extends AppController public function view($id) { - $taxonomy = $this->Taxonomy->getTaxonomy($id, ['full' => $this->_isRest()]); + $taxonomy = $this->Taxonomy->getTaxonomy($id, $this->_isRest()); if (empty($taxonomy)) { throw new NotFoundException(__('Taxonomy not found.')); } @@ -498,6 +498,32 @@ class TaxonomiesController extends AppController $this->render('ajax/toggle_required'); } + public function toggleHighlighted($id) + { + $taxonomy = $this->Taxonomy->find('first', array( + 'recursive' => -1, + 'conditions' => array('Taxonomy.id' => $id) + )); + if (empty($taxonomy)) { + return $this->RestResponse->saveFailResponse('Taxonomy', 'toggleHighlighted', $id, 'Invalid Taxonomy', $this->response->type()); + } + if ($this->request->is('post')) { + $taxonomy['Taxonomy']['highlighted'] = $this->request->data['Taxonomy']['highlighted']; + $result = $this->Taxonomy->save($taxonomy); + if ($result) { + return $this->RestResponse->saveSuccessResponse('Taxonomy', 'toggleHighlighted', $id, $this->response->type()); + } else { + return $this->RestResponse->saveFailResponse('Taxonomy', 'toggleHighlighted', $id, $this->validationError, $this->response->type()); + } + } + + $this->set('highlighted', !$taxonomy['Taxonomy']['highlighted']); + $this->set('id', $id); + $this->autoRender = false; + $this->layout = false; + $this->render('ajax/toggle_highlighted'); + } + /** * @param string $action * @param int $modelId diff --git a/app/Controller/UsersController.php b/app/Controller/UsersController.php index 3ba568809..51ca9e6d2 100644 --- a/app/Controller/UsersController.php +++ b/app/Controller/UsersController.php @@ -117,8 +117,14 @@ class UsersController extends AppController return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => 'Something went wrong, please try again later.')), 'status'=>200, 'type' => 'json')); } - public function unsubscribe($code) + public function unsubscribe($code, $type = null) { + if ($type === null) { + $type = 'autoalert'; + } else if (!in_array($type, ['autoalert', 'notification_daily', 'notification_weekly', 'notification_monthly'], true)) { + throw new NotFoundException("Invalid type $type."); + } + $user = $this->Auth->user(); if (!hash_equals($this->User->unsubscribeCode($user), rtrim($code, '.'))) { @@ -126,11 +132,11 @@ class UsersController extends AppController $this->redirect(['action' => 'view', 'me']); } - if ($user['autoalert']) { - $this->User->updateField($this->Auth->user(), 'autoalert', false); - $this->Flash->success(__('Successfully unsubscribed from event alert.')); + if ($user[$type]) { + $this->User->updateField($user, $type, false); + $this->Flash->success(__('Successfully unsubscribed from notification.')); } else { - $this->Flash->info(__('Already unsubscribed from event alert.')); + $this->Flash->info(__('Already unsubscribed from notification.')); } $this->redirect(['action' => 'view', 'me']); } @@ -1080,27 +1086,35 @@ class UsersController extends AppController public function admin_delete($id = null) { - $this->request->allowMethod(['post', 'delete']); - - $user = $this->User->find('first', array( - 'conditions' => $this->__adminFetchConditions($id), - 'recursive' => -1 - )); - if (empty($user)) { - throw new NotFoundException(__('Invalid user')); - } - if ($this->User->delete($id)) { - $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email']; - $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 { - $this->Flash->success(__('User deleted')); - $this->redirect(array('action' => 'index')); + if ($this->request->is('post') || $this->request->is('delete')) { + $user = $this->User->find('first', array( + 'conditions' => $this->__adminFetchConditions($id), + 'recursive' => -1 + )); + if (empty($user)) { + throw new NotFoundException(__('Invalid user')); } + if ($this->User->delete($id)) { + $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email']; + $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 { + $this->Flash->success(__('User deleted')); + $this->redirect(array('action' => 'index')); + } + } + $this->Flash->error(__('User was not deleted')); + $this->redirect(array('action' => 'index')); + } else { + $this->set( + 'question', + __('Are you sure you want to delete the user? It is highly recommended to never delete users but to disable them instead.') + ); + $this->set('title', __('Delete user')); + $this->set('actionName', 'Delete'); + $this->render('/genericTemplates/confirm'); } - $this->Flash->error(__('User was not deleted')); - $this->redirect(array('action' => 'index')); } public function admin_massToggleField($fieldName, $enabled) @@ -2828,4 +2842,55 @@ class UsersController extends AppController } return $conditions; } + + public function admin_destroy($id = null) + { + $conditionFields = ['id', 'email']; + $params = $this->IndexFilter->harvestParameters(['id', 'email']); + if (!empty($id)) { + $params['id'] = $id; + } + $conditions = []; + foreach ($conditionFields as $conditionField) { + if (!empty($params[$conditionField])) { + $conditions[$conditionField . ' LIKE'] = $params[$conditionField]; + } + } + if (!empty($conditions)) { + $user_ids = $this->User->find('list', [ + 'recursive' => -1, + 'fields' => ['email', 'id'], + 'conditions' => $conditions + ]); + } else { + $user_ids = [__('Every user') => 'all']; + } + if ($this->request->is('post')) { + $redis = RedisTool::init(); + $kill_before = time(); + foreach (array_values($user_ids) as $user_id) { + $redis->set('misp:session_destroy:' . $user_id, $kill_before); + } + $message = __( + 'Session destruction cutoff set to the current timestamp for the given selection (%s). Session(s) will be destroyed on the next user interaction.', + implode(', ', array_keys($user_ids)) + ); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', 'admin_destroy', false, $this->response->type(), $message); + } + $this->Flash->success($message); + $this->redirect($this->referer()); + } else { + $this->set( + 'question', + __( + 'Do you really wish to destroy the session for: %s ? The session destruction will occur when the users try to interact with MISP the next time.', + implode(', ', array_keys($user_ids)) + ) + ); + $this->set('title', __('Destroy sessions')); + $this->set('actionName', 'Destroy'); + $this->render('/genericTemplates/confirm'); + } + } } diff --git a/app/Lib/Dashboard/RecentSightingsWidget.php b/app/Lib/Dashboard/RecentSightingsWidget.php index 7a34a6c6d..eeb64ab53 100644 --- a/app/Lib/Dashboard/RecentSightingsWidget.php +++ b/app/Lib/Dashboard/RecentSightingsWidget.php @@ -35,21 +35,21 @@ class RecentSightingsWidget $data = array(); $count = 0; - foreach (JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())->{'response'} as $el) { - $sighting = $el->{'Sighting'}; - $event = $sighting->{'Event'}; - $attribute = $sighting->{'Attribute'}; + foreach (JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())['response'] as $el) { + $sighting = $el['Sighting']; + $event = $sighting['Event']; + $attribute = $sighting['Attribute']; - if ($sighting->{'type'} == 0) $type = "Sighting"; - elseif ($sighting->{'type'} == 1) $type = "False positive"; + if ($sighting['type'] == 0) $type = "Sighting"; + elseif ($sighting['type'] == 1) $type = "False positive"; else $type = "Expiration"; - $output = $attribute->{'value'} . " (id: " . $attribute->{'id'} . ") in " . $event->{'info'} . " (id: " . $event->{'id'} . ")"; + $output = $attribute['value'] . " (id: " . $attribute['id'] . ") in " . $event['info'] . " (id: " . $event['id'] . ")"; $data[] = array( 'title' => $type, 'value' => $output, 'html' => sprintf( ' (Event %s)', - Configure::read('MISP.baseurl') . '/events/view/', $event->{'id'}, - $event->{'id'} + Configure::read('MISP.baseurl') . '/events/view/', $event['id'], + $event['id'] ) ); ++$count; diff --git a/app/Lib/Dashboard/TresholdSightingsWidget.php b/app/Lib/Dashboard/TresholdSightingsWidget.php index 95fdf969a..d74dafeb4 100644 --- a/app/Lib/Dashboard/TresholdSightingsWidget.php +++ b/app/Lib/Dashboard/TresholdSightingsWidget.php @@ -31,21 +31,21 @@ class TresholdSightingsWidget $data = array(); $sightings_score = array(); - $restSearch = JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())->{'response'}; + $restSearch = JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())['response']; foreach ($restSearch as $el) { - $sighting = $el->{'Sighting'}; - $attribute = $sighting->{'Attribute'}; - $event = $sighting->{'Event'}; + $sighting = $el['Sighting']; + $attribute = $sighting['Attribute']; + $event = $sighting['Event']; - if (!array_key_exists($attribute->{'id'}, $sightings_score)) $sightings_score[$attribute->{'id'}] = array( 'value' => $attribute->{'value'}, + if (!array_key_exists($attribute['id'], $sightings_score)) $sightings_score[$attribute['id']] = array( 'value' => $attribute['value'], 'score' => 0, - 'event_title' => $event->{'info'}, - 'event_id' => $event->{'id'}); + 'event_title' => $event['info'], + 'event_id' => $event['id']); # Sighting - if ($sighting->{'type'} == 0) $sightings_score[$attribute->{'id'}]['score'] = $sightings_score[$attribute->{'id'}]['score'] - 1; + if ($sighting['type'] == 0) $sightings_score[$attribute['id']]['score'] = $sightings_score[$attribute['id']]['score'] - 1; # False Positive - elseif ($sighting->{'type'} == 1) $sightings_score[$attribute->{'id'}]['score'] = $sightings_score[$attribute->{'id'}]['score'] + 1; + elseif ($sighting['type'] == 1) $sightings_score[$attribute['id']]['score'] = $sightings_score[$attribute['id']]['score'] + 1; } foreach ($sightings_score as $attribute_id => $s) { diff --git a/app/Lib/Tools/AttributeValidationTool.php b/app/Lib/Tools/AttributeValidationTool.php index 564a36915..20a734936 100644 --- a/app/Lib/Tools/AttributeValidationTool.php +++ b/app/Lib/Tools/AttributeValidationTool.php @@ -202,7 +202,7 @@ class AttributeValidationTool $value = substr($value, 2); // remove 'AS' } if (strpos($value, '.') !== false) { // maybe value is in asdot notation - $parts = explode('.', $value); + $parts = explode('.', $value, 2); if (self::isPositiveInteger($parts[0]) && self::isPositiveInteger($parts[1])) { return $parts[0] * 65536 + $parts[1]; } @@ -412,12 +412,12 @@ class AttributeValidationTool } return __('Email address has an invalid format. Please double check the value or select type "other".'); case 'vulnerability': - if (preg_match("#^(CVE-)[0-9]{4}(-)[0-9]{4,}$#", $value)) { + if (preg_match("#^CVE-[0-9]{4}-[0-9]{4,}$#", $value)) { return true; } return __('Invalid format. Expected: CVE-xxxx-xxxx...'); case 'weakness': - if (preg_match("#^(CWE-)[0-9]{1,}$#", $value)) { + if (preg_match("#^CWE-[0-9]+$#", $value)) { return true; } return __('Invalid format. Expected: CWE-x...'); @@ -583,6 +583,28 @@ class AttributeValidationTool throw new InvalidArgumentException("Unknown type $type."); } + /** + * This method will generate all valid types for given value. + * @param array $types Typos to check + * @param array $compositeTypes Composite types + * @param string $value Values to check + * @return array + */ + public static function validTypesForValue(array $types, array $compositeTypes, $value) + { + $possibleTypes = []; + foreach ($types as $type) { + if (in_array($type, $compositeTypes, true) && substr_count($value, '|') !== 1) { + continue; // value is not in composite format + } + $modifiedValue = AttributeValidationTool::modifyBeforeValidation($type, $value); + if (AttributeValidationTool::validate($type, $modifiedValue) === true) { + $possibleTypes[] = $type; + } + } + return $possibleTypes; + } + /** * @param string $value * @return bool diff --git a/app/Lib/Tools/EventTimelineTool.php b/app/Lib/Tools/EventTimelineTool.php index 145ab9909..d684d8882 100644 --- a/app/Lib/Tools/EventTimelineTool.php +++ b/app/Lib/Tools/EventTimelineTool.php @@ -57,6 +57,12 @@ $event['Attribute'] = array(); } + if (!empty($fullevent[0]['Sighting'])) { + $event['Sighting'] = $fullevent[0]['Sighting']; + } else { + $event['Sighting'] = array(); + } + return $event; } @@ -81,6 +87,11 @@ $attribute = array(); } + $sightingsAttributeMap = []; + foreach ($event['Sighting'] as $sighting) { + $sightingsAttributeMap[$sighting['attribute_id']][] = $sighting['date_sighting']; + } + // extract links and node type foreach ($attribute as $attr) { $toPush = array( @@ -93,6 +104,7 @@ 'first_seen' => $attr['first_seen'], 'last_seen' => $attr['last_seen'], 'attribute_type' => $attr['type'], + 'date_sighting' => $sightingsAttributeMap[$attr['id']] ?? [], 'is_image' => $this->__eventModel->Attribute->isImage($attr), ); $this->__json['items'][] = $toPush; @@ -134,6 +146,7 @@ 'group' => 'object_attribute', 'timestamp' => $obj_attr['timestamp'], 'attribute_type' => $obj_attr['type'], + 'date_sighting' => $sightingsAttributeMap[$attr['id']] ?? [], 'is_image' => $this->__eventModel->Attribute->isImage($obj_attr), ); $toPush_obj['Attribute'][] = $toPush_attr; diff --git a/app/Lib/Tools/FileAccessTool.php b/app/Lib/Tools/FileAccessTool.php index 3e3aa01cd..b56338a21 100644 --- a/app/Lib/Tools/FileAccessTool.php +++ b/app/Lib/Tools/FileAccessTool.php @@ -107,8 +107,14 @@ class FileAccessTool } if (file_put_contents($file, $content, LOCK_EX | (!empty($append) ? FILE_APPEND : 0)) === false) { - $freeSpace = disk_free_space($dir); - throw new Exception("An error has occurred while attempt to write to file `$file`. Maybe not enough space? ($freeSpace bytes left)"); + if (file_exists($file) && !is_writable($file)) { + $errorMessage = 'File is not writeable.'; + } else { + $freeSpace = disk_free_space($dir); + $errorMessage = "Maybe not enough space? ($freeSpace bytes left)"; + } + + throw new Exception("An error has occurred while attempt to write to file `$file`. $errorMessage"); } } diff --git a/app/Lib/Tools/RedisTool.php b/app/Lib/Tools/RedisTool.php index b1ae623c1..8a68dfe20 100644 --- a/app/Lib/Tools/RedisTool.php +++ b/app/Lib/Tools/RedisTool.php @@ -26,12 +26,18 @@ class RedisTool } $host = Configure::read('MISP.redis_host') ?: '127.0.0.1'; - $port = Configure::read('MISP.redis_port') ?: 6379; + $socket = false; + if ($host[0] === '/') { + $socket = $host; + } else { + $port = Configure::read('MISP.redis_port') ?: 6379; + } $database = Configure::read('MISP.redis_database') ?: 13; $pass = Configure::read('MISP.redis_password'); $redis = new Redis(); - if (!$redis->connect($host, (int) $port)) { + $connection = empty($socket) ? $redis->connect($host, (int) $port) : $redis->connect($host); + if (!$connection) { throw new Exception("Could not connect to Redis: {$redis->getLastError()}"); } if (!empty($pass)) { diff --git a/app/Model/AccessLog.php b/app/Model/AccessLog.php index bb637f0c5..ffde1b5b8 100644 --- a/app/Model/AccessLog.php +++ b/app/Model/AccessLog.php @@ -57,6 +57,9 @@ class AccessLog extends AppModel $result['AccessLog']['request'] = false; } } + if (!empty($result['AccessLog']['query_log'])) { + $result['AccessLog']['query_log'] = JsonTool::decode($this->decompress($result['AccessLog']['query_log'])); + } if (!empty($result['AccessLog']['memory_usage'])) { $result['AccessLog']['memory_usage'] = $result['AccessLog']['memory_usage'] * 1024; } @@ -90,8 +93,12 @@ class AccessLog extends AppModel $accessLog['request_method'] = $requestMethodIds[$accessLog['request_method']] ?? 0; } - if (isset($accessLog['request'])) { - $accessLog['request'] = $this->encodeRequest($accessLog['request']); + if (!empty($accessLog['request'])) { + $accessLog['request'] = $this->compress($accessLog['request']); + } + + if (!empty($accessLog['query_log'])) { + $accessLog['query_log'] = $this->compress(JsonTool::encode($accessLog['query_log'])); } // In database save size in kb to avoid overflow signed int type @@ -110,12 +117,16 @@ class AccessLog extends AppModel */ public function logRequest(array $user, $remoteIp, CakeRequest $request, $includeRequestBody = true) { - $requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true); - $now = DateTime::createFromFormat('U.u', $requestTime); + $requestTime = $this->requestTime(); $logClientIp = Configure::read('MISP.log_client_ip'); + $includeSqlQueries = Configure::read('MISP.log_paranoid_include_sql_queries'); + + if ($includeSqlQueries) { + $this->getDataSource()->fullDebug = true; // Enable SQL logging + } $dataToSave = [ - 'created' => $now->format('Y-m-d H:i:s.u'), + 'created' => $requestTime->format('Y-m-d H:i:s.u'), 'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null, 'user_id' => (int)$user['id'], 'org_id' => (int)$user['org_id'], @@ -133,14 +144,32 @@ class AccessLog extends AppModel } // Save data on shutdown - register_shutdown_function(function () use ($dataToSave, $requestTime) { + register_shutdown_function(function () use ($dataToSave, $requestTime, $includeSqlQueries) { session_write_close(); // close session to allow concurrent requests - $this->saveOnShutdown($dataToSave, $requestTime); + $this->saveOnShutdown($dataToSave, $requestTime, $includeSqlQueries); }); return true; } + /** + * @param DateTime $duration + * @return int Number of deleted entries + */ + public function deleteOldLogs(DateTime $duration) + { + $this->deleteAll([ + ['created <' => $duration->format('Y-m-d H:i:s.u')], + ], false); + + $deleted = $this->getAffectedRows(); + if ($deleted > 100) { + $dataSource = $this->getDataSource(); + $dataSource->query('OPTIMISE TABLE ' . $dataSource->name($this->table)); + } + return $deleted; + } + /** * @param CakeRequest $request * @return string @@ -161,18 +190,29 @@ class AccessLog extends AppModel /** * @param array $data - * @param float $requestTime + * @param DateTime $requestTime + * @param bool $includeSqlQueries * @return bool * @throws Exception */ - private function saveOnShutdown(array $data, $requestTime) + private function saveOnShutdown(array $data, DateTime $requestTime, $includeSqlQueries) { - $queryCount = $this->getDataSource()->getLog(false, false)['count']; + $sqlLog = $this->getDataSource()->getLog(false, false); + $queryCount = $sqlLog['count']; + + if ($includeSqlQueries && !empty($sqlLog['log'])) { + foreach ($sqlLog['log'] as &$log) { + $log['query'] = $this->escapeNonUnicode($log['query']); + unset($log['affected']); // affected is the same as numRows + unset($log['params']); // no need to save for your use case + } + $data['query_log'] = ['time' => $sqlLog['time'], 'log' => $sqlLog['log']]; + } $data['response_code'] = http_response_code(); $data['memory_usage'] = memory_get_peak_usage(); $data['query_count'] = $queryCount; - $data['duration'] = (int)((microtime(true) - $requestTime) * 1000); // in milliseconds + $data['duration'] = (int)((microtime(true) - $requestTime->format('U.u')) * 1000); // in milliseconds try { return $this->save($data, ['atomic' => false]); @@ -196,23 +236,31 @@ class AccessLog extends AppModel // In future add support for sending logs to elastic } + /** + * @return DateTime + */ + private function requestTime() + { + $requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true); + $requestTime = (string) $requestTime; + // Fix string if float value doesnt contain decimal part + if (strpos($requestTime, '.') === false) { + $requestTime .= '.0'; + } + return DateTime::createFromFormat('U.u', $requestTime); + } + /** * @param string $request - * @return array|bool + * @return array|false */ private function decodeRequest($request) { - $header = substr($request, 0, 4); - if ($header === self::BROTLI_HEADER) { - if (function_exists('brotli_uncompress')) { - $request = brotli_uncompress(substr($request, 4)); - if ($request === false) { - return false; - } - } else { - return false; - } + $request = $this->decompress($request); + if ($request === false) { + return false; } + list($contentType, $encoding, $data) = explode("\n", $request, 3); if ($encoding === 'gzip') { @@ -229,17 +277,69 @@ class AccessLog extends AppModel } /** - * @param string $request + * @param string $data + * @return false|string + */ + private function decompress($data) + { + $header = substr($data, 0, 4); + if ($header === self::BROTLI_HEADER) { + if (function_exists('brotli_uncompress')) { + $data = brotli_uncompress(substr($data, 4)); + if ($data === false) { + return false; + } + } else { + return false; + } + } + return $data; + } + + /** + * @param string $data * @return string */ - private function encodeRequest($request) + private function compress($data) { $compressionEnabled = Configure::read('MISP.log_new_audit_compress') && function_exists('brotli_compress'); - if ($compressionEnabled && strlen($request) >= self::COMPRESS_MIN_LENGTH) { - return self::BROTLI_HEADER . brotli_compress($request, 4, BROTLI_TEXT); + if ($compressionEnabled && strlen($data) >= self::COMPRESS_MIN_LENGTH) { + return self::BROTLI_HEADER . brotli_compress($data, 4, BROTLI_TEXT); } - return $request; + return $data; + } + + /** + * @param $string + * @return string + */ + private function escapeNonUnicode($string) + { + if (json_encode($string, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS) !== false) { + return $string; // string is valid unicode + } + + if (function_exists('mb_str_split')) { + $result = mb_str_split($string); + } else { + $result = []; + $length = mb_strlen($string); + for ($i = 0; $i < $length; $i++) { + $result[] = mb_substr($string, $i, 1); + } + } + + $string = ''; + foreach ($result as $char) { + if (strlen($char) === 1 && !preg_match('/[[:print:]]/', $char)) { + $string .= '\x' . bin2hex($char); + } else { + $string .= $char; + } + } + + return $string; } } \ No newline at end of file diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index 2557e9453..52b8ba552 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -83,7 +83,8 @@ class AppModel extends Model 81 => false, 82 => false, 83 => false, 84 => false, 85 => false, 86 => false, 87 => false, 88 => false, 89 => false, 90 => false, 91 => false, 92 => false, 93 => false, 94 => false, 95 => true, 96 => false, 97 => true, 98 => false, - 99 => false, 100 => false, 101 => false + 99 => false, 100 => false, 101 => false, 102 => false, 103 => false, 104 => false, + 105 => false ); const ADVANCED_UPDATES_DESCRIPTION = array( @@ -1920,6 +1921,26 @@ class AppModel extends Model INDEX `baseurl` (`baseurl`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; break; + case 102: + $sqlArray[] = "UPDATE roles SET perm_audit = 1;"; + break; + case 103: + $sqlArray[] = "ALTER TABLE `taxonomies` ADD `highlighted` tinyint(1) DEFAULT 0;"; + break; + case 104: + $sqlArray[] = "ALTER TABLE `access_logs` ADD `query_log` blob DEFAULT NULL"; + break; + case 105: + // set a default role if there is none + if (!$this->AdminSetting->getSetting('default_role')) { + $role = ClassRegistry::init('Role')->findByName('User'); + if ($role) { + $sqlArray[] = "INSERT INTO `admin_settings` (setting, value) VALUES ('default_role', '".$role['Role']['id']."');"; + } else { + // there is no role called User, do nothing + } + } + break; case 'fixNonEmptySharingGroupID': $sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; $sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; @@ -3050,6 +3071,16 @@ class AppModel extends Model return [$subQuery]; } + /** + * Returns estimated number of table rows + * @return int + */ + public function tableRows() + { + $rows = $this->query("SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->table}';"); + return $rows[0]['TABLES']['TABLE_ROWS']; + } + // start a benchmark run for the given bench name public function benchmarkInit($name = 'default') { @@ -3912,8 +3943,10 @@ class AppModel extends Model */ public function _remoteIp() { - $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; - return isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : $_SERVER['REMOTE_ADDR']; + $ipHeader = Configure::read('MISP.log_client_ip_header') ?: null; + if ($ipHeader && isset($_SERVER[$ipHeader])) { + return trim($_SERVER[$ipHeader]); + } + return $_SERVER['REMOTE_ADDR'] ?? null; } - } diff --git a/app/Model/Attribute.php b/app/Model/Attribute.php index 9d146e8c0..b5dbb3713 100644 --- a/app/Model/Attribute.php +++ b/app/Model/Attribute.php @@ -3233,20 +3233,20 @@ class Attribute extends AppModel private function generateTypeDefinitions() { return array( - 'md5' => array('desc' => __('A checksum in md5 format'), 'formdesc' => __("You are encouraged to use filename|md5 instead. A checksum in md5 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha1' => array('desc' => __('A checksum in sha1 format'), 'formdesc' => __("You are encouraged to use filename|sha1 instead. A checksum in sha1 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha256' => array('desc' => __('A checksum in sha256 format'), 'formdesc' => __("You are encouraged to use filename|sha256 instead. A checksum in sha256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'md5' => array('desc' => __('A checksum in MD5 format'), 'formdesc' => __("You are encouraged to use filename|md5 instead. A checksum in md5 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha1' => array('desc' => __('A checksum in SHA1 format'), 'formdesc' => __("You are encouraged to use filename|sha1 instead. A checksum in sha1 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha256' => array('desc' => __('A checksum in SHA256 format'), 'formdesc' => __("You are encouraged to use filename|sha256 instead. A checksum in sha256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename' => array('desc' => __('Filename'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'pdb' => array('desc' => __('Microsoft Program database (PDB) path information'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), - 'filename|md5' => array('desc' => __('A filename and an md5 hash separated by a |'), 'formdesc' => __("A filename and an md5 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha1' => array('desc' => __('A filename and an sha1 hash separated by a |'), 'formdesc' => __("A filename and an sha1 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha256' => array('desc' => __('A filename and an sha256 hash separated by a |'), 'formdesc' => __("A filename and an sha256 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|md5' => array('desc' => __('A filename and an MD5 hash separated by a |'), 'formdesc' => __("A filename and an md5 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha1' => array('desc' => __('A filename and an SHA1 hash separated by a |'), 'formdesc' => __("A filename and an sha1 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha256' => array('desc' => __('A filename and an SHA256 hash separated by a |'), 'formdesc' => __("A filename and an sha256 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'ip-src' => array('desc' => __("A source IP address of the attacker"), 'default_category' => 'Network activity', 'to_ids' => 1), 'ip-dst' => array('desc' => __('A destination IP address of the attacker or C&C server'), 'formdesc' => __("A destination IP address of the attacker or C&C server. Also set the IDS flag on when this IP is hardcoded in malware"), 'default_category' => 'Network activity', 'to_ids' => 1), 'hostname' => array('desc' => __('A full host/dnsname of an attacker'), 'formdesc' => __("A full host/dnsname of an attacker. Also set the IDS flag on when this hostname is hardcoded in malware"), 'default_category' => 'Network activity', 'to_ids' => 1), 'domain' => array('desc' => __('A domain name used in the malware'), 'formdesc' => __("A domain name used in the malware. Use this instead of hostname when the upper domain is important or can be used to create links between events."), 'default_category' => 'Network activity', 'to_ids' => 1), 'domain|ip' => array('desc' => __('A domain name and its IP address (as found in DNS lookup) separated by a |'),'formdesc' => __("A domain name and its IP address (as found in DNS lookup) separated by a | (no spaces)"), 'default_category' => 'Network activity', 'to_ids' => 1), - 'email' => array('desc' => ('An e-mail address'), 'default_category' => 'Social network', 'to_ids' => 1), + 'email' => array('desc' => ('An email address'), 'default_category' => 'Social network', 'to_ids' => 1), 'email-src' => array('desc' => __("The source email address. Used to describe the sender when describing an e-mail."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'eppn' => array('desc' => __("eduPersonPrincipalName - eppn - the NetId of the person for the purposes of inter-institutional authentication. Should be stored in the form of user@univ.edu, where univ.edu is the name of the local security domain."), 'default_category' => 'Network activity', 'to_ids' => 1), 'email-dst' => array('desc' => __("The destination email address. Used to describe the recipient when describing an e-mail."), 'default_category' => 'Network activity', 'to_ids' => 1), @@ -3254,8 +3254,8 @@ class Attribute extends AppModel 'email-attachment' => array('desc' => __("File name of the email attachment."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'email-body' => array('desc' => __('Email body'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'float' => array('desc' => __("A floating point value."), 'default_category' => 'Other', 'to_ids' => 0), - 'git-commit-id' => array('desc' => __("A git commit ID."), 'default_category' => 'Internal reference', 'to_ids' => 0), - 'url' => array('desc' => __('url'), 'default_category' => 'Network activity', 'to_ids' => 1), + 'git-commit-id' => array('desc' => __("A Git commit ID."), 'default_category' => 'Internal reference', 'to_ids' => 0), + 'url' => array('desc' => __('Uniform Resource Locator'), 'default_category' => 'Network activity', 'to_ids' => 1), 'http-method' => array('desc' => __("HTTP method used by the malware (e.g. POST, GET, ...)."), 'default_category' => 'Network activity', 'to_ids' => 0), 'user-agent' => array('desc' => __("The user-agent used by the malware in the HTTP request."), 'default_category' => 'Network activity', 'to_ids' => 0), 'ja3-fingerprint-md5' => array('desc' => __("JA3 is a method for creating SSL/TLS client fingerprints that should be easy to produce on any platform and can be easily shared for threat intelligence."), 'default_category' => 'Network activity', 'to_ids' => 1), @@ -3269,7 +3269,7 @@ class Attribute extends AppModel 'snort' => array('desc' => __('An IDS rule in Snort rule-format'), 'formdesc' => __("An IDS rule in Snort rule-format. This rule will be automatically rewritten in the NIDS exports."), 'default_category' => 'Network activity', 'to_ids' => 1), 'bro' => array('desc' => __('An NIDS rule in the Bro rule-format'), 'formdesc' => __("An NIDS rule in the Bro rule-format."), 'default_category' => 'Network activity', 'to_ids' => 1), 'zeek' => array('desc' => __('An NIDS rule in the Zeek rule-format'), 'formdesc' => __("An NIDS rule in the Zeek rule-format."), 'default_category' => 'Network activity', 'to_ids' => 1), - 'community-id' => array('desc' => __('a community ID flow hashing algorithm to map multiple traffic monitors into common flow id'), 'formdesc' => __("a community ID flow hashing algorithm to map multiple traffic monitors into common flow id"), 'default_category' => 'Network activity', 'to_ids' => 1), + 'community-id' => array('desc' => __('A community ID flow hashing algorithm to map multiple traffic monitors into common flow id'), 'formdesc' => __("a community ID flow hashing algorithm to map multiple traffic monitors into common flow id"), 'default_category' => 'Network activity', 'to_ids' => 1), 'pattern-in-file' => array('desc' => __('Pattern in file that identifies the malware'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'pattern-in-traffic' => array('desc' => __('Pattern in network traffic that identifies the malware'), 'default_category' => 'Network activity', 'to_ids' => 1), 'pattern-in-memory' => array('desc' => __('Pattern in memory dump that identifies the malware'), 'default_category' => 'Payload installation', 'to_ids' => 1), @@ -3277,7 +3277,7 @@ class Attribute extends AppModel 'pgp-public-key' => array('desc' => __('A PGP public key'), 'default_category' => 'Person', 'to_ids' => 0), 'pgp-private-key' => array('desc' => __('A PGP private key'), 'default_category' => 'Person', 'to_ids' => 0), 'ssh-fingerprint' => array('desc' => __('A fingerprint of SSH key material'), 'default_category' => 'Network activity', 'to_ids' => 0), - 'yara' => array('desc' => __('Yara signature'), 'default_category' => 'Payload installation', 'to_ids' => 1), + 'yara' => array('desc' => __('YARA signature'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'stix2-pattern' => array('desc' => __('STIX 2 pattern'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'sigma' => array('desc' => __('Sigma - Generic Signature Format for SIEM Systems'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'gene' => array('desc' => __('GENE - Go Evtx sigNature Engine'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), @@ -3287,7 +3287,7 @@ class Attribute extends AppModel 'cookie' => array('desc' => __('HTTP cookie as often stored on the user web client. This can include authentication cookie or session cookie.'), 'default_category' => 'Network activity', 'to_ids' => 0), 'vulnerability' => array('desc' => __('A reference to the vulnerability used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), 'cpe' => array('desc' => __('Common Platform Enumeration - structured naming scheme for information technology systems, software, and packages.'), 'default_category' => 'External analysis', 'to_ids' => 0), - 'weakness' => array('desc'=> __('A reference to the weakness used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), + 'weakness' => array('desc'=> __('A reference to the weakness (CWE) used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), 'attachment' => array('desc' => __('Attachment with external information'), 'formdesc' => __("Please upload files using the Upload Attachment button."), 'default_category' => 'External analysis', 'to_ids' => 0), 'malware-sample' => array('desc' => __('Attachment containing encrypted malware sample'), 'formdesc' => __("Please upload files using the Upload Attachment button."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'link' => array('desc' => __('Link to an external information'), 'default_category' => 'External analysis', 'to_ids' => 0), @@ -3325,34 +3325,34 @@ class Attribute extends AppModel 'ssdeep' => array('desc' => __('A checksum in ssdeep format'), 'formdesc' => __("You are encouraged to use filename|ssdeep instead. A checksum in the SSDeep format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'imphash' => array('desc' => __('Import hash - a hash created based on the imports in the sample.'), 'formdesc' => __("You are encouraged to use filename|imphash instead. A hash created based on the imports in the sample, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'telfhash' => array('desc' => __('telfhash is symbol hash for ELF files, just like imphash is imports hash for PE files.'), 'formdesc' => __("You are encouraged to use a file object with telfash"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'pehash' => array('desc' => __('PEhash - a hash calculated based of certain pieces of a PE executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'pehash' => array('desc' => __('peHash - a hash calculated based of certain pieces of a PE executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'impfuzzy' => array('desc' => __('A fuzzy hash of import table of Portable Executable format'), 'formdesc' => __("You are encouraged to use filename|impfuzzy instead. A fuzzy hash created based on the imports in the sample, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha224' => array('desc' => __('A checksum in sha-224 format'), 'formdesc' => __("You are encouraged to use filename|sha224 instead. A checksum in sha224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha384' => array('desc' => __('A checksum in sha-384 format'), 'formdesc' => __("You are encouraged to use filename|sha384 instead. A checksum in sha384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512' => array('desc' => __('A checksum in sha-512 format'), 'formdesc' => __("You are encouraged to use filename|sha512 instead. A checksum in sha512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512/224' => array('desc' => __('A checksum in the sha-512/224 format'), 'formdesc' => __("You are encouraged to use filename|sha512/224 instead. A checksum in sha512/224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512/256' => array('desc' => __('A checksum in the sha-512/256 format'), 'formdesc' => __("You are encouraged to use filename|sha512/256 instead. A checksum in sha512/256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-224' => array('desc' => __('A checksum in sha3-224 format'), 'formdesc' => __("You are encouraged to use filename|sha3-224 instead. A checksum in sha3-224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-256' => array('desc' => __('A checksum in sha3-256 format'), 'formdesc' => __("You are encouraged to use filename|sha3-256 instead. A checksum in sha3-256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-384' => array('desc' => __('A checksum in sha3-384 format'), 'formdesc' => __("You are encouraged to use filename|sha3-384 instead. A checksum in sha3-384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-512' => array('desc' => __('A checksum in sha3-512 format'), 'formdesc' => __("You are encouraged to use filename|sha3-512 instead. A checksum in sha3-512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha224' => array('desc' => __('A checksum in SHA-224 format'), 'formdesc' => __("You are encouraged to use filename|sha224 instead. A checksum in sha224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha384' => array('desc' => __('A checksum in SHA-384 format'), 'formdesc' => __("You are encouraged to use filename|sha384 instead. A checksum in sha384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512' => array('desc' => __('A checksum in SHA-512 format'), 'formdesc' => __("You are encouraged to use filename|sha512 instead. A checksum in sha512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512/224' => array('desc' => __('A checksum in the SHA-512/224 format'), 'formdesc' => __("You are encouraged to use filename|sha512/224 instead. A checksum in sha512/224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512/256' => array('desc' => __('A checksum in the SHA-512/256 format'), 'formdesc' => __("You are encouraged to use filename|sha512/256 instead. A checksum in sha512/256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-224' => array('desc' => __('A checksum in SHA3-224 format'), 'formdesc' => __("You are encouraged to use filename|sha3-224 instead. A checksum in sha3-224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-256' => array('desc' => __('A checksum in SHA3-256 format'), 'formdesc' => __("You are encouraged to use filename|sha3-256 instead. A checksum in sha3-256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-384' => array('desc' => __('A checksum in SHA3-384 format'), 'formdesc' => __("You are encouraged to use filename|sha3-384 instead. A checksum in sha3-384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-512' => array('desc' => __('A checksum in SHA3-512 format'), 'formdesc' => __("You are encouraged to use filename|sha3-512 instead. A checksum in sha3-512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'tlsh' => array('desc' => __('A checksum in the Trend Micro Locality Sensitive Hash format'), 'formdesc' => __("You are encouraged to use filename|tlsh instead. A checksum in the Trend Micro Locality Sensitive Hash format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'cdhash' => array('desc' => __('An Apple Code Directory Hash, identifying a code-signed Mach-O executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|authentihash' => array('desc' => __('A checksum in md5 format'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|authentihash' => array('desc' => __('A filename and Authenticode executable signature hash'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|vhash' => array('desc' => __('A filename and a VirusTotal hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|ssdeep' => array('desc' => __('A checksum in ssdeep format'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|imphash' => array('desc' => __('Import hash - a hash created based on the imports in the sample.'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|impfuzzy' => array('desc' => __('Import fuzzy hash - a fuzzy hash created based on the imports in the sample.'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|pehash' => array('desc' => __('A filename and a PEhash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha224' => array('desc' => __('A filename and a sha-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha384' => array('desc' => __('A filename and a sha-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512' => array('desc' => __('A filename and a sha-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512/224' => array('desc' => __('A filename and a sha-512/224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512/256' => array('desc' => __('A filename and a sha-512/256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-224' => array('desc' => __('A filename and an sha3-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-256' => array('desc' => __('A filename and an sha3-256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-384' => array('desc' => __('A filename and an sha3-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-512' => array('desc' => __('A filename and an sha3-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|pehash' => array('desc' => __('A filename and a peHash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha224' => array('desc' => __('A filename and a SHA-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha384' => array('desc' => __('A filename and a SHA-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512' => array('desc' => __('A filename and a SHA-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512/224' => array('desc' => __('A filename and a SHa-512/224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512/256' => array('desc' => __('A filename and a SHA-512/256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-224' => array('desc' => __('A filename and an SHA3-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-256' => array('desc' => __('A filename and an SHA3-256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-384' => array('desc' => __('A filename and an SHA3-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-512' => array('desc' => __('A filename and an SHA3-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|tlsh' => array('desc' => __('A filename and a Trend Micro Locality Sensitive Hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'windows-scheduled-task' => array('desc' => __('A scheduled task in windows'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), 'windows-service-name' => array('desc' => __('A windows service name. This is the name used internally by windows. Not to be confused with the windows-service-displayname.'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), @@ -3370,7 +3370,7 @@ class Attribute extends AppModel 'x509-fingerprint-sha1' => array('desc' => __('X509 fingerprint in SHA-1 format'), 'default_category' => 'Network activity', 'to_ids' => 1), 'x509-fingerprint-md5' => array('desc' => __('X509 fingerprint in MD5 format'), 'default_category' => 'Network activity', 'to_ids' => 1), 'x509-fingerprint-sha256' => array('desc' => __('X509 fingerprint in SHA-256 format'), 'default_category' => 'Network activity', 'to_ids' => 1), - 'dns-soa-email' => array('desc' => __('RFC1035 mandates that DNS zones should have a SOA (Statement Of Authority) record that contains an email address where a PoC for the domain could be contacted. This can sometimes be used for attribution/linkage between different domains even if protected by whois privacy'), 'default_category' => 'Attribution', 'to_ids' => 0), + 'dns-soa-email' => array('desc' => __('RFC 1035 mandates that DNS zones should have a SOA (Statement Of Authority) record that contains an email address where a PoC for the domain could be contacted. This can sometimes be used for attribution/linkage between different domains even if protected by whois privacy'), 'default_category' => 'Attribution', 'to_ids' => 0), 'size-in-bytes' => array('desc' => __('Size expressed in bytes'), 'default_category' => 'Other', 'to_ids' => 0), 'counter' => array('desc' => __('An integer counter, generally to be used in objects'), 'default_category' => 'Other', 'to_ids' => 0), 'datetime' => array('desc' => __('Datetime in the ISO 8601 format'), 'default_category' => 'Other', 'to_ids' => 0), @@ -3378,8 +3378,8 @@ class Attribute extends AppModel 'ip-dst|port' => array('desc' => __('IP destination and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), 'ip-src|port' => array('desc' => __('IP source and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), 'hostname|port' => array('desc' => __('Hostname and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), - 'mac-address' => array('desc' => __('Mac address'), 'default_category' => 'Network activity', 'to_ids' => 0), - 'mac-eui-64' => array('desc' => __('Mac EUI-64 address'), 'default_category' => 'Network activity', 'to_ids' => 0), + 'mac-address' => array('desc' => __('MAC address'), 'default_category' => 'Network activity', 'to_ids' => 0), + 'mac-eui-64' => array('desc' => __('MAC EUI-64 address'), 'default_category' => 'Network activity', 'to_ids' => 0), // verify IDS flag defaults for these 'email-dst-display-name' => array('desc' => __('Email destination display name'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-src-display-name' => array('desc' => __('Email source display name'), 'default_category' => 'Payload delivery', 'to_ids' => 0), @@ -3389,9 +3389,9 @@ class Attribute extends AppModel 'email-mime-boundary' => array('desc' => __('The email mime boundary separating parts in a multipart email'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-thread-index' => array('desc' => __('The email thread index header'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-message-id' => array('desc' => __('The email message ID'), 'default_category' => 'Payload delivery', 'to_ids' => 0), - 'github-username' => array('desc' => __('A github user name'), 'default_category' => 'Social network', 'to_ids' => 0), - 'github-repository' => array('desc' => __('A github repository'), 'default_category' => 'Social network', 'to_ids' => 0), - 'github-organisation' => array('desc' => __('A github organisation'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-username' => array('desc' => __('A GitHub user name'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-repository' => array('desc' => __('A Github repository'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-organisation' => array('desc' => __('A GitHub organisation'), 'default_category' => 'Social network', 'to_ids' => 0), 'jabber-id' => array('desc' => __('Jabber ID'), 'default_category' => 'Social network', 'to_ids' => 0), 'twitter-id' => array('desc' => __('Twitter ID'), 'default_category' => 'Social network', 'to_ids' => 0), 'dkim' => array('desc' => __('DKIM public key'), 'default_category' => 'Network activity', 'to_ids' => 0), diff --git a/app/Model/Bruteforce.php b/app/Model/Bruteforce.php index 29507c1cd..46c8342f7 100644 --- a/app/Model/Bruteforce.php +++ b/app/Model/Bruteforce.php @@ -12,25 +12,43 @@ class Bruteforce extends AppModel $ip = $this->_remoteIp(); $expire = Configure::check('SecureAuth.expire') ? Configure::read('SecureAuth.expire') : 300; $amount = Configure::check('SecureAuth.amount') ? Configure::read('SecureAuth.amount') : 5; - $expire = time() + $expire; - $expire = date('Y-m-d H:i:s', $expire); + $expireTime = time() + $expire; + $expireTime = date('Y-m-d H:i:s', $expireTime); $bruteforceEntry = array( 'ip' => $ip, 'username' => trim(strtolower($username)), - 'expire' => $expire + 'expire' => $expireTime ); $this->save($bruteforceEntry); $title = 'Failed login attempt using username ' . $username . ' from IP: ' . $ip . '.'; if ($this->isBlocklisted($username)) { - $title .= 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The user is now blocklisted for ' . $expire . ' seconds.'; + $change = 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The source IP/username is now blocklisted for ' . $expire . ' seconds.'; + } else { + $change = ''; } + // lookup the real user details + $this->User = ClassRegistry::init('User'); + $user = $this->User->find('first', array( + 'conditions' => array('User.email' => $username), + 'fields' => array('User.id', 'Organisation.name'), + 'recursive' => 0)); + if ($user) { + $org = $user['Organisation']['name']; + $userId = $user['User']['id']; + } else { + $org = 'SYSTEM'; + $userId = 0; + } + $log = array( - 'org' => 'SYSTEM', + 'org' => $org, 'model' => 'User', - 'model_id' => 0, + 'model_id' => $userId, 'email' => $username, + 'user_id' => $userId, 'action' => 'login_fail', - 'title' => $title + 'title' => $title, + 'change' => $change ); $this->Log->save($log); } diff --git a/app/Model/Community.php b/app/Model/Community.php index 60429a0b7..870657685 100644 --- a/app/Model/Community.php +++ b/app/Model/Community.php @@ -4,37 +4,20 @@ class Community extends AppModel { public $useTable = false; - public $recursive = -1; - - public $actsAs = array( - 'Containable', - ); - - public $validate = array( - ); - - public function beforeValidate($options = array()) - { - parent::beforeValidate(); - return true; - } - + /** + * @param string $context + * @param string|null $value + * @return array + */ public function getCommunityList($context, $value) { - $community_file = new File(APP . 'files/community-metadata/defaults.json'); - if (!$community_file->exists()) { - throw new NotFoundException(__('Default community list not found.')); - } - $community_list = $community_file->read(); - if (empty($community_list)) { - throw new NotFoundException(__('Default community list empty.')); - } try { - $community_list = json_decode($community_list, true); + $community_list = FileAccessTool::readJsonFromFile(APP . 'files/community-metadata/defaults.json'); } catch (Exception $e) { throw new NotFoundException(__('Default community list not in the expected format.')); } - $fieldsToCheck = array('name', 'uuid', 'description', 'url', 'sector', 'nationality', 'type', 'org_uuid', 'org_name'); + + $fieldsToCheck = ['name', 'uuid', 'description', 'url', 'sector', 'nationality', 'type', 'org_uuid', 'org_name']; foreach ($community_list as $k => $v) { if ($v['misp_project_vetted'] === ($context === 'vetted')) { $community_list[$k]['id'] = $k + 1; @@ -44,11 +27,12 @@ class Community extends AppModel continue; } if (!empty($value)) { + $value = mb_strtolower($value); $found = false; foreach ($fieldsToCheck as $field) { - if (strpos(strtolower($v[$field]), $value) !== false) { + if (strpos(mb_strtolower($v[$field]), $value) !== false) { $found = true; - continue; + break; } } if (!$found) { @@ -56,42 +40,32 @@ class Community extends AppModel } } } - $community_list = array_values($community_list); - return $community_list; + return array_values($community_list); } + /** + * @param int|string $id Community ID or UUID + * @return array + */ public function getCommunity($id) { - $community_file = new File(APP . 'files/community-metadata/defaults.json'); - if (!$community_file->exists()) { - throw new NotFoundException(__('Default community list not found.')); - } - $community_list = $community_file->read(); - if (empty($community_list)) { - throw new NotFoundException(__('Default community list empty.')); - } try { - $community_list = json_decode($community_list, true); + $community_list = FileAccessTool::readJsonFromFile(APP . 'files/community-metadata/defaults.json'); } catch (Exception $e) { throw new NotFoundException(__('Default community list not in the expected format.')); } + foreach ($community_list as $k => $v) { $community_list[$k]['id'] = $k + 1; $community_list[$k]['Org'] = array('uuid' => $v['org_uuid'], 'name' => $v['org_name']); } - $community = false; - $lookupField = 'id'; - if (Validation::uuid($id)) { - $lookupField = 'uuid'; - } + + $lookupField = Validation::uuid($id) ? 'uuid' : 'id'; foreach ($community_list as $s) { - if ($s[$lookupField === 'uuid' ? 'uuid' : 'id'] === $id) { - $community = $s; + if ($s[$lookupField === 'uuid' ? 'uuid' : 'id'] == $id) { + return $s; } } - if (empty($community)) { - throw new NotFoundException(__('Community not found.')); - } - return $community; + throw new NotFoundException(__('Community not found.')); } } diff --git a/app/Model/Dashboard.php b/app/Model/Dashboard.php index c74698530..0503b2bd1 100644 --- a/app/Model/Dashboard.php +++ b/app/Model/Dashboard.php @@ -97,6 +97,7 @@ class Dashboard extends AppModel } } } + ksort($widgets); return $widgets; } diff --git a/app/Model/DecayingModelsFormulas/PolynomialExtended.php b/app/Model/DecayingModelsFormulas/PolynomialExtended.php index 4fa8ecfbb..71c98a9a3 100644 --- a/app/Model/DecayingModelsFormulas/PolynomialExtended.php +++ b/app/Model/DecayingModelsFormulas/PolynomialExtended.php @@ -18,7 +18,7 @@ class PolynomialExtended extends Polynomial } else { $retention_taxonomy_id = $retention_taxonomy_id['Taxonomy']['id']; } - $taxonomy = $this->Taxonomy->getTaxonomy($retention_taxonomy_id, array('full' => true)); + $taxonomy = $this->Taxonomy->getTaxonomy($retention_taxonomy_id); $this->retention_taxonomy = array(); foreach ($taxonomy['entries'] as $k => $entry) { $this->retention_taxonomy[$entry['tag']] = $entry['numerical_value']; diff --git a/app/Model/Event.php b/app/Model/Event.php index 17a31e004..98e6166f8 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -5594,6 +5594,10 @@ class Event extends AppModel return $resultArray; } + /** + * @param array $result + * @return array + */ public function handleMispFormatFromModuleResult(&$result) { $defaultDistribution = $this->Attribute->defaultDistribution(); @@ -5607,7 +5611,7 @@ class Event extends AppModel $event['Attribute'] = $attributes; } if (!empty($result['results']['Object'])) { - $object = array(); + $objects = array(); foreach ($result['results']['Object'] as $tmp_object) { $tmp_object['distribution'] = (isset($tmp_object['distribution']) ? (int)$tmp_object['distribution'] : $defaultDistribution); $tmp_object['sharing_group_id'] = (isset($tmp_object['sharing_group_id']) ? (int)$tmp_object['sharing_group_id'] : 0); @@ -5631,6 +5635,11 @@ class Event extends AppModel return $event; } + /** + * @param array $attribute + * @param int $defaultDistribution + * @return array + */ private function __fillAttribute($attribute, $defaultDistribution) { if (is_array($attribute['type'])) { @@ -6327,11 +6336,33 @@ class Event extends AppModel return $message; } - public function processModuleResultsData($user, $resolved_data, $id, $default_comment = '', $jobId = false, $adhereToWarninglists = false, $event_level = false) + /** + * @param array $user + * @param array $resolved_data + * @param int $id + * @param string $default_comment + * @param int|false $jobId + * @param bool $adhereToWarninglists + * @param bool $event_level + * @return int|string + * @throws JsonException + */ + public function processModuleResultsData(array $user, $resolved_data, $id, $default_comment = '', $jobId = false, $adhereToWarninglists = false, $event_level = false) { + $event = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['id' => $id], + ]); + if (empty($event)) { + throw new Exception("Event with ID `$id` not found."); + } if ($jobId) { $this->Job = ClassRegistry::init('Job'); $this->Job->id = $jobId; + + /** @var EventLock $eventLock */ + $eventLock = ClassRegistry::init('EventLock'); + $eventLock->insertLockBackgroundJob($event['Event']['id'], $jobId); } $failed_attributes = $failed_objects = $failed_object_attributes = $failed_reports = 0; $saved_attributes = $saved_objects = $saved_object_attributes = $saved_reports = 0; @@ -6377,6 +6408,7 @@ class Event extends AppModel } } } else { + $this->Attribute->logDropped($user, $attribute); $failed_attributes++; $lastAttributeError = $this->Attribute->validationErrors; $original_uuid = $this->__findOriginalUUID( @@ -6392,8 +6424,7 @@ class Event extends AppModel } if ($jobId) { $processedAttributes++; - $this->Job->saveField('message', 'Attribute ' . $processedAttributes . '/' . $total_attributes); - $this->Job->saveField('progress', ($processedAttributes * 100 / $items_count)); + $this->Job->saveProgress($jobId, "Attribute $processedAttributes/$total_attributes", $processedAttributes * 100 / $items_count); } } } else { @@ -6438,7 +6469,7 @@ class Event extends AppModel if (isset($initial_attributes[$object_relation]) && in_array($object_attribute['value'], $initial_attributes[$object_relation])) { continue; } - if ($this->__saveObjectAttribute($object_attribute, $default_comment, $id, $initial_object_id, $user)) { + if ($this->__saveObjectAttribute($object_attribute, null, $event, $initial_object_id, $user)) { $saved_object_attributes++; } else { $failed_object_attributes++; @@ -6471,7 +6502,7 @@ class Event extends AppModel if ($this->Object->save($object)) { $object_id = $this->Object->id; foreach ($object['Attribute'] as $object_attribute) { - if ($this->__saveObjectAttribute($object_attribute, $default_comment, $id, $object_id, $user)) { + if ($this->__saveObjectAttribute($object_attribute, null, $event, $object_id, $user)) { $saved_object_attributes++; } else { $failed_object_attributes++; @@ -6506,8 +6537,7 @@ class Event extends AppModel } if ($jobId) { $processedObjects++; - $this->Job->saveField('message', 'Object ' . $processedObjects . '/' . $total_objects); - $this->Job->saveField('progress', (($processedObjects + $total_attributes) * 100 / $items_count)); + $this->Job->saveProgress($jobId, "Object $processedObjects/$total_objects", ($processedObjects + $total_attributes) * 100 / $items_count); } } @@ -6574,14 +6604,13 @@ class Event extends AppModel } if ($jobId) { $current = ($i + 1); - $this->Job->saveField('message', 'EventReport ' . $current . '/' . $total_reports); - $this->Job->saveField('progress', ($current * 100 / $items_count)); + $this->Job->saveProgress($jobId, "EventReport $current/$total_reports", $current * 100 / $items_count); } } } if ($saved_attributes > 0 || $saved_objects > 0 || $saved_reports > 0) { - $this->unpublishEvent($id); + $this->unpublishEvent($event); } if ($event_level) { return $saved_attributes + $saved_object_attributes + $saved_reports; @@ -6642,8 +6671,8 @@ class Event extends AppModel $message .= $failed_reports . $reason; } if ($jobId) { - $this->Job->saveField('message', 'Processing complete. ' . $message); - $this->Job->saveField('progress', 100); + $this->Job->saveStatus($jobId, true, 'Processing complete. ' . $message); + $eventLock->deleteBackgroundJobLock($event['Event']['id'], $jobId); } return $message; } @@ -6730,28 +6759,39 @@ class Event extends AppModel return (!empty($original_uuid)) ? $original_uuid['Object']['uuid'] : $original_uuid; } - private function __saveObjectAttribute($attribute, $default_comment, $event_id, $object_id, $user) + /** + * @param array $attribute + * @param string|null $default_comment + * @param array $event + * @param int $object_id + * @param array $user + * @return array|bool|mixed + * @throws Exception + */ + private function __saveObjectAttribute(array $attribute, $default_comment, array $event, $object_id, array $user) { $attribute['object_id'] = $object_id; - $attribute['event_id'] = $event_id; - if (empty($attribute['comment'])) { + $attribute['event_id'] = $event['Event']['id']; + if (empty($attribute['comment']) && $default_comment) { $attribute['comment'] = $default_comment; } if (!empty($attribute['data']) && !empty($attribute['encrypt'])) { $attribute = $this->Attribute->onDemandEncrypt($attribute); } $this->Attribute->create(); - $attribute_save = $this->Attribute->save($attribute); + $attribute_save = $this->Attribute->save($attribute, ['parentEvent' => $event]); if ($attribute_save) { if (!empty($attribute['Tag'])) { foreach ($attribute['Tag'] as $tag) { $tag_id = $this->Attribute->AttributeTag->Tag->captureTag($tag, $user); $relationship_type = empty($tag['relationship_type']) ? false : $tag['relationship_type']; if ($tag_id) { - $this->Attribute->AttributeTag->attachTagToAttribute($this->Attribute->id, $event_id, $tag_id, !empty($tag['local']), $relationship_type); + $this->Attribute->AttributeTag->attachTagToAttribute($this->Attribute->id, $event['Event']['id'], $tag_id, !empty($tag['local']), $relationship_type); } } } + } else { + $this->Attribute->logDropped($user, $attribute); } return $attribute_save; } diff --git a/app/Model/EventTag.php b/app/Model/EventTag.php index f60ba2574..c855de995 100644 --- a/app/Model/EventTag.php +++ b/app/Model/EventTag.php @@ -91,7 +91,7 @@ class EventTag extends AppModel /** * @param int $event_id - * @param int $tagId + * @param array $tag * @param bool $nothingToChange * @return bool * @throws Exception diff --git a/app/Model/Galaxy.php b/app/Model/Galaxy.php index 40ef276d7..d08b499ae 100644 --- a/app/Model/Galaxy.php +++ b/app/Model/Galaxy.php @@ -380,16 +380,16 @@ class Galaxy extends AppModel /** * @param array $user - * @param string $target_type + * @param string $targetType Can be 'event', 'attribute' or 'tag_collection' * @param array $target * @param int $cluster_id * @param bool $local * @return string * @throws Exception */ - public function attachCluster(array $user, $target_type, array $target, $cluster_id, $local = false) + public function attachCluster(array $user, $targetType, array $target, $cluster_id, $local = false) { - $connectorModel = Inflector::camelize($target_type) . 'Tag'; + $connectorModel = Inflector::camelize($targetType) . 'Tag'; $local = $local == 1 || $local === true ? 1 : 0; $cluster_alias = $this->GalaxyCluster->alias; $galaxy_alias = $this->alias; @@ -409,36 +409,36 @@ class Galaxy extends AppModel } $this->Tag = ClassRegistry::init('Tag'); $tag_id = $this->Tag->captureTag(array('name' => $cluster['GalaxyCluster']['tag_name'], 'colour' => '#0088cc', 'exportable' => 1, 'local_only' => $local_only), $user, true); - if ($target_type === 'event') { + if ($targetType === 'event') { $target_id = $target['Event']['id']; - } elseif ($target_type === 'attribute') { + } elseif ($targetType === 'attribute') { $target_id = $target['Attribute']['id']; } else { $target_id = $target['TagCollection']['id']; } - $existingTag = $this->Tag->$connectorModel->hasAny(array($target_type . '_id' => $target_id, 'tag_id' => $tag_id)); + $existingTag = $this->Tag->$connectorModel->hasAny(array($targetType . '_id' => $target_id, 'tag_id' => $tag_id)); if ($existingTag) { return 'Cluster already attached.'; } $this->Tag->$connectorModel->create(); - $toSave = array($target_type . '_id' => $target_id, 'tag_id' => $tag_id, 'local' => $local); - if ($target_type === 'attribute') { + $toSave = array($targetType . '_id' => $target_id, 'tag_id' => $tag_id, 'local' => $local); + if ($targetType === 'attribute') { $toSave['event_id'] = $target['Attribute']['event_id']; } $result = $this->Tag->$connectorModel->save($toSave); if ($result) { if (!$local) { - if ($target_type === 'attribute') { + if ($targetType === 'attribute') { $this->Tag->AttributeTag->Attribute->touch($target); - } elseif ($target_type === 'event') { + } elseif ($targetType === 'event') { $this->Tag->EventTag->Event->unpublishEvent($target); } } - if ($target_type === 'attribute' || $target_type === 'event') { + if ($targetType === 'attribute' || $targetType === 'event') { $this->Tag->EventTag->Event->insertLock($user, $target['Event']['id']); } - $logTitle = 'Attached ' . $cluster['GalaxyCluster']['value'] . ' (' . $cluster['GalaxyCluster']['id'] . ') to ' . $target_type . ' (' . $target_id . ')'; - $this->loadLog()->createLogEntry($user, 'galaxy', ucfirst($target_type), $target_id, $logTitle); + $logTitle = 'Attached ' . $cluster['GalaxyCluster']['value'] . ' (' . $cluster['GalaxyCluster']['id'] . ') to ' . $targetType . ' (' . $target_id . ')'; + $this->loadLog()->createLogEntry($user, 'galaxy', ucfirst($targetType), $target_id, $logTitle); return 'Cluster attached.'; } return 'Could not attach the cluster'; diff --git a/app/Model/GalaxyElement.php b/app/Model/GalaxyElement.php index 58d60e4fc..b648ce605 100644 --- a/app/Model/GalaxyElement.php +++ b/app/Model/GalaxyElement.php @@ -1,5 +1,9 @@ 'name', + 'meta-category' => 'meta-category', + 'description' => 'description', + 'template_version' => 'version', + 'template_uuid' => 'uuid' + ); + foreach ($templateFields as $objectField => $templateField) { + $object['Object'][$objectField] = $template['ObjectTemplate'][$templateField]; + } + return $object; + } + + /** + * @param array $object + * @param int $eventId + * @param array $template + * @param array $user + * @param string $errorBehaviour + * @param bool $breakOnDuplicate + * @return array|array[]|bool|int|mixed|string + * @throws Exception + */ + public function saveObject(array $object, $eventId, $template = false, array $user, $errorBehaviour = 'drop', $breakOnDuplicate = false) { $templateFields = array( 'name' => 'name', @@ -562,7 +592,7 @@ class MispObject extends AppModel // conditions // order // group - public function fetchObjects($user, $options = array()) + public function fetchObjects(array $user, array $options = array()) { $attributeConditions = array(); if (!$user['Role']['perm_site_admin']) { @@ -608,8 +638,6 @@ class MispObject extends AppModel } if (isset($options['contain'])) { $params['contain'] = array_merge_recursive($params['contain'], $options['contain']); - } else { - $option['contain']['Event']['fields'] = array('id', 'info', 'org_id', 'orgc_id'); } if ( empty($options['metadata']) && @@ -662,7 +690,6 @@ class MispObject extends AppModel if ($options['enforceWarninglist'] && !isset($this->Warninglist)) { $this->Warninglist = ClassRegistry::init('Warninglist'); } - $results = array_values($results); $proposals_block_attributes = Configure::read('MISP.proposals_block_attributes'); if (empty($options['metadata'])) { foreach ($results as $key => $object) { @@ -764,7 +791,7 @@ class MispObject extends AppModel /** * Clean the attribute list up from artifacts introduced by the object form * @param array $attributes - * @return string|array + * @return array * @throws InternalErrorException * @throws Exception */ @@ -812,6 +839,70 @@ class MispObject extends AppModel return $attributes; } + /** + * @param array $user + * @param int $eventId + * @param array $attributes + * @param array $template + * @param int $threshold + * @return array + */ + public function findSimilarObjects(array $user, $eventId, array $attributes, array $template, $threshold = 15) + { + $attributeValues = array_column($attributes, 'value'); + $conditions = array( + 'event_id' => $eventId, + 'value1' => $attributeValues, + 'object_id !=' => 0, + ); + $similarObjects = $this->Attribute->find('all', array( + 'conditions' => $conditions, + 'recursive' => -1, + 'fields' => 'object_id, count(object_id) as similarity_amount', + 'group' => 'object_id', + 'order' => 'similarity_amount DESC' + )); + + if (empty($similarObjects)) { + return [0, [], [], []]; + } + + $similar_object_ids = array(); + $similar_object_similarity_amount = array(); + foreach ($similarObjects as $obj) { + $similar_object_ids[] = $obj['Attribute']['object_id']; + $similar_object_similarity_amount[$obj['Attribute']['object_id']] = (int)$obj[0]['similarity_amount']; + } + $similar_objects_count = count($similar_object_ids); + $similar_object_ids = array_slice($similar_object_ids, 0, $threshold); // slice to honor the threshold + $similar_objects = $this->fetchObjects($user, array( + 'conditions' => array( + 'Object.id' => $similar_object_ids, + 'Object.template_uuid' => $template['ObjectTemplate']['uuid'] + ) + )); + foreach ($similar_objects as $key => $obj) { + $similar_objects[$key]['Object']['similarity_amount'] = $similar_object_similarity_amount[$obj['Object']['id']]; // sorting function cannot use external variables + } + usort($similar_objects, function ($a, $b) { // fetch Object returns object sorted by IDs, force the sort by the similarity amount + if ($a['Object']['similarity_amount'] == $b['Object']['similarity_amount']) { + return 0; + } + return ($a['Object']['similarity_amount'] > $b['Object']['similarity_amount']) ? -1 : 1; + }); + + $simple_flattened_attribute = []; + $simple_flattened_attribute_noval = []; + foreach ($attributes as $k => $attribute) { + $curFlat = $attribute['object_relation'] . '.' . $attribute['type'] . '.' .$attribute['value']; + $simple_flattened_attribute[$curFlat] = $k; + $curFlatNoval = $attribute['object_relation'] . '.' . $attribute['type']; + $simple_flattened_attribute_noval[$curFlatNoval] = $k; + } + + return [$similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval]; + } + // Set Object's *-seen (and ObjectAttribute's *-seen and ObjectAttribute's value if requested) to the provided *-seen value // Therefore, synchronizing the 3 values public function syncObjectAndAttributeSeen($object, $forcedSeenOnElements, $applyOnAttribute=True) { @@ -851,6 +942,14 @@ class MispObject extends AppModel return $object; } + /** + * @param array $object + * @param array $objectToSave + * @param bool $onlyAddNewAttribute + * @param array $user + * @return array|int + * @throws JsonException + */ public function deltaMerge(array $object, array $objectToSave, $onlyAddNewAttribute=false, array $user) { if (!isset($objectToSave['Object'])) { @@ -965,7 +1064,6 @@ class MispObject extends AppModel } } else { // we only add the new attribute $newAttribute = $objectToSave['Attribute'][0]; - $this->Event->Attribute->create(); $newAttribute['event_id'] = $object['Object']['event_id']; $newAttribute['object_id'] = $object['Object']['id']; // Set seen of object at attribute level @@ -980,10 +1078,9 @@ class MispObject extends AppModel (!array_key_exists('last_seen', $object['Object']) && !is_null($object['Object']['last_seen'])) ) { $newAttribute['last_seen'] = $object['Object']['last_seen']; - $different = true; } $saveAttributeResult = $this->Attribute->saveAttributes(array($newAttribute), $user); - return $saveAttributeResult ? $this->id : $this->validationErrors; + return $saveAttributeResult ? $this->id : $this->Attribute->validationErrors; } return $this->id; } @@ -1232,51 +1329,14 @@ class MispObject extends AppModel 'Attribute.event_id' => $eventId, 'Attribute.object_id' => 0, ], + 'fields' => ['Attribute.type'], ]); if (empty($attributes)) { return array('templates' => array(), 'types' => array()); } - $attributeTypes = array(); - foreach ($attributes as $i => $attribute) { - $attributeTypes[$attribute['Attribute']['type']] = true; - $attributes[$i]['Attribute']['object_relation'] = $attribute['Attribute']['type']; - } - $attributeTypes = array_keys($attributeTypes); - $potentialTemplateIds = $this->ObjectTemplate->find('column', array( - 'recursive' => -1, - 'fields' => array( - 'ObjectTemplate.id', - ), - 'conditions' => array( - 'ObjectTemplate.active' => true, - 'ObjectTemplateElement.type' => $attributeTypes, - ), - 'joins' => array( - array( - 'table' => 'object_template_elements', - 'alias' => 'ObjectTemplateElement', - 'type' => 'RIGHT', - 'fields' => array('ObjectTemplateElement.object_relation', 'ObjectTemplateElement.type'), - 'conditions' => array('ObjectTemplate.id = ObjectTemplateElement.object_template_id') - ) - ), - 'group' => 'ObjectTemplate.id', - )); - - $templates = $this->ObjectTemplate->find('all', [ - 'recursive' => -1, - 'conditions' => ['id' => $potentialTemplateIds], - 'contain' => ['ObjectTemplateElement' => ['fields' => ['object_relation', 'type', 'multiple']]] - ]); - - foreach ($templates as $i => $template) { - $res = $this->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributes); - $templates[$i]['ObjectTemplate']['compatibility'] = $res['valid'] ? true : $res['missingTypes']; - $templates[$i]['ObjectTemplate']['invalidTypes'] = $res['invalidTypes']; - $templates[$i]['ObjectTemplate']['invalidTypesMultiple'] = $res['invalidTypesMultiple']; - } - return array('templates' => $templates, 'types' => $attributeTypes); + $attributeTypes = array_column(array_column($attributes, 'Attribute'), 'type'); + return $this->ObjectTemplate->fetchPossibleTemplatesBasedOnTypes($attributeTypes); } public function groupAttributesIntoObject(array $user, $event_id, array $object, $template, array $selected_attribute_ids, array $selected_object_relation_mapping, $hard_delete_attribute) diff --git a/app/Model/Module.php b/app/Model/Module.php index 325408ec2..cb99a10fd 100644 --- a/app/Model/Module.php +++ b/app/Model/Module.php @@ -6,7 +6,8 @@ class Module extends AppModel { public $useTable = false; - private $__validTypes = array( + // private + const VALID_TYPES = array( 'Enrichment' => array('hover', 'expansion'), 'Import' => array('import'), 'Export' => array('export'), @@ -14,6 +15,7 @@ class Module extends AppModel 'Cortex' => array('cortex') ); + // private const TYPE_TO_FAMILY = array( 'Import' => 'Import', 'Export' => 'Export', @@ -23,7 +25,7 @@ class Module extends AppModel 'Cortex' => 'Cortex' ); - public $configTypes = array( + const CONFIG_TYPES = array( 'IP' => array( 'validation' => 'validateIPField', 'field' => 'text', @@ -351,7 +353,7 @@ class Module extends AppModel $result = array(); if (is_array($modules)) { foreach ($modules as $module) { - if (array_intersect($this->__validTypes[$moduleFamily], $module['meta']['module-type'])) { + if (array_intersect(self::VALID_TYPES[$moduleFamily], $module['meta']['module-type'])) { $moduleSettings = [ [ 'name' => 'enabled', diff --git a/app/Model/ObjectTemplate.php b/app/Model/ObjectTemplate.php index f82b89681..0062d231b 100644 --- a/app/Model/ObjectTemplate.php +++ b/app/Model/ObjectTemplate.php @@ -207,11 +207,69 @@ class ObjectTemplate extends AppModel } /** - * @param array $template - * @param array $attributes + * @param array $attributeTypes Array of attribute types to check, can contains multiple types * @return array */ - public function checkTemplateConformityBasedOnTypes(array $template, array $attributes) + public function fetchPossibleTemplatesBasedOnTypes(array $attributeTypes) + { + $uniqueAttributeTypes = array_unique($attributeTypes, SORT_REGULAR); + $potentialTemplateIds = $this->find('column', array( + 'recursive' => -1, + 'fields' => array( + 'ObjectTemplate.id', + ), + 'conditions' => array( + 'ObjectTemplate.active' => true, + 'ObjectTemplateElement.type' => $uniqueAttributeTypes, + ), + 'joins' => array( + array( + 'table' => 'object_template_elements', + 'alias' => 'ObjectTemplateElement', + 'type' => 'RIGHT', + 'fields' => array('ObjectTemplateElement.object_relation', 'ObjectTemplateElement.type'), + 'conditions' => array('ObjectTemplate.id = ObjectTemplateElement.object_template_id') + ) + ), + 'group' => 'ObjectTemplate.id', + )); + + $templates = $this->find('all', [ + 'recursive' => -1, + 'conditions' => ['id' => $potentialTemplateIds], + 'contain' => ['ObjectTemplateElement' => ['fields' => ['object_relation', 'type', 'multiple']]] + ]); + + foreach ($templates as $i => $template) { + $res = $this->checkTemplateConformityBasedOnTypes($template, $attributeTypes); + $templates[$i]['ObjectTemplate']['compatibility'] = $res['valid'] ? true : $res['missingTypes']; + $templates[$i]['ObjectTemplate']['invalidTypes'] = $res['invalidTypes']; + $templates[$i]['ObjectTemplate']['invalidTypesMultiple'] = $res['invalidTypesMultiple']; + } + + usort($templates, function($a, $b) { + if ($a['ObjectTemplate']['id'] == $b['ObjectTemplate']['id']) { + return 0; + } else if (is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { + return count($a['ObjectTemplate']['compatibility']) > count($b['ObjectTemplate']['compatibility']) ? 1 : -1; + } else if (is_array($a['ObjectTemplate']['compatibility']) && !is_array($b['ObjectTemplate']['compatibility'])) { + return 1; + } else if (!is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { + return -1; + } else { // sort based on invalidTypes count + return count($a['ObjectTemplate']['invalidTypes']) > count($b['ObjectTemplate']['invalidTypes']) ? 1 : -1; + } + }); + + return array('templates' => $templates, 'types' => $uniqueAttributeTypes); + } + + /** + * @param array $template + * @param array $attributeTypes Array of attribute types to check, can contains multiple types + * @return array + */ + public function checkTemplateConformityBasedOnTypes(array $template, array $attributeTypes) { $to_return = array('valid' => true, 'missingTypes' => array()); if (!empty($template['ObjectTemplate']['requirements'])) { @@ -222,13 +280,7 @@ class ObjectTemplate extends AppModel if (!empty($template['ObjectTemplate']['requirements']['required'])) { foreach ($template['ObjectTemplate']['requirements']['required'] as $requiredField) { $requiredType = $elementsByObjectRelationName[$requiredField]['type']; - $found = false; - foreach ($attributes as $attribute) { - if ($attribute['Attribute']['type'] === $requiredType) { - $found = true; - break; - } - } + $found = in_array($requiredType, $attributeTypes, true); if (!$found) { $to_return = array('valid' => false, 'missingTypes' => array($requiredType)); } @@ -241,11 +293,8 @@ class ObjectTemplate extends AppModel foreach ($template['ObjectTemplate']['requirements']['requiredOneOf'] as $requiredField) { $requiredType = $elementsByObjectRelationName[$requiredField]['type'] ?? null; $allRequiredTypes[] = $requiredType; - foreach ($attributes as $attribute) { - if ($attribute['Attribute']['type'] === $requiredType) { - $found = true; - break; - } + if (!$found) { + $found = in_array($requiredType, $attributeTypes, true); } } if (!$found) { @@ -262,17 +311,17 @@ class ObjectTemplate extends AppModel $valid_types[$templateElement['type']] = $templateElement['multiple']; } $check_for_multiple_type = array(); - foreach ($attributes as $attribute) { - if (isset($valid_types[$attribute['Attribute']['type']])) { - if (!$valid_types[$attribute['Attribute']['type']]) { // is not multiple - if (isset($check_for_multiple_type[$attribute['Attribute']['type']])) { - $to_return['invalidTypesMultiple'][] = $attribute['Attribute']['type']; + foreach ($attributeTypes as $attributeType) { + if (isset($valid_types[$attributeType])) { + if (!$valid_types[$attributeType]) { // is not multiple + if (isset($check_for_multiple_type[$attributeType])) { + $to_return['invalidTypesMultiple'][] = $attributeType; } else { - $check_for_multiple_type[$attribute['Attribute']['type']] = 1; + $check_for_multiple_type[$attributeType] = 1; } } } else { - $to_return['invalidTypes'][] = $attribute['Attribute']['type']; + $to_return['invalidTypes'][] = $attributeType; } } $to_return['invalidTypes'] = array_unique($to_return['invalidTypes'], SORT_REGULAR); diff --git a/app/Model/Server.php b/app/Model/Server.php index 2659da8d9..7751e6c67 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -5539,7 +5539,7 @@ class Server extends AppModel ), 'log_client_ip_header' => array( 'level' => 1, - 'description' => __('If log_client_ip is enabled, you can customize which header field contains the client\'s IP address. This is generally used when you have a reverse proxy infront of your MISP instance.'), + 'description' => __('If log_client_ip is enabled, you can customize which header field contains the client\'s IP address. This is generally used when you have a reverse proxy in front of your MISP instance. Prepend the variable with "HTTP_", for example "HTTP_X_FORWARDED_FOR".'), 'value' => 'REMOTE_ADDR', 'test' => 'testForEmpty', 'type' => 'string', @@ -5595,7 +5595,7 @@ class Server extends AppModel ), 'log_paranoid_skip_db' => array( 'level' => 0, - 'description' => __('You can decide to skip the logging of the paranoid logs to the database.'), + 'description' => __('You can decide to skip the logging of the paranoid logs to the database. Logs will be just published to ZMQ or Kafka.'), 'value' => false, 'test' => 'testParanoidSkipDb', 'type' => 'boolean', @@ -5609,6 +5609,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'log_paranoid_include_sql_queries' => [ + 'level' => 0, + 'description' => __('If paranoid logging is enabled, include the SQL queries in the entries.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ], 'log_user_ips' => array( 'level' => 0, 'description' => __('Log user IPs on each request. 30 day retention for lookups by IP to get the last authenticated user ID for the given IP, whilst on the reverse, indefinitely stores all associated IPs for a user ID.'), @@ -5649,6 +5657,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'discussion_disable' => [ + 'level' => 1, + 'description' => __('Completely disable ability for user to add discussion to events.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ], 'showCorrelationsOnIndex' => array( 'level' => 1, 'description' => __('When enabled, the number of correlations visible to the currently logged in user will be visible on the event index UI. This comes at a performance cost but can be very useful to see correlating events at a glance.'), diff --git a/app/Model/Tag.php b/app/Model/Tag.php index d3c67fe9a..25388043a 100644 --- a/app/Model/Tag.php +++ b/app/Model/Tag.php @@ -459,7 +459,7 @@ class Tag extends AppModel $tags_temp = $this->find('all', $tag_params); $tags = array(); foreach ($tags_temp as $temp) { - $tags[strtoupper($temp['Tag']['name'])] = $temp; + $tags[mb_strtolower($temp['Tag']['name'])] = $temp; } return $tags; } diff --git a/app/Model/Taxonomy.php b/app/Model/Taxonomy.php index 7ce1a1e10..5bae392dc 100644 --- a/app/Model/Taxonomy.php +++ b/app/Model/Taxonomy.php @@ -118,7 +118,7 @@ class Taxonomy extends AppModel $current = $this->find('first', array( 'conditions' => array('namespace' => $vocab['namespace']), 'recursive' => -1, - 'fields' => array('version', 'enabled', 'namespace') + 'fields' => array('version', 'enabled', 'namespace', 'highlighted') )); $current = empty($current) ? [] : $current['Taxonomy']; $result = $this->__updateVocab($vocab, $current); @@ -147,6 +147,7 @@ class Taxonomy extends AppModel 'version' => $vocab['version'], 'exclusive' => !empty($vocab['exclusive']), 'enabled' => $enabled, + 'highlighted' => !empty($vocab['highlighted']), ]]; $predicateLookup = array(); foreach ($vocab['predicates'] as $k => $predicate) { @@ -176,15 +177,10 @@ class Taxonomy extends AppModel /** * @param int|string $id Taxonomy ID or namespace - * @param string|null $options * @return array|false */ - private function __getTaxonomy($id, $options = array('full' => false, 'filter' => false)) + private function __getTaxonomy($id) { - $filter = false; - if (isset($options['filter'])) { - $filter = $options['filter']; - } if (!is_numeric($id)) { $conditions = ['Taxonomy.namespace' => trim(mb_strtolower($id))]; } else { @@ -236,17 +232,10 @@ class Taxonomy extends AppModel $entries[] = $temp; } } - $taxonomy = array('Taxonomy' => $taxonomy['Taxonomy']); - if ($filter) { - $filter = mb_strtolower($filter); - $namespaceLength = strlen($taxonomy['Taxonomy']['namespace']); - foreach ($entries as $k => $entry) { - if (strpos(substr(mb_strtolower($entry['tag']), $namespaceLength), $filter) === false) { - unset($entries[$k]); - } - } - } - $taxonomy['entries'] = $entries; + $taxonomy = [ + 'Taxonomy' => $taxonomy['Taxonomy'], + 'entries' => $entries, + ]; return $taxonomy; } @@ -264,8 +253,13 @@ class Taxonomy extends AppModel { $taxonomies = $this->find('all', [ 'fields' => ['namespace'], - 'contain' => ['TaxonomyPredicate' => ['TaxonomyEntry']], + 'recursive' => -1, + 'contain' => ['TaxonomyPredicate' => [ + 'fields' => ['value'], + 'TaxonomyEntry' => ['fields' => ['value']]], + ], ]); + $allTaxonomyTags = []; foreach ($taxonomies as $taxonomy) { $namespace = $taxonomy['Taxonomy']['namespace']; @@ -326,7 +320,7 @@ class Taxonomy extends AppModel public function getTaxonomyTags($id, $upperCase = false, $existingOnly = false) { - $taxonomy = $this->__getTaxonomy($id, array('full' => true, 'filter' => false)); + $taxonomy = $this->__getTaxonomy($id); if ($existingOnly) { $this->Tag = ClassRegistry::init('Tag'); $tags = $this->Tag->find('list', array('fields' => array('name'), 'order' => array('UPPER(Tag.name) ASC'))); @@ -352,45 +346,31 @@ class Taxonomy extends AppModel /** * @param int|string $id Taxonomy ID or namespace - * @param array|null $options + * @param bool $full Add tag information to entries * @return array|false */ - public function getTaxonomy($id, $options = array('full' => true)) + public function getTaxonomy($id, $full = true) { - $taxonomy = $this->__getTaxonomy($id, $options); + $taxonomy = $this->__getTaxonomy($id); if (empty($taxonomy)) { return false; } - $this->Tag = ClassRegistry::init('Tag'); - if (isset($options['full']) && $options['full']) { + if ($full) { + $this->Tag = ClassRegistry::init('Tag'); $tagNames = array_column($taxonomy['entries'], 'tag'); $tags = $this->Tag->getTagsByName($tagNames, false); - $filterActive = false; - if (isset($options['enabled'])) { - $filterActive = true; - $enabledTag = isset($options['enabled']) ? $options['enabled'] : null; - } - if (isset($taxonomy['entries'])) { - foreach ($taxonomy['entries'] as $key => $temp) { - if (isset($tags[strtoupper($temp['tag'])])) { - $existingTag = $tags[strtoupper($temp['tag'])]; - if ($filterActive && $options['enabled'] == $existingTag['Tag']['hide_tag']) { - unset($taxonomy['entries'][$key]); - continue; - } - $taxonomy['entries'][$key]['existing_tag'] = $existingTag; - // numerical_value is overridden at tag level. Propagate the override further up - if (isset($existingTag['Tag']['original_numerical_value'])) { - $taxonomy['entries'][$key]['original_numerical_value'] = $existingTag['Tag']['original_numerical_value']; - $taxonomy['entries'][$key]['numerical_value'] = $existingTag['Tag']['numerical_value']; - } - } else { - if ($filterActive) { - unset($taxonomy['entries'][$key]); - } else { - $taxonomy['entries'][$key]['existing_tag'] = false; - } + foreach ($taxonomy['entries'] as $key => $temp) { + $tagLower = mb_strtolower($temp['tag']); + if (isset($tags[$tagLower])) { + $existingTag = $tags[$tagLower]; + $taxonomy['entries'][$key]['existing_tag'] = $existingTag; + // numerical_value is overridden at tag level. Propagate the override further up + if (isset($existingTag['Tag']['original_numerical_value'])) { + $taxonomy['entries'][$key]['original_numerical_value'] = $existingTag['Tag']['original_numerical_value']; + $taxonomy['entries'][$key]['numerical_value'] = $existingTag['Tag']['numerical_value']; } + } else { + $taxonomy['entries'][$key]['existing_tag'] = false; } } } @@ -401,7 +381,7 @@ class Taxonomy extends AppModel { App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); $this->Tag = ClassRegistry::init('Tag'); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace'], false); @@ -440,7 +420,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); if (empty($taxonomy)) { return false; } @@ -486,7 +466,7 @@ class Taxonomy extends AppModel if ($tagList) { $tags = $tagList; } else { - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); foreach ($taxonomy['entries'] as $entry) { $tags[] = $entry['tag']; } @@ -513,7 +493,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace']); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); foreach ($taxonomy['entries'] as $k => $entry) { @@ -546,7 +526,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace']); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); foreach ($taxonomy['entries'] as $k => $entry) { @@ -699,6 +679,12 @@ class Taxonomy extends AppModel // at this point, we have a duplicated namespace(-predicate) $taxonomy = $this->getTaxonomyForTag($newTagName); if (!empty($taxonomy['Taxonomy']['exclusive'])) { + if ( + ($newTagName === 'tlp:white' && in_array('tlp:clear', $tagNameList)) || + ($newTagName === 'tlp:clear' && in_array('tlp:white', $tagNameList)) + ) { + return true; + } return false; // only one tag of this taxonomy is allowed } elseif (!empty($taxonomy['TaxonomyPredicate'][0]['exclusive'])) { return false; // only one tag belonging to this predicate is allowed @@ -877,4 +863,46 @@ class Taxonomy extends AppModel { return $this->Tag->mergeTag($source_id, $target_id); } + + /** + * @return array + */ + public function getHighlightedTaxonomies() + { + return $this->find('all', [ + 'conditions' => [ + 'highlighted' => 1, + ] + ]); + } + + /** + * + * @param array $highlightedTaxonomies + * @param array $tags + * @return array + */ + public function getHighlightedTags($highlightedTaxonomies, $tags) + { + $highlightedTags = []; + if (is_array($highlightedTaxonomies) && !empty($highlightedTaxonomies)) { + foreach ($highlightedTaxonomies as $k => $taxonomy) { + $highlightedTags[$k] = [ + 'taxonomy' => $taxonomy, + 'tags' => [] + ]; + + foreach ($tags as $tag) { + $splits = $this->splitTagToComponents($tag['Tag']['name']); + if (!empty($splits) && $splits['namespace'] === $taxonomy['Taxonomy']['namespace']) { + $highlightedTags[$k]['tags'][] = $tag; + } + } + } + + return $highlightedTags; + } + + return $highlightedTags; + } } diff --git a/app/Model/User.php b/app/Model/User.php index 74d85e4e8..5ea2ca298 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -849,7 +849,7 @@ class User extends AppModel return true; } - $this->loadLog(); + $log = $this->loadLog(); $replyToLog = $replyToUser ? ' from ' . $replyToUser['User']['email'] : ''; $gpg = $this->initializeGpg(); @@ -859,8 +859,8 @@ class User extends AppModel } catch (SendEmailException $e) { $this->logException("Exception during sending e-mail", $e); - $this->Log->create(); - $this->Log->save(array( + $log->create(); + $log->save(array( 'org' => 'SYSTEM', 'model' => 'User', 'model_id' => $user['User']['id'], @@ -876,8 +876,8 @@ class User extends AppModel // Intentional two spaces to pass test :) $logTitle .= $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $result['subject'] . '".'; - $this->Log->create(); - $this->Log->save(array( + $log->create(); + $log->save(array( 'org' => 'SYSTEM', 'model' => 'User', 'model_id' => $user['User']['id'], @@ -1458,18 +1458,23 @@ class User extends AppModel */ public function checkIfUserIsValid(array $user) { - $auth = Configure::read('Security.auth'); - if (!$auth) { - return true; + static $oidc; + + if ($oidc === null) { + $auth = Configure::read('Security.auth'); + if (!$auth) { + return true; + } + if (!is_array($auth)) { + throw new Exception("`Security.auth` config value must be array."); + } + if (!in_array('OidcAuth.Oidc', $auth, true)) { + return true; // this method currently makes sense just for OIDC auth provider + } + App::uses('Oidc', 'OidcAuth.Lib'); + $oidc = new Oidc($this); } - if (!is_array($auth)) { - throw new Exception("`Security.auth` config value must be array."); - } - if (!in_array('OidcAuth.Oidc', $auth, true)) { - return true; // this method currently makes sense just for OIDC auth provider - } - App::uses('Oidc', 'OidcAuth.Lib'); - $oidc = new Oidc($this); + return $oidc->isUserValid($user); } @@ -1971,4 +1976,30 @@ class User extends AppModel } return $users; } + + public function checkForSessionDestruction($id) + { + if (empty(CakeSession::read('creation_timestamp'))) { + return false; + } + $redis = $this->setupRedis(); + if ($redis) { + $cutoff = $redis->get('misp:session_destroy:' . $id); + $allcutoff = $redis->get('misp:session_destroy:all'); + if ( + empty($cutoff) || + ( + !empty($cutoff) && + !empty($allcutoff) && + $allcutoff < $cutoff + ) + ) { + $cutoff = $allcutoff; + } + if ($cutoff && CakeSession::read('creation_timestamp') < $cutoff) { + return true; + } + } + return false; + } } diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index 2b5170db6..80dca6ef0 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -621,16 +621,20 @@ class Workflow extends AppModel 'id' => Configure::read('MISP.host_org_id') ], ]); + $this->User = ClassRegistry::init('User'); if (!empty($hostOrg)) { + $perms = array_keys($this->User->Role->permFlags); + $allPermEnabled = array_map(function($perm) { + return true; + }, array_flip($perms)); $userForWorkflow = [ 'email' => 'SYSTEM', 'id' => 0, 'org_id' => $hostOrg['Organisation']['id'], - 'Role' => ['perm_site_admin' => 1], + 'Role' => $allPermEnabled, 'Organisation' => $hostOrg['Organisation'] ]; } else { - $this->User = ClassRegistry::init('User'); $userForWorkflow = $this->User->find('first', [ 'recursive' => -1, 'conditions' => [ diff --git a/app/Test/AttributeValidationToolTest.php b/app/Test/AttributeValidationToolTest.php index 16226d308..b1781220d 100644 --- a/app/Test/AttributeValidationToolTest.php +++ b/app/Test/AttributeValidationToolTest.php @@ -110,6 +110,20 @@ class AttributeValidationToolTest extends TestCase ]); } + public function testValidateAs(): void + { + $this->shouldBeValid('AS', [ + '0', + 0, + 1, + '1', + 4294967295, + ]); + $this->shouldBeInvalid('AS', [ + '1.2.3.4', + ]); + } + public function testCompressIpv6(): void { $this->assertEquals('1234:fd2:5621:1:89::4500', AttributeValidationTool::modifyBeforeValidation('ip-src', '1234:0fd2:5621:0001:0089:0000:0000:4500')); diff --git a/app/View/AccessLogs/admin_index.ctp b/app/View/AccessLogs/admin_index.ctp index e3859c81c..ec1a677de 100644 --- a/app/View/AccessLogs/admin_index.ctp +++ b/app/View/AccessLogs/admin_index.ctp @@ -315,13 +315,14 @@ - ' : '' ?> + ' : '' ?> ms - + ' : '') ?> + @@ -347,6 +348,17 @@ return false; }); + $('.query-log').click(function (e) { + e.preventDefault(); + var id = $(this).data('log-id'); + $.get(baseurl + "/admin/access_logs/queryLog/" + id, function(data) { + var $popoverFormLarge = $('#popover_form_large'); + $popoverFormLarge.html(data); + openPopup($popoverFormLarge); + }).fail(xhrFailCallback); + return false; + }); + $(function() { filterSearch(function (e, searchKey, searchValue) { if (searchKey === 'controller:action') { diff --git a/app/View/AccessLogs/admin_query_log.ctp b/app/View/AccessLogs/admin_query_log.ctp new file mode 100644 index 000000000..f25bc0cb7 --- /dev/null +++ b/app/View/AccessLogs/admin_query_log.ctp @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +
diff --git a/app/View/Attributes/add.ctp b/app/View/Attributes/add.ctp index 288dd0387..db89756d6 100644 --- a/app/View/Attributes/add.ctp +++ b/app/View/Attributes/add.ctp @@ -52,16 +52,18 @@ 'div' => 'input clear', 'label' => __("Contextual Comment") ), + array( + 'field' => 'batch_import', + 'type' => 'checkbox', + 'requirements' => $action === 'add', + 'label' => __('Batch import') . ' ', + ), array( 'field' => 'to_ids', 'type' => 'checkbox', 'label' => __("For Intrusion Detection System"), //'stayInLine' => 1 ), - array( - 'field' => 'batch_import', - 'type' => 'checkbox' - ), array( 'field' => 'disable_correlation', 'type' => 'checkbox' @@ -89,7 +91,7 @@ ), 'metaFields' => array( '', - '
' + '
' ) ) )); diff --git a/app/View/Attributes/ajax/attributeEditCategoryForm.ctp b/app/View/Attributes/ajax/attributeEditCategoryForm.ctp index 4d45de114..a44a9b7f1 100644 --- a/app/View/Attributes/ajax/attributeEditCategoryForm.ctp +++ b/app/View/Attributes/ajax/attributeEditCategoryForm.ctp @@ -1,12 +1,12 @@ Form->create('Attribute', array('class' => 'inline-form inline-field-form', 'id' => 'Attribute_' . $object['id'] . '_category_form', 'url' => $baseurl . '/attributes/editField/' . $object['id'])); ?> -
+
Form->input('category', array( - 'options' => array(array_combine($typeCategory[$object['type']], $typeCategory[$object['type']])), + 'options' => array_combine($possibleCategories, $possibleCategories), 'label' => false, 'selected' => $object['category'], 'error' => array('escape' => false), diff --git a/app/View/Attributes/ajax/attributeEditTypeForm.ctp b/app/View/Attributes/ajax/attributeEditTypeForm.ctp index 64dc561fa..fb3092932 100644 --- a/app/View/Attributes/ajax/attributeEditTypeForm.ctp +++ b/app/View/Attributes/ajax/attributeEditTypeForm.ctp @@ -1,12 +1,12 @@ Form->create('Attribute', array('class' => 'inline-form inline-field-form', 'id' => 'Attribute_' . $object['id'] . '_type_form', 'url' => $baseurl . '/attributes/editField/' . $object['id'])); ?> -
+
Form->input('type', array( - 'options' => array(array_combine($categoryDefinitions[$object['category']]['types'], $categoryDefinitions[$object['category']]['types'])), + 'options' => $options, 'label' => false, 'selected' => $object['type'], 'error' => array('escape' => false), diff --git a/app/View/Attributes/ajax/attributeViewFieldForm.ctp b/app/View/Attributes/ajax/attributeViewFieldForm.ctp index 9c145fcc9..e89641444 100644 --- a/app/View/Attributes/ajax/attributeViewFieldForm.ctp +++ b/app/View/Attributes/ajax/attributeViewFieldForm.ctp @@ -3,12 +3,12 @@ if ($field === 'value') { echo $this->element('Events/View/value_field', ['object' => $object['Attribute']]); } elseif ($field === 'timestamp') { echo $this->Time->date($value); -} else { - if ($value === 'No') { - echo ''; - } else if ($value === 'Yes') { - echo ''; +} elseif ($field === 'distribution') { + if ($value == 0) { + echo '' . $shortDist[$value] . ''; } else { - echo nl2br(h($value), false); + echo $shortDist[$value]; } +} else { + echo nl2br(h($value), false); } diff --git a/app/View/Attributes/index.ctp b/app/View/Attributes/index.ctp index 788657df6..b6d73610d 100755 --- a/app/View/Attributes/index.ctp +++ b/app/View/Attributes/index.ctp @@ -140,22 +140,19 @@ echo $this->element('/genericElements/IndexTable/index_table', [ 'Attribute.id' ], 'icon' => 'comment', - 'complex_requirement' => [ - 'function' => function ($object) use ($isSiteAdmin, $me) { - return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); - } - ] + 'title' => __('Add proposal'), + 'complex_requirement' => function ($object) use ($isSiteAdmin, $me) { + return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); + }, ], [ 'onclick' => "deleteObject('shadow_attributes', 'delete', '[onclick_params_data_path]');", 'onclick_params_data_path' => 'Attribute.id', 'icon' => 'trash', 'title' => __('Propose deletion'), - 'complex_requirement' => [ - 'function' => function ($object) use ($isSiteAdmin, $me) { - return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); - } - ] + 'complex_requirement' => function ($object) use ($isSiteAdmin, $me) { + return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); + } ], [ 'title' => __('Propose enrichment'), @@ -272,7 +269,6 @@ $class = $isSearch ? 'searchAttributes' : 'listAttributes'; echo $this->element('/genericElements/SideMenu/side_menu', ['menuList' => 'event-collection', 'menuItem' => $class]); ?> - element('/genericElements/SideMenu/side_menu', array('menuList' => 'event-collection', 'menuItem' => 'searchAttributes')); diff --git a/app/View/AuditLogs/admin_index.ctp b/app/View/AuditLogs/admin_index.ctp index cc8b05896..5230b068b 100644 --- a/app/View/AuditLogs/admin_index.ctp +++ b/app/View/AuditLogs/admin_index.ctp @@ -278,7 +278,7 @@ ' : '' ?> - + ' : '' ?> diff --git a/app/View/AuditLogs/event_index.ctp b/app/View/AuditLogs/event_index.ctp index 76c88152a..9fd0aa7d4 100644 --- a/app/View/AuditLogs/event_index.ctp +++ b/app/View/AuditLogs/event_index.ctp @@ -1,22 +1,21 @@
-

+

- - - - + + + + @@ -32,22 +31,16 @@ } ?> - +
Paginator->sort('created') ?>Paginator->sort('user_id', __('User')) ?>Paginator->sort('org_id', __('Org')) ?>Paginator->sort('action') ?>LightPaginator->sort('created') ?>LightPaginator->sort('user_id', __('User')) ?>LightPaginator->sort('org_id', __('Org')) ?>LightPaginator->sort('action') ?> OrgImg->getOrgLogo($item, 24) : '' ?> element('AuditLog/change', ['item' => $item]) ?>
-

- Paginator->counter(array( - 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') - )); - ?> -

diff --git a/app/View/AuthKeys/index.ctp b/app/View/AuthKeys/index.ctp index bc61d58fa..0fc6e0dd8 100644 --- a/app/View/AuthKeys/index.ctp +++ b/app/View/AuthKeys/index.ctp @@ -43,6 +43,7 @@ 'element' => empty($user_id) ? 'links' : 'generic_field', 'url' => $baseurl . '/users/view', 'url_params_data_paths' => ['User.id'], + 'requirement' => $me['Role']['perm_admin'] || $me['Role']['perm_site_admin'], ], [ 'name' => __('Auth Key'), diff --git a/app/View/Communities/index.ctp b/app/View/Communities/index.ctp index 4b1498fcd..9280af2cf 100644 --- a/app/View/Communities/index.ctp +++ b/app/View/Communities/index.ctp @@ -31,7 +31,7 @@ ), 'fields' => array( array( - 'name' => __('Id'), + 'name' => __('ID'), 'sort' => 'id', 'class' => 'short', 'data_path' => 'id', @@ -76,14 +76,16 @@ 'url_params_data_paths' => array( 'uuid' ), - 'icon' => 'eye' + 'icon' => 'eye', + 'title' => __('View'), ), array( 'url' => $baseurl . '/communities/requestAccess', 'url_params_data_paths' => array( 'uuid' ), - 'icon' => 'comments' + 'icon' => 'comments', + 'title' => __('Request access'), ) ) ) @@ -91,12 +93,12 @@ echo '
'; echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'sync', 'menuItem' => 'list_communities')); ?> - diff --git a/app/View/Elements/templateElements/populateTemplateAttribute.ctp b/app/View/Elements/templateElements/populateTemplateAttribute.ctp index c6b77ab93..e7d853c92 100644 --- a/app/View/Elements/templateElements/populateTemplateAttribute.ctp +++ b/app/View/Elements/templateElements/populateTemplateAttribute.ctp @@ -65,7 +65,7 @@ ?>
> - +
diff --git a/app/View/Elements/view_event_distribution_graph.ctp b/app/View/Elements/view_event_distribution_graph.ctp index 74a323944..6326133e5 100644 --- a/app/View/Elements/view_event_distribution_graph.ctp +++ b/app/View/Elements/view_event_distribution_graph.ctp @@ -1,7 +1,7 @@
-
+
diff --git a/app/View/Elements/view_timeline.ctp b/app/View/Elements/view_timeline.ctp index ab8bb1bfb..370b09ddd 100644 --- a/app/View/Elements/view_timeline.ctp +++ b/app/View/Elements/view_timeline.ctp @@ -1,4 +1,4 @@ -
+
element('genericElements/assetLoader', [ - 'js' => [ - 'moment.min', - 'event-timeline', - ], - 'css' => [ - 'event-timeline', - ], - ]); + 'js' => [ + 'moment.min', + 'Chart.min', + 'chartjs-adapter-moment.min', + 'event-timeline', + ], + 'css' => [ + 'event-timeline', + ], +]); diff --git a/app/View/Events/ajax/importChoice.ctp b/app/View/Events/ajax/importChoice.ctp index 6996f364b..285ef97c9 100644 --- a/app/View/Events/ajax/importChoice.ctp +++ b/app/View/Events/ajax/importChoice.ctp @@ -2,9 +2,9 @@
- $import): ?> + - +
')">')">
@@ -12,7 +12,7 @@
+ +element('/genericElements/SideMenu/side_menu', ['menuList' => 'event', 'menuItem' => 'freetextResults']); \ No newline at end of file diff --git a/app/View/Objects/get_row.ctp b/app/View/Objects/get_row.ctp index cc85899a4..7fa25174d 100644 --- a/app/View/Objects/get_row.ctp +++ b/app/View/Objects/get_row.ctp @@ -8,6 +8,6 @@ ) ); ?> - diff --git a/app/View/Objects/revise_object.ctp b/app/View/Objects/revise_object.ctp index c66979690..e08f12094 100644 --- a/app/View/Objects/revise_object.ctp +++ b/app/View/Objects/revise_object.ctp @@ -26,7 +26,7 @@ $tableData = [ echo $this->Form->create('Object', array('id', 'url' => $url)); $formSettings = array( 'type' => 'hidden', - 'value' => json_encode($data), + 'value' => JsonTool::encode($data), 'label' => false, 'div' => false ); @@ -49,7 +49,7 @@ $tableData = [ - + @@ -61,15 +61,11 @@ $tableData = [ $attribute): - $cur_flat = h($attribute['object_relation']) . '.' . h($attribute['type']) . '.' .h($attribute['value']); - $cur_flat_noval = h($attribute['object_relation']) . '.' . h($attribute['type']); - $simple_flattened_attribute[$cur_flat] = $id; - $simple_flattened_attribute_noval[$cur_flat_noval] = $id; + $cur_flat = $simple_flattened_attribute[$id] ?? ''; + $cur_flat_noval = $simple_flattened_attribute_noval[$id] ?? ''; echo sprintf('', h($cur_flat), h($cur_flat_noval)); echo ''; foreach ($attributeFields as $field) { @@ -103,16 +99,16 @@ $tableData = [ Form->button($action === 'add' ? __('Create new object') : __('Update object'), array('class' => 'btn btn-primary')); ?> - - ' . __('This event contains similar objects.') . ''; ?> - ' . __('Instead of creating a new object, would you like to merge your new object into one of the following?') . ''; ?> + +

+
element('Objects/object_similarities', array( 'object' => $object, + 'attributes' => $data['Attribute'], 'template' => $template, - 'similar_object_similarity_amount' => $similar_object_similarity_amount, 'simple_flattened_attribute_noval' => $simple_flattened_attribute_noval, 'simple_flattened_attribute' => $simple_flattened_attribute, 'merge_button_functionname' => 'setMergeObject' @@ -123,7 +119,7 @@ $tableData = [

- +
@@ -172,7 +168,7 @@ function highlight_rows($panel, state) { } var un_highlight_time; -$(document).ready(function() { +$(function() { $('.similarObjectPanel').hover( function() { var $panel = $(this); diff --git a/app/View/Pages/doc/categories_and_types.ctp b/app/View/Pages/doc/categories_and_types.ctp index 296f20d95..11244e8f1 100644 --- a/app/View/Pages/doc/categories_and_types.ctp +++ b/app/View/Pages/doc/categories_and_types.ctp @@ -1,15 +1,10 @@ -
- -

' . h($attribute['object_relation']) . '
- $catDef): ?> + $catDef): ?> @@ -21,7 +16,7 @@ $catDef): ?> @@ -73,3 +68,4 @@
- +

+element('/genericElements/SideMenu/side_menu', array('menuList' => 'globalActions', 'menuItem' => 'categoriesAndTypes')); \ No newline at end of file diff --git a/app/View/Regexp/index.ctp b/app/View/Regexp/index.ctp index 13d822f34..d4d124653 100755 --- a/app/View/Regexp/index.ctp +++ b/app/View/Regexp/index.ctp @@ -11,7 +11,7 @@
- + diff --git a/app/View/SharingGroups/view.ctp b/app/View/SharingGroups/view.ctp index fb08dbe2e..48dada082 100644 --- a/app/View/SharingGroups/view.ctp +++ b/app/View/SharingGroups/view.ctp @@ -50,7 +50,8 @@ echo $this->element( [ 'key' => __('Organisations'), 'type' => 'custom', - 'function' => function ($sharingGroup) { + 'requirement' => isset($sg['SharingGroupOrg']), + 'function' => function (array $sharingGroup) { echo sprintf( '
Paginator->sort('id');?>Paginator->sort('id', __('ID'));?> Paginator->sort('regexp', __('Regexp'));?> Paginator->sort('replacement', __('Replacement'));?> Paginator->sort('type');?>
@@ -77,7 +78,8 @@ echo $this->element( [ 'key' => __('Instances'), 'type' => 'custom', - 'function' => function ($sharingGroup) { + 'requirement' => isset($sg['SharingGroupServer']), + 'function' => function (array $sharingGroup) { echo sprintf( '
diff --git a/app/View/TagCollections/index.ctp b/app/View/TagCollections/index.ctp index 7d4d0195a..2a7cb31fd 100755 --- a/app/View/TagCollections/index.ctp +++ b/app/View/TagCollections/index.ctp @@ -4,7 +4,7 @@ 'alias' => __('Tag Collections'), 'controller' => 'tag_collections', 'headers' => array( - 'id' => array('sort' => 1), + 'id' => array('sort' => 1, 'alias' => __('ID')), 'uuid' => array('sort' => 1, 'alias' => __('UUID')), 'name' => array('sort' => 1), 'tags' => array('alias' => __('Tags')), diff --git a/app/View/Taxonomies/ajax/taxonomy_tags.ctp b/app/View/Taxonomies/ajax/taxonomy_tags.ctp index 2b92d3fad..e7b422af3 100644 --- a/app/View/Taxonomies/ajax/taxonomy_tags.ctp +++ b/app/View/Taxonomies/ajax/taxonomy_tags.ctp @@ -71,7 +71,9 @@ $actions = [ 'icon' => 'share-alt', 'url' => $baseurl . '/tags/viewGraph', 'url_params_data_paths' => ['existing_tag.Tag.id'], - 'postLinkConfirm' => __('Are you sure you want to create this tag?'), + 'complex_requirement' => function ($row) { + return $row['existing_tag']; + }, 'requirement' => $isAclTagger && $taxonomy['enabled'], ], [ diff --git a/app/View/Taxonomies/ajax/toggle_highlighted.ctp b/app/View/Taxonomies/ajax/toggle_highlighted.ctp new file mode 100644 index 000000000..a17c30252 --- /dev/null +++ b/app/View/Taxonomies/ajax/toggle_highlighted.ctp @@ -0,0 +1,15 @@ +Form->create('Taxonomy', array( + 'id' => 'HighlightedCheckboxForm' . h($id), + 'label' => false, + 'style' => 'display:none;', + 'url' => $baseurl . '/taxonomies/toggleHighlighted/' . $id + )); + echo $this->Form->checkbox('highlighted', array( + 'checked' => $highlighted, + 'label' => false, + 'disabled' => !$isSiteAdmin, + 'class' => 'highlighted-toggle' + )); + echo $this->Form->end(); +?> diff --git a/app/View/Taxonomies/index.ctp b/app/View/Taxonomies/index.ctp index f2aefb7f0..b85fe5790 100644 --- a/app/View/Taxonomies/index.ctp +++ b/app/View/Taxonomies/index.ctp @@ -75,6 +75,18 @@ 'data_path' => 'Taxonomy.required', 'disabled' => !$isSiteAdmin, ), + array( + 'name' => __('Highlighted'), + 'element' => 'toggle', + 'url' => $baseurl . '/taxonomies/toggleHighlighted', + 'url_params_data_paths' => array( + 'Taxonomy.id' + ), + 'sort' => 'highlighted', + 'class' => 'short', + 'data_path' => 'Taxonomy.highlighted', + 'disabled' => !$isSiteAdmin, + ), array( 'name' => __('Active Tags'), 'element' => 'custom', diff --git a/app/View/Taxonomies/view.ctp b/app/View/Taxonomies/view.ctp index e569aa323..5a5cc786f 100644 --- a/app/View/Taxonomies/view.ctp +++ b/app/View/Taxonomies/view.ctp @@ -46,6 +46,11 @@ echo $this->element( 'path' => 'enabled', 'type' => 'boolean' ], + [ + 'key' => __('Highlighted'), + 'path' => 'highlighted', + 'type' => 'boolean' + ], [ 'key' => __('Action'), 'type' => 'custom', diff --git a/app/View/Templates/upload_file.ctp b/app/View/Templates/upload_file.ctp index 77d68182a..315b50be6 100644 --- a/app/View/Templates/upload_file.ctp +++ b/app/View/Templates/upload_file.ctp @@ -6,7 +6,7 @@ if ($batch == 'yes') { $multiple = false; if (isset($filenames)) { $buttonText = __('Replace File'); - } else { + } else { $buttonText = __('Upload File'); } } @@ -18,13 +18,13 @@ if ($batch == 'yes') { echo $this->Form->end(); ?> - +