From ad1b373766c79d8ad9c17b88cc6343043607d27a Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Fri, 22 Jan 2021 13:01:23 +0100 Subject: [PATCH] new: [log] Audit log --- app/Config/routes.php | 1 + app/Console/Command/EventShell.php | 3 + app/Controller/AuditLogsController.php | 571 ++++++++++++++++++ app/Controller/Component/ACLComponent.php | 4 + app/Controller/LogsController.php | 4 +- app/Model/AdminSetting.php | 1 + app/Model/Allowedlist.php | 1 + app/Model/AppModel.php | 24 +- app/Model/Attribute.php | 1 + app/Model/AttributeTag.php | 2 +- app/Model/AuditLog.php | 323 ++++++++++ app/Model/AuthKey.php | 1 + app/Model/Behavior/AuditLogBehavior.php | 351 +++++++++++ app/Model/Cerebrate.php | 1 + app/Model/CorrelationExclusion.php | 1 + app/Model/Event.php | 1 + app/Model/EventBlocklist.php | 1 + app/Model/EventDelegation.php | 2 +- app/Model/EventReport.php | 1 + app/Model/EventTag.php | 2 +- app/Model/Feed.php | 4 +- app/Model/Galaxy.php | 1 + app/Model/GalaxyCluster.php | 1 + app/Model/GalaxyClusterBlocklist.php | 1 + app/Model/GalaxyClusterRelation.php | 1 + app/Model/GalaxyClusterRelationTag.php | 2 +- app/Model/GalaxyElement.php | 1 + app/Model/Log.php | 3 + app/Model/MispObject.php | 1 + app/Model/News.php | 2 +- app/Model/ObjectReference.php | 1 + app/Model/ObjectRelationship.php | 1 + app/Model/ObjectTemplate.php | 1 + app/Model/ObjectTemplateElement.php | 1 + app/Model/OrgBlocklist.php | 1 + app/Model/Organisation.php | 1 + app/Model/Post.php | 1 + app/Model/Regexp.php | 1 + app/Model/Role.php | 1 + app/Model/Server.php | 5 +- app/Model/ShadowAttribute.php | 1 + app/Model/SharingGroup.php | 1 + app/Model/SharingGroupOrg.php | 2 +- app/Model/SharingGroupServer.php | 2 +- app/Model/Sightingdb.php | 1 + app/Model/SightingdbOrg.php | 1 + app/Model/Tag.php | 1 + app/Model/TagCollection.php | 1 + app/Model/TagCollectionTag.php | 1 + app/Model/Taxonomy.php | 1 + app/Model/Thread.php | 1 + app/Model/User.php | 1 + app/Model/UserSetting.php | 1 + app/Model/Warninglist.php | 1 + .../Model/Behavior/SysLogLogableBehavior.php | 14 +- app/View/AuditLogs/admin_index.ctp | 390 ++++++++++++ app/View/AuditLogs/event_index.ctp | 88 +++ .../genericElements/SideMenu/side_menu.ctp | 8 +- app/View/Elements/global_menu.ctp | 7 +- app/View/Events/view.ctp | 8 +- app/webroot/css/main.css | 5 + db_schema.json | 163 ++++- 62 files changed, 2008 insertions(+), 18 deletions(-) create mode 100644 app/Controller/AuditLogsController.php create mode 100644 app/Model/AuditLog.php create mode 100644 app/Model/Behavior/AuditLogBehavior.php create mode 100644 app/View/AuditLogs/admin_index.ctp create mode 100644 app/View/AuditLogs/event_index.ctp diff --git a/app/Config/routes.php b/app/Config/routes.php index 48a8f7c76..6a2d5ecbf 100644 --- a/app/Config/routes.php +++ b/app/Config/routes.php @@ -32,6 +32,7 @@ Router::connect('/users/admin_index/*', array('controller' => 'users', 'action' => 'index', 'admin' => true)); Router::connect('/roles/admin_index/*', array('controller' => 'roles', 'action' => 'index', 'admin' => true)); Router::connect('/logs/admin_search/*', array('controller' => 'logs', 'action' => 'search', 'admin' => true)); + Router::connect('/audit_logs/admin_index/*', array('controller' => 'audit_logs', 'action' => 'index', 'admin' => true)); Router::connect('/logs/admin_index/*', array('controller' => 'logs', 'action' => 'index', 'admin' => true)); Router::connect('/regexp/admin_index/*', array('controller' => 'regexp', 'action' => 'index', 'admin' => true)); diff --git a/app/Console/Command/EventShell.php b/app/Console/Command/EventShell.php index ec746fcf7..d67aa79e5 100644 --- a/app/Console/Command/EventShell.php +++ b/app/Console/Command/EventShell.php @@ -445,6 +445,7 @@ class EventShell extends AppShell $inputData = $tempFile->read(); $inputData = json_decode($inputData, true); $tempFile->delete(); + Configure::write('CurrentUserId', $inputData['user']['id']); $this->Event->processFreeTextData( $inputData['user'], $inputData['attributes'], @@ -465,6 +466,7 @@ class EventShell extends AppShell $tempFile = new File(APP . 'tmp/cache/ingest' . DS . $inputFile); $inputData = json_decode($tempFile->read(), true); $tempFile->delete(); + Configure::write('CurrentUserId', $inputData['user']['id']); $this->Event->processModuleResultsData( $inputData['user'], $inputData['misp_format'], @@ -530,6 +532,7 @@ class EventShell extends AppShell if (empty($user)) { $this->error("User with ID $userId does not exists."); } + Configure::write('CurrentUserId', $user['id']); // for audit logging purposes return $user; } diff --git a/app/Controller/AuditLogsController.php b/app/Controller/AuditLogsController.php new file mode 100644 index 000000000..91f80cb09 --- /dev/null +++ b/app/Controller/AuditLogsController.php @@ -0,0 +1,571 @@ + -1, + 'limit' => 60, + 'fields' => ['id', 'created', 'user_id', 'org_id', 'action', 'model', 'model_id', 'model_title', 'event_id', 'change'], + 'contain' => [ + 'User' => ['fields' => ['id', 'email', 'org_id']], + 'Organisation' => ['fields' => ['id', 'name', 'uuid']], + ], + 'order' => [ + 'AuditLog.id' => 'DESC' + ], + ]; + + public function __construct($id = false, $table = null, $ds = null) + { + parent::__construct($id, $table, $ds); + $this->actions = [ + AuditLog::ACTION_ADD => __('Add'), + AuditLog::ACTION_EDIT => __('Edit'), + AuditLog::ACTION_SOFT_DELETE => __('Soft delete'), + AuditLog::ACTION_DELETE => __('Delete'), + AuditLog::ACTION_UNDELETE => __('Undelete'), + AuditLog::ACTION_TAG => __('Tag'), + AuditLog::ACTION_TAG_LOCAL => __('Tag'), + AuditLog::ACTION_REMOVE_TAG => __('Remove tag'), + AuditLog::ACTION_REMOVE_TAG_LOCAL => __('Remove tag'), + AuditLog::ACTION_GALAXY => __('Galaxy cluster'), + AuditLog::ACTION_GALAXY_LOCAL => __('Galaxy cluster'), + AuditLog::ACTION_REMOVE_GALAXY => __('Remove galaxy cluster'), + AuditLog::ACTION_REMOVE_GALAXY_LOCAL => __('Remove galaxy cluster'), + AuditLog::ACTION_PUBLISH => __('Publish'), + AuditLog::ACTION_PUBLISH_SIGHTINGS => __('Publish sightings'), + ]; + } + + public function admin_index() + { + $this->paginate['fields'][] = 'ip'; + $this->paginate['fields'][] = 'request_type'; + $this->paginate['fields'][] = 'authkey_id'; + + if ($this->_isRest()) { + $this->paginate['fields'][] = 'request_id'; + } + + $this->paginate['conditions'] = $this->__searchConditions(); + $list = $this->paginate(); + + if ($this->_isRest()) { + return $this->RestResponse->viewData($list, 'json'); + } + + $list = $this->__appendModelLinks($list); + foreach ($list as $k => $item) { + $list[$k]['AuditLog']['action_human'] = $this->actions[$item['AuditLog']['action']]; + } + + $this->set('list', $list); + $this->set('actions', [ + AuditLog::ACTION_ADD => __('Add'), + AuditLog::ACTION_EDIT => __('Edit'), + AuditLog::ACTION_SOFT_DELETE => __('Soft delete'), + AuditLog::ACTION_DELETE => __('Delete'), + AuditLog::ACTION_UNDELETE => __('Undelete'), + AuditLog::ACTION_TAG . '||' . AuditLog::ACTION_TAG_LOCAL => __('Tag'), + AuditLog::ACTION_REMOVE_TAG . '||' . AuditLog::ACTION_REMOVE_TAG_LOCAL => __('Remove tag'), + AuditLog::ACTION_GALAXY . '||' . AuditLog::ACTION_GALAXY_LOCAL => __('Galaxy cluster'), + AuditLog::ACTION_REMOVE_GALAXY . '||' . AuditLog::ACTION_REMOVE_GALAXY_LOCAL => __('Remove galaxy cluster'), + AuditLog::ACTION_PUBLISH => __('Publish'), + AuditLog::ACTION_PUBLISH_SIGHTINGS => $this->actions[AuditLog::ACTION_PUBLISH_SIGHTINGS], + ]); + $models = $this->models; + sort($models); + $this->set('models', $models); + $this->set('title_for_layout', __('Audit logs')); + } + + public function eventIndex($eventId, $org = null) + { + $this->loadModel('Event'); + $event = $this->Event->fetchSimpleEvent($this->Auth->user(), $eventId); + if (empty($event)) { + throw new NotFoundException('Invalid event.'); + } + + $this->paginate['conditions'] = $this->__createEventIndexConditions($event); + + if ($org) { + $org = $this->AuditLog->Organisation->fetchOrg($org); + if ($org) { + $this->paginate['conditions']['AND']['org_id'] = $org['id']; + } else { + $this->paginate['conditions']['AND']['org_id'] = -1; + } + } + + $list = $this->paginate(); + + if (!$this->_isSiteAdmin()) { + // Remove all user info about users from different org + $this->loadModel('User'); + $orgUserIds = $this->User->find('column', array( + 'conditions' => ['User.org_id' => $this->Auth->user('org_id')], + 'fields' => ['User.id'], + )); + foreach ($list as $k => $item) { + if ($item['AuditLog']['user_id'] == 0) { + continue; + } + if (!in_array($item['User']['id'], $orgUserIds)) { + unset($list[$k]['User']); + unset($list[$k]['AuditLog']['user_id']); + } + } + } + + if ($this->_isRest()) { + return $this->RestResponse->viewData($list, 'json'); + } + + foreach ($list as $k => $item) { + $list[$k]['AuditLog']['action_human'] = $this->actions[$item['AuditLog']['action']]; + } + + $this->set('list', $list); + $this->set('event', $event); + $this->set('mayModify', $this->__canModifyEvent($event)); + $this->set('title_for_layout', __('Audit logs for event #%s', $event['Event']['id'])); + } + + /** + * @return array + */ + private function __searchConditions() + { + $params = $this->IndexFilter->harvestParameters([ + 'ip', + 'user', + 'request_id', + 'authkey_id', + 'model', + 'model_id', + 'event_id', + 'model_title', + 'action', + 'org', + 'created', + 'request_type', + ]); + + $qbRules = []; + foreach ($params as $key => $value) { + if ($key === 'model' && strpos($value, ':') !== false) { + $parts = explode(':', $value); + $qbRules[] = [ + 'id' => 'model', + 'value' => $parts[0], + ]; + $qbRules[] = [ + 'id' => 'model_id', + 'value' => $parts[1], + ]; + } elseif ($key === 'created') { + $qbRules[] = [ + 'id' => $key, + 'operator' => is_array($value) ? 'between' : 'greater_or_equal', + 'value' => $value, + ]; + } else { + if (is_array($value)) { + $value = implode('||', $value); + } + $qbRules[] = [ + 'id' => $key, + 'value' => $value, + ]; + } + } + $this->set('qbRules', $qbRules); + + $conditions = []; + if (isset($params['user'])) { + if (strtoupper($params['user']) === 'SYSTEM') { + $conditions['AuditLog.user_id'] = 0; + } else if (is_numeric($params['user'])) { + $conditions['AuditLog.user_id'] = $params['user']; + } else { + $user = $this->User->find('first', [ + 'conditions' => ['User.email' => $params['user']], + 'fields' => ['id'], + ]); + if (!empty($user)) { + $conditions['AuditLog.user_id'] = $user['User']['id']; + } else { + $conditions['AuditLog.user_id'] = -1; + } + } + } + if (isset($params['ip'])) { + $conditions['AuditLog.ip'] = inet_pton($params['ip']); + } + if (isset($params['authkey_id'])) { + $conditions['AuditLog.authkey_id'] = $params['authkey_id']; + } + if (isset($params['request_id'])) { + $conditions['AuditLog.request_id'] = $params['request_id']; + } + if (isset($params['request_type'])) { + $conditions['AuditLog.request_type'] = $params['request_type']; + } + if (isset($params['model'])) { + $conditions['AuditLog.model'] = $params['model']; + } + if (isset($params['model_id'])) { + $conditions['AuditLog.model_id'] = $params['model_id']; + } + if (isset($params['event_id'])) { + $conditions['AuditLog.event_id'] = $params['event_id']; + } + if (isset($params['model_title'])) { + $conditions['AuditLog.model_title LIKE'] = '%' . $params['model_title'] . '%'; + } + if (isset($params['action'])) { + $conditions['AuditLog.action'] = $params['action']; + } + if (isset($params['org'])) { + if (is_numeric($params['org'])) { + $conditions['AuditLog.org_id'] = $params['org']; + } else { + $org = $this->AuditLog->Organisation->fetchOrg($params['org']); + if ($org) { + $conditions['AuditLog.org_id'] = $org['id']; + } else { + $conditions['AuditLog.org_id'] = -1; + } + } + } + if (isset($params['created'])) { + $tempData = is_array($params['created']) ? $params['created'] : [$params['created']]; + foreach ($tempData as $k => $v) { + $tempData[$k] = $this->AuditLog->resolveTimeDelta($v); + } + if (count($tempData) === 1) { + $conditions['AuditLog.created >='] = date("Y-m-d H:i:s", $tempData[0]); + } else { + if ($tempData[0] < $tempData[1]) { + $temp = $tempData[1]; + $tempData[1] = $tempData[0]; + $tempData[0] = $temp; + } + $conditions['AND'][] = ['AuditLog.created <=' => date("Y-m-d H:i:s", $tempData[0])]; + $conditions['AND'][] = ['AuditLog.created >=' => date("Y-m-d H:i:s", $tempData[1])]; + } + } + return $conditions; + } + + /** + * Create conditions that will include just events parts that user can see. + * @param array $event + * @return array + */ + private function __createEventIndexConditions(array $event) + { + if ($this->_isSiteAdmin() || $event['Event']['orgc_id'] == $this->Auth->user('org_id')) { + // Site admins and event owners can see all changes + return ['event_id' => $event['Event']['id']]; + } + + $event = $this->Event->fetchEvent($this->Auth->user(), [ + 'eventid' => $event['Event']['id'], + 'sgReferenceOnly' => 1, + 'deleted' => [0, 1], + 'deleted_proposals' => 1, + 'noSightings' => true, + 'includeEventCorrelations' => false, + 'excludeGalaxy' => true, + ])[0]; + + $attributeIds = []; + $objectIds = []; + $proposalIds = array_column($event['ShadowAttribute'], 'id'); + $objectReferenceId = []; + foreach ($event['Attribute'] as $aa) { + $attributeIds[] = $aa['id']; + if (!empty($aa['ShadowAttribute'])) { + foreach ($aa['ShadowAttribute'] as $sa) { + $proposalIds[] = $sa['id']; + } + } + } + unset($event['Attribute']); + foreach ($event['Object'] as $ob) { + foreach ($ob['Attribute'] as $aa) { + $attributeIds[] = $aa['id']; + if (!empty($aa['ShadowAttribute'])) { + foreach ($aa['ShadowAttribute'] as $sa) { + $proposalIds[] = $sa['id']; + } + } + } + foreach ($ob['ObjectReference'] as $or) { + $objectReferenceId[] = $or['id']; + } + $objectIds[] = $ob['id']; + } + unset($event['Object']); + + $conditions = []; + $conditions['AND']['event_id'] = $event['Event']['id']; + $conditions['AND']['OR'][] = ['model' => 'Event']; + + $parts = [ + 'Attribute' => $attributeIds, + 'ShadowAttribute' => $proposalIds, + 'Object' => $objectIds, + 'ObjectReference' => $objectReferenceId, + 'EventReport' => array_column($event['EventReport'], 'id'), + ]; + + foreach ($parts as $model => $modelIds) { + if (!empty($modelIds)) { + $conditions['AND']['OR'][] = [ + 'AND' => [ + 'model' => $model, + 'model_id' => $modelIds, + ], + ]; + } + } + + return $conditions; + } + + /** + * Generate link to model view if exists and use has permission to access it. + * @param array $auditLogs + * @return array + */ + private function __appendModelLinks(array $auditLogs) + { + $models = []; + foreach ($auditLogs as $auditLog) { + if (isset($models[$auditLog['AuditLog']['model']])) { + $models[$auditLog['AuditLog']['model']][] = $auditLog['AuditLog']['model_id']; + } else { + $models[$auditLog['AuditLog']['model']] = [$auditLog['AuditLog']['model_id']]; + } + } + + $eventIds = isset($models['Event']) ? $models['Event'] : []; + + if (isset($models['ObjectReference'])) { + $this->loadModel('ObjectReference'); + $objectReferences = $this->ObjectReference->find('list', [ + 'conditions' => ['ObjectReference.id' => array_unique($models['ObjectReference'])], + 'fields' => ['ObjectReference.id', 'ObjectReference.object_id'], + ]); + } + + if (isset($models['Object']) || isset($objectReferences)) { + $objectIds = array_unique(array_merge( + isset($models['Object']) ? $models['Object'] : [], + isset($objectReferences) ? array_values($objectReferences) : [] + )); + $this->loadModel('MispObject'); + $conditions = $this->MispObject->buildConditions($this->Auth->user()); + $conditions['Object.id'] = $objectIds; + $objects = $this->MispObject->find('all', [ + 'conditions' => $conditions, + 'contain' => ['Event'], + 'fields' => ['Object.id', 'Object.event_id', 'Object.uuid', 'Object.deleted'], + ]); + $objects = array_column(array_column($objects, 'Object'), null, 'id'); + $eventIds = array_merge($eventIds, array_column($objects, 'event_id')); + } + + if (isset($models['Attribute'])) { + $this->loadModel('Attribute'); + $attributes = $this->Attribute->fetchAttributesSimple($this->Auth->user(), [ + 'conditions' => ['Attribute.id' => array_unique($models['Attribute'])], + 'fields' => ['Attribute.id', 'Attribute.event_id', 'Attribute.uuid', 'Attribute.deleted'], + ]); + $attributes = array_column(array_column($attributes, 'Attribute'), null, 'id'); + $eventIds = array_merge($eventIds, array_column($attributes, 'event_id')); + } + + if (isset($models['ShadowAttribute'])) { + $this->loadModel('ShadowAttribute'); + $conditions = $this->ShadowAttribute->buildConditions($this->Auth->user()); + $conditions['AND'][] = ['ShadowAttribute.id' => array_unique($models['ShadowAttribute'])]; + $shadowAttributes = $this->ShadowAttribute->find('all', [ + 'conditions' => $conditions, + 'fields' => ['ShadowAttribute.id', 'ShadowAttribute.event_id', 'ShadowAttribute.uuid', 'ShadowAttribute.deleted'], + 'contain' => ['Event', 'Attribute'], + ]); + $shadowAttributes = array_column(array_column($shadowAttributes, 'ShadowAttribute'), null, 'id'); + $eventIds = array_merge($eventIds, array_column($shadowAttributes, 'event_id')); + } + + if (!empty($eventIds)) { + $this->loadModel('Event'); + $events = $this->Event->fetchSimpleEvents($this->Auth->user(), [ + 'conditions' => ['Event.id' => array_unique($eventIds)], + ]); + $events = array_column(array_column($events, 'Event'), null, 'id'); + } + + $existingObjects = []; + foreach (['User', 'Organisation', 'Galaxy', 'GalaxyCluster', 'Warninglist'] as $modelName) { + if (isset($models[$modelName])) { + $this->loadModel($modelName); + $data = $this->{$modelName}->find('column', [ + 'conditions' => ['id' => array_unique($models[$modelName])], + 'fields' => ['id'], + ]); + $existingObjects[$modelName] = array_flip($data); + } + } + + foreach ($auditLogs as $k => $auditLog) { + $auditLog = $auditLog['AuditLog']; + $modelId = $auditLog['model_id']; + $url = null; + $eventInfo = null; + switch ($auditLog['model']) { + case 'Event': + if (isset($events[$modelId])) { + $url = '/events/view/' . $modelId; + $eventInfo = $events[$modelId]['info']; + } + break; + case 'ObjectReference': + if (isset($objectReferences[$modelId]) && isset($objects[$objectReferences[$modelId]])) { + $url = '/events/view/' . $objects[$objectReferences[$modelId]]['event_id'] . '/focus:' . $objects[$objectReferences[$modelId]]['uuid']; + if ($objects[$objectReferences[$modelId]]['deleted']) { + $url .= '/deleted:2'; + } + if (isset($events[$objects[$objectReferences[$modelId]]['event_id']])) { + $eventInfo = $events[$objects[$objectReferences[$modelId]]['event_id']]['info']; + } + } + break; + case 'Object': + if (isset($objects[$modelId])) { + $url = '/events/view/' . $objects[$modelId]['event_id'] . '/focus:' . $objects[$modelId]['uuid']; + if ($objects[$modelId]['deleted']) { + $url .= '/deleted:2'; + } + if (isset($events[$objects[$modelId]['event_id']])) { + $eventInfo = $events[$objects[$modelId]['event_id']]['info']; + } + } + break; + case 'Attribute': + if (isset($attributes[$modelId])) { + $url = '/events/view/' . $attributes[$modelId]['event_id'] . '/focus:' . $attributes[$modelId]['uuid']; + if ($attributes[$modelId]['deleted']) { + $url .= '/deleted:2'; + } + if (isset($events[$attributes[$modelId]['event_id']])) { + $eventInfo = $events[$attributes[$modelId]['event_id']]['info']; + } + } + break; + case 'ShadowAttribute': + if (isset($shadowAttributes[$modelId])) { + $url = '/events/view/' . $shadowAttributes[$modelId]['event_id'] . '/focus:' . $shadowAttributes[$modelId]['uuid']; + if (isset($events[$shadowAttributes[$modelId]['event_id']])) { + $eventInfo = $events[$shadowAttributes[$modelId]['event_id']]['info']; + } + } + break; + case 'User': + if (isset($existingObjects['User'][$modelId])) { + $url = '/admin/users/view/' . $modelId; + } + break; + case 'Warninglist': + if (isset($existingObjects['Warninglist'][$modelId])) { + $url = '/warninglists/view/' . $modelId; + } + break; + case 'Organisation': + if (isset($existingObjects['Organisation'][$modelId])) { + $url = '/organisation/view/' . $modelId; + } + break; + case 'Galaxy': + if (isset($existingObjects['Galaxy'][$modelId])) { + $url = '/galaxies/view/' . $modelId; + } + break; + case 'GalaxyCluster': + if (isset($existingObjects['GalaxyCluster'][$modelId])) { + $url = '/galaxy_clusters/view/' . $modelId; + } + break; + default: + continue 2; + } + if ($url) { + $auditLogs[$k]['AuditLog']['model_link'] = $this->baseurl . $url; + } + if ($eventInfo) { + $auditLogs[$k]['AuditLog']['event_info'] = $eventInfo; + } + } + + return $auditLogs; + } + + +} diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php index 881f97927..539d9a59e 100644 --- a/app/Controller/Component/ACLComponent.php +++ b/app/Controller/Component/ACLComponent.php @@ -377,6 +377,10 @@ class ACLComponent extends Component 'testForStolenAttributes' => array(), 'pruneUpdateLogs' => array() ), + 'auditLogs' => [ + 'admin_index' => ['perm_audit'], + 'eventIndex' => ['*'], + ], 'modules' => array( 'index' => array('perm_auth'), 'queryEnrichment' => array('perm_auth'), diff --git a/app/Controller/LogsController.php b/app/Controller/LogsController.php index 8df9b7d10..7da3bf9a2 100644 --- a/app/Controller/LogsController.php +++ b/app/Controller/LogsController.php @@ -118,6 +118,8 @@ class LogsController extends AppController 'deleted_proposals' => 1, 'noSightings' => true, 'noEventReports' => true, + 'includeEventCorrelations' => false, + 'excludeGalaxy' => true, )); if (empty($event)) { throw new NotFoundException('Invalid event.'); @@ -125,7 +127,7 @@ class LogsController extends AppController $event = $event[0]; $attribute_ids = array(); $object_ids = array(); - $proposal_ids = array(); + $proposal_ids = array_column($event['ShadowAttribute'], 'id');; if (!empty($event['Attribute'])) { foreach ($event['Attribute'] as $aa) { $attribute_ids[] = $aa['id']; diff --git a/app/Model/AdminSetting.php b/app/Model/AdminSetting.php index ef1de4bb7..16bf772e0 100644 --- a/app/Model/AdminSetting.php +++ b/app/Model/AdminSetting.php @@ -6,6 +6,7 @@ class AdminSetting extends AppModel public $useTable = 'admin_settings'; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Allowedlist.php b/app/Model/Allowedlist.php index 21ab7c76b..aea47d01a 100644 --- a/app/Model/Allowedlist.php +++ b/app/Model/Allowedlist.php @@ -9,6 +9,7 @@ class Allowedlist extends AppModel public $displayField = 'name'; public $actsAs = array( + 'AuditLog', 'Trim', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Role', diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index 08bb47bc5..e007ef595 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -89,7 +89,8 @@ class AppModel extends Model 45 => false, 46 => false, 47 => false, 48 => false, 49 => false, 50 => false, 51 => false, 52 => false, 53 => false, 54 => false, 55 => false, 56 => false, 57 => false, 58 => false, 59 => false, 60 => false, 61 => false, 62 => false, - 63 => true, 64 => false, 65 => false, 66 => false, 67 => false, 68 => false + 63 => true, 64 => false, 65 => false, 66 => false, 67 => false, 68 => false, + 69 => false, ); public $advanced_updates_description = array( @@ -1578,6 +1579,27 @@ class AppModel extends Model case 68: $sqlArray[] = "ALTER TABLE `correlation_exclusions` ADD `comment` text DEFAULT NULL;"; break; + case 69: + $sqlArray[] = "CREATE TABLE IF NOT EXISTS `audit_logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `user_id` int(11) NOT NULL, + `org_id` int(11) NOT NULL, + `authkey_id` int(11) DEFAULT NULL, + `ip` varbinary(16) DEFAULT NULL, + `request_type` tinyint NOT NULL, + `request_id` varchar(255) DEFAULT NULL, + `action` varchar(20) NOT NULL, + `model` varchar(80) NOT NULL, + `model_id` int(11) NOT NULL, + `model_title` text DEFAULT NULL, + `event_id` int(11) NULL, + `change` blob, + PRIMARY KEY (`id`), + INDEX `event_id` (`event_id`), + INDEX `model_id` (`model_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + 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;'; diff --git a/app/Model/Attribute.php b/app/Model/Attribute.php index b82adc2d8..cd0eac241 100644 --- a/app/Model/Attribute.php +++ b/app/Model/Attribute.php @@ -23,6 +23,7 @@ class Attribute extends AppModel public $name = 'Attribute'; // TODO general public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/AttributeTag.php b/app/Model/AttributeTag.php index e80a763d3..ba1b45e14 100644 --- a/app/Model/AttributeTag.php +++ b/app/Model/AttributeTag.php @@ -7,7 +7,7 @@ App::uses('AppModel', 'Model'); */ class AttributeTag extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $validate = array( 'attribute_id' => array( diff --git a/app/Model/AuditLog.php b/app/Model/AuditLog.php new file mode 100644 index 000000000..fac78ad66 --- /dev/null +++ b/app/Model/AuditLog.php @@ -0,0 +1,323 @@ + 0, + 'bytes_compressed' => 0, + 'bytes_uncompressed' => 0, + ]; + + public $belongsTo = [ + 'User' => [ + 'className' => 'User', + 'foreignKey' => 'user_id', + ], + 'Event' => [ + 'className' => 'Event', + 'foreignKey' => 'event_id', + ], + 'Organisation' => [ + 'className' => 'Organisation', + 'foreignKey' => 'org_id', + ], + ]; + + public function __construct($id = false, $table = null, $ds = null) + { + parent::__construct($id, $table, $ds); + $this->compressionEnabled = Configure::read('MISP.log_new_audit_compress') && function_exists('brotli_compress'); + } + + public function afterFind($results, $primary = false) + { + foreach ($results as $key => $result) { + if (isset($result['AuditLog']['ip'])) { + $results[$key]['AuditLog']['ip'] = inet_ntop($result['AuditLog']['ip']); + } + if (isset($result['AuditLog']['change']) && $result['AuditLog']['change']) { + $results[$key]['AuditLog']['change'] = $this->decodeChange($result['AuditLog']['change']); + } + if (isset($result['AuditLog']['action']) && isset($result['AuditLog']['model']) && isset($result['AuditLog']['model_id'])) { + $results[$key]['AuditLog']['title'] = $this->generateUserFriendlyTitle($result['AuditLog']); + } + } + return $results; + } + + /** + * @param array $auditLog + * @return string + */ + private function generateUserFriendlyTitle(array $auditLog) + { + if (in_array($auditLog['action'], [self::ACTION_TAG, self::ACTION_TAG_LOCAL, self::ACTION_REMOVE_TAG, self::ACTION_REMOVE_TAG_LOCAL], true)) { + $attached = ($auditLog['action'] === self::ACTION_TAG || $auditLog['action'] === self::ACTION_TAG_LOCAL); + $local = ($auditLog['action'] === self::ACTION_TAG_LOCAL || $auditLog['action'] === self::ACTION_REMOVE_TAG_LOCAL) ? 'local' : 'global'; + if ($attached) { + return __('Attached %s tag "%s" to %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } else { + return __('Detached %s tag "%s" from %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } + } + + if (in_array($auditLog['action'], [self::ACTION_GALAXY, self::ACTION_GALAXY_LOCAL, self::ACTION_REMOVE_GALAXY, self::ACTION_REMOVE_GALAXY_LOCAL], true)) { + $attached = ($auditLog['action'] === self::ACTION_GALAXY || $auditLog['action'] === self::ACTION_GALAXY_LOCAL); + $local = ($auditLog['action'] === self::ACTION_GALAXY_LOCAL || $auditLog['action'] === self::ACTION_REMOVE_GALAXY_LOCAL) ? 'local' : 'global'; + if ($attached) { + return __('Attached %s galaxy cluster "%s" to %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } else { + return __('Detached %s galaxy cluster "%s" from %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } + } + + if (in_array($auditLog['model'], ['Attribute', 'Object', 'ShadowAttribute'], true)) { + $modelName = $auditLog['model'] === 'ShadowAttribute' ? 'Proposal' : $auditLog['model']; + $title = __('%s from Event #%s', $modelName, $auditLog['event_id']); + } else { + $title = "{$auditLog['model']} #{$auditLog['model_id']}"; + } + if (isset($auditLog['model_title']) && $auditLog['model_title']) { + $title .= ": {$auditLog['model_title']}"; + } + return $title; + } + + /** + * @param string $change + * @return array|string + * @throws JsonException + */ + private function decodeChange($change) + { + if (substr($change, 0, 4) === self::BROTLI_HEADER) { + $this->compressionStats['compressed']++; + if (function_exists('brotli_uncompress')) { + $this->compressionStats['bytes_compressed'] += strlen($change); + $change = brotli_uncompress(substr($change, 4)); + $this->compressionStats['bytes_uncompressed'] += strlen($change); + if ($change === false) { + return 'Compressed'; + } + } else { + return 'Compressed'; + } + } + return $this->jsonDecode($change); + } + + public function beforeValidate($options = array()) + { + if (isset($this->data['AuditLog']['change']) && !is_array($this->data['AuditLog']['change'])) { + $this->invalidate('change', 'Change field must be array'); + } + } + + public function beforeSave($options = array()) + { + if (!isset($this->data['AuditLog']['ip']) && Configure::read('MISP.log_client_ip')) { + $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; + if (isset($_SERVER[$ipHeader])) { + $this->data['AuditLog']['ip'] = inet_pton($_SERVER[$ipHeader]); + } + } + + if (!isset($this->data['AuditLog']['user_id'])) { + $this->data['AuditLog']['user_id'] = $this->userInfo()['id']; + } + + if (!isset($this->data['AuditLog']['org_id'])) { + $this->data['AuditLog']['org_id'] = $this->userInfo()['org_id']; + } + + if (!isset($this->data['AuditLog']['request_type'])) { + $this->data['AuditLog']['request_type'] = $this->userInfo()['request_type']; + } + + if (!isset($this->data['AuditLog']['authkey_id'])) { + $this->data['AuditLog']['authkey_id'] = $this->userInfo()['authkey_id']; + } + + if (!isset($this->data['AuditLog']['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) { + $this->data['AuditLog']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID']; + } + + // Truncate request_id + if (isset($this->data['AuditLog']['request_id']) && strlen($this->data['AuditLog']['request_id']) > 255) { + $this->data['AuditLog']['request_id'] = substr($this->data['AuditLog']['request_id'], 0, 255); + } + + // Truncate model title + if (isset($this->data['AuditLog']['model_title']) && mb_strlen($this->data['AuditLog']['model_title']) > 255) { + $this->data['AuditLog']['model_title'] = mb_substr($this->data['AuditLog']['model_title'], 0, 252) . '...'; + } + + $this->logData($this->data); + + if (isset($this->data['AuditLog']['change'])) { + $change = json_encode($this->data['AuditLog']['change'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($this->compressionEnabled && strlen($change) >= self::BROTLI_MIN_LENGTH) { + $change = self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT); + } + $this->data['AuditLog']['change'] = $change; + } + } + + /** + * @param array $data + * @return bool + */ + private function logData(array $data) + { + if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) { + $pubSubTool = $this->getPubSubTool(); + $pubSubTool->publish($data, 'audit', 'log'); + } + + $this->publishKafkaNotification('audit', $data, 'log'); + + if (Configure::read('Plugin.ElasticSearch_logging_enable')) { + // send off our logs to distributed /dev/null + $logIndex = Configure::read("Plugin.ElasticSearch_log_index"); + $elasticSearchClient = $this->getElasticSearchTool(); + $elasticSearchClient->pushDocument($logIndex, "log", $data); + } + + // write to syslogd as well if enabled + if ($this->syslog === null) { + if (Configure::read('Security.syslog')) { + $options = []; + $syslogToStdErr = Configure::read('Security.syslog_to_stderr'); + if ($syslogToStdErr !== null) { + $options['to_stderr'] = $syslogToStdErr; + } + $syslogIdent = Configure::read('Security.syslog_ident'); + if ($syslogIdent) { + $options['ident'] = $syslogIdent; + } + $this->syslog = new SysLog($options); + } else { + $this->syslog = false; + } + } + if ($this->syslog) { + $entry = $data['AuditLog']['action']; + $title = $this->generateUserFriendlyTitle($data['AuditLog']); + if ($title) { + $entry .= " -- $title"; + } + $this->syslog->write('info', $entry); + } + return true; + } + + /** + * @return array + */ + private function userInfo() + { + if ($this->user !== null) { + return $this->user; + } + + $this->user = ['id' => 0, 'org_id' => 0, 'authkey_id' => 0, 'request_type' => self::REQUEST_TYPE_DEFAULT]; + + $isShell = defined('CAKEPHP_SHELL') && CAKEPHP_SHELL; + if ($isShell) { + // do not start session for shell commands and fetch user info from configuration + $this->user['request_type'] = self::REQUEST_TYPE_CLI; + $currentUserId = Configure::read('CurrentUserId'); + if (!empty($currentUserId)) { + $this->user['id'] = $currentUserId; + $userFromDb = $this->User->find('first', [ + 'conditions' => ['User.id' => $currentUserId], + 'fields' => ['User.org_id'], + ]); + $this->user['org_id'] = $userFromDb['User']['org_id']; + } + } else { + App::uses('AuthComponent', 'Controller/Component'); + $authUser = AuthComponent::user(); + if (!empty($authUser)) { + $this->user['id'] = $authUser['id']; + $this->user['org_id'] = $authUser['org_id']; + if (isset($authUser['logged_by_authkey']) && $authUser['logged_by_authkey']) { + $this->user['request_type'] = self::REQUEST_TYPE_API; + } + if (isset($authUser['authkey_id'])) { + $this->user['authkey_id'] = $authUser['authkey_id']; + } + } + } + return $this->user; + } + + public function insert(array $data) + { + try { + $this->create(); + } catch (Exception $e) { + return; // Table is missing when updating, so this is intentional + } + if ($this->save($data) === false) { + throw new Exception($this->validationErrors); + } + } + + public function recompress() + { + $changes = $this->find('all', [ + 'fields' => ['AuditLog.id', 'AuditLog.change'], + 'recursive' => -1, + 'conditions' => ['length(AuditLog.change) >=' => self::BROTLI_MIN_LENGTH], + ]); + foreach ($changes as $change) { + $this->save($change, true, ['id', 'change']); + } + } +} diff --git a/app/Model/AuthKey.php b/app/Model/AuthKey.php index 5dbfb8ac2..87a71a385 100644 --- a/app/Model/AuthKey.php +++ b/app/Model/AuthKey.php @@ -11,6 +11,7 @@ class AuthKey extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Behavior/AuditLogBehavior.php b/app/Model/Behavior/AuditLogBehavior.php new file mode 100644 index 000000000..741ddd5e3 --- /dev/null +++ b/app/Model/Behavior/AuditLogBehavior.php @@ -0,0 +1,351 @@ + true, + 'lastpushedid' => true, + 'timestamp' => true, + 'revision' => true, + 'modified' => true, + 'date_modified' => true, // User + 'current_login' => true, // User + 'last_login' => true, // User + 'newsread' => true, // User + 'proposal_email_lock' => true, // Event + ]; + + private $modelInfo = [ + 'Event' => 'info', + 'User' => 'email', + 'Object' => 'name', + 'EventReport' => 'name', + 'Server' => 'name', + 'Feed' => 'name', + 'Role' => 'name', + 'SharingGroup' => 'name', + 'Tag' => 'name', + 'TagCollection' => 'name', + 'Taxonomy' => 'namespace', + 'Organisation' => 'name', + 'AdminSetting' => 'setting', + 'UserSetting' => 'setting', + 'Galaxy' => 'name', + 'GalaxyCluster' => 'value', + 'Warninglist' => 'name', + ]; + + public function __construct() + { + parent::__construct(); + $this->enabled = Configure::read('MISP.log_new_audit'); + } + + public function setup(Model $model, $config = []) + { + $this->config = $config; + // Generate model info for attribute and proposals + $attributeInfo = function (array $new, array $old) { + $category = isset($new['category']) ? $new['category'] : $old['category']; + $type = isset($new['type']) ? $new['type'] : $old['type']; + $value1 = trim(isset($new['value1']) ? $new['value1'] : $old['value1']); + $value2 = trim(isset($new['value2']) ? $new['value2'] : $old['value2']); + $value = $value1 . (empty($value2) ? '' : '|' . $value2); + return "$category/$type $value"; + }; + $this->modelInfo['Attribute'] = $attributeInfo; + $this->modelInfo['ShadowAttribute'] = $attributeInfo; + } + + public function beforeSave(Model $model, $options = []) + { + if (!$this->enabled) { + return true; + } + if ($model->id) { + $this->old = $model->find('first', [ + 'conditions' => [$model->alias . '.' . $model->primaryKey => $model->id], + 'recursive' => -1, + 'callbacks' => false, + ]); + } else { + $this->old = null; + } + return true; + } + + public function afterSave(Model $model, $created, $options = []) + { + if (!$this->enabled) { + return; + } + + if ($model->id) { + $id = $model->id; + } else if ($model->insertId) { + $id = $model->insertId; + } else { + $id = null; + } + + if ($created) { + $action = AuditLog::ACTION_ADD; + } else { + $action = AuditLog::ACTION_EDIT; + if (isset($model->data[$model->alias]['deleted'])) { + if ($model->data[$model->alias]['deleted']) { + $action = AuditLog::ACTION_SOFT_DELETE; + } else if (!$model->data[$model->alias]['deleted'] && $this->old[$model->alias]['deleted']) { + $action = AuditLog::ACTION_UNDELETE; + } + } + } + + $changedFields = $this->changedFields($model, isset($options['fieldList']) ? $options['fieldList'] : null); + if (empty($changedFields)) { + return; + } + + if ($model->name === 'Event') { + $eventId = $id; + } else if (isset($model->data[$model->alias]['event_id'])) { + $eventId = $model->data[$model->alias]['event_id']; + } else if (isset($this->old[$model->alias]['event_id'])) { + $eventId = $this->old[$model->alias]['event_id']; + } else { + $eventId = null; + } + + $modelTitle = null; + if (isset($this->modelInfo[$model->name])) { + $modelTitleField = $this->modelInfo[$model->name]; + if (is_callable($modelTitleField)) { + $modelTitle = $modelTitleField($model->data[$model->alias], isset($this->old[$model->alias]) ? $this->old[$model->alias] : []); + } else if (isset($model->data[$model->alias][$modelTitleField])) { + $modelTitle = $model->data[$model->alias][$modelTitleField]; + } else if ($this->old[$model->alias][$modelTitleField]) { + $modelTitle = $this->old[$model->alias][$modelTitleField]; + } + } + + $modelName = $model->name === 'MispObject' ? 'Object' : $model->name; + + if ($modelName === 'AttributeTag' || $modelName === 'EventTag') { + $action = $model->data[$model->alias]['local'] ? AuditLog::ACTION_TAG_LOCAL : AuditLog::ACTION_TAG; + $tagInfo = $this->getTagInfo($model, $model->data[$model->alias]['tag_id']); + if ($tagInfo) { + $modelTitle = $tagInfo['tag_name']; + if ($tagInfo['is_galaxy']) { + $action = $model->data[$model->alias]['local'] ? AuditLog::ACTION_GALAXY_LOCAL : AuditLog::ACTION_GALAXY; + if ($tagInfo['galaxy_cluster_name']) { + $modelTitle = $tagInfo['galaxy_cluster_name']; + } + } + } + $id = $modelName === 'AttributeTag' ? $model->data[$model->alias]['attribute_id'] : $model->data[$model->alias]['event_id']; + $modelName = $modelName === 'AttributeTag' ? 'Attribute' : 'Event'; + } + + if ($modelName === 'Event') { + if (isset($changedFields['published'][1]) && $changedFields['published'][1]) { + $action = AuditLog::ACTION_PUBLISH; + } else if (isset($changedFields['sighting_timestamp'][1]) && $changedFields['sighting_timestamp'][1]) { + $action = AuditLog::ACTION_PUBLISH_SIGHTINGS; + } + } + + $this->auditLog()->insert([ + 'action' => $action, + 'model' => $modelName, + 'model_id' => $id, + 'model_title' => $modelTitle, + 'event_id' => $eventId, + 'change' => $changedFields, + ]); + } + + public function beforeDelete(Model $model, $cascade = true) + { + if (!$this->enabled) { + return true; + } + $model->recursive = -1; + $model->read(); + return true; + } + + public function afterDelete(Model $model) + { + if (!$this->enabled) { + return; + } + if ($model->name === 'Event') { + $eventId = $model->id; + } else { + $eventId = isset($model->data[$model->alias]['event_id']) ? $model->data[$model->alias]['event_id'] : null; + } + + $modelTitle = null; + if (isset($this->modelInfo[$model->name])) { + $modelTitleField = $this->modelInfo[$model->name]; + if (is_callable($modelTitleField)) { + $modelTitle = $modelTitleField($model->data[$model->alias], []); + } else if (isset($model->data[$model->alias][$modelTitleField])) { + $modelTitle = $model->data[$model->alias][$modelTitleField]; + } + } + + $modelName = $model->name === 'MispObject' ? 'Object' : $model->name; + $action = AuditLog::ACTION_DELETE; + $id = $model->id; + + if ($modelName === 'AttributeTag' || $modelName === 'EventTag') { + $action = $model->data[$model->alias]['local'] ? AuditLog::ACTION_REMOVE_TAG_LOCAL : AuditLog::ACTION_REMOVE_TAG; + $tagInfo = $this->getTagInfo($model, $model->data[$model->alias]['tag_id']); + if ($tagInfo) { + $modelTitle = $tagInfo['tag_name']; + if ($tagInfo['is_galaxy']) { + $action = $model->data[$model->alias]['local'] ? AuditLog::ACTION_REMOVE_GALAXY_LOCAL : AuditLog::ACTION_REMOVE_GALAXY; + if ($tagInfo['galaxy_cluster_name']) { + $modelTitle = $tagInfo['galaxy_cluster_name']; + } + } + } + $id = $modelName === 'AttributeTag' ? $model->data[$model->alias]['attribute_id'] : $model->data[$model->alias]['event_id']; + $modelName = $modelName === 'AttributeTag' ? 'Attribute' : 'Event'; + } + + $this->auditLog()->insert([ + 'action' => $action, + 'model' => $modelName, + 'model_id' => $id, + 'model_title' => $modelTitle, + 'event_id' => $eventId, + 'change' => $this->changedFields($model), + ]); + } + + /** + * @param Model $model + * @param int $tagId + * @return array|null + */ + private function getTagInfo(Model $model, $tagId) + { + $tag = $model->Tag->find('first', [ + 'conditions' => ['Tag.id' => $tagId], + 'recursive' => -1, + 'fields' => ['Tag.name', 'Tag.is_galaxy'], + ]); + if (empty($tag)) { + return null; + } + + $galaxyClusterName = null; + if ($tag['Tag']['is_galaxy']) { + if (!isset($this->GalaxyCluster)) { + $this->GalaxyCluster = ClassRegistry::init('GalaxyCluster'); + } + $galaxyCluster = $this->GalaxyCluster->find('first', [ + 'conditions' => ['GalaxyCluster.tag_name' => $tag['Tag']['name']], + 'recursive' => -1, + 'fields' => ['GalaxyCluster.value'], + ]); + if (!empty($galaxyCluster)) { + $galaxyClusterName = $galaxyCluster['GalaxyCluster']['value']; + } + } + + return [ + 'tag_name' => $tag['Tag']['name'], + 'is_galaxy' => $tag['Tag']['is_galaxy'], + 'galaxy_cluster_name' => $galaxyClusterName, + ]; + } + + /** + * @param Model $model + * @param array|null $fieldsToSave + * @return array + */ + private function changedFields(Model $model, $fieldsToSave = null) + { + $dbFields = $model->schema(); + $changedFields = []; + foreach ($model->data[$model->alias] as $key => $value) { + if (isset($this->skipFields[$key])) { + continue; + } + if (!isset($dbFields[$key])) { + continue; + } + if ($fieldsToSave && !in_array($key, $fieldsToSave, true)) { + continue; + } + + if (isset($model->data[$model->alias][$model->primaryKey]) && isset($this->old[$model->alias][$key])) { + $old = $this->old[$model->alias][$key]; + } else { + $old = null; + } + + // Normalize + if (is_bool($old)) { + $old = $old ? 1 : 0; + } + if (is_bool($value)) { + $value = $value ? 1 : 0; + } + $dbType = $dbFields[$key]['type']; + if ($dbType === 'integer' || $dbType === 'tinyinteger' || $dbType === 'biginteger' || $dbType === 'boolean') { + $value = (int)$value; + if ($old !== null) { + $old = (int)$old; + } + } + + if ($value == $old) { + continue; + } + + if ($key === 'password' || $key === 'authkey') { + $value = '*****'; + if ($old !== null) { + $old = $value; + } + } + + if ($old === null) { + $changedFields[$key] = $value; + } else { + $changedFields[$key] = [$old, $value]; + } + } + + return $changedFields; + } + + /** + * @return AuditLog + */ + private function auditLog() + { + if ($this->AuditLog === null) { + $this->AuditLog = ClassRegistry::init('AuditLog'); + } + return $this->AuditLog; + } +} diff --git a/app/Model/Cerebrate.php b/app/Model/Cerebrate.php index 45aed6aa7..1ef22b1c1 100644 --- a/app/Model/Cerebrate.php +++ b/app/Model/Cerebrate.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class Cerebrate extends AppModel { public $actsAs = [ + 'AuditLog', 'SysLogLogable.SysLogLogable' => [ 'roleModel' => 'Role', 'roleKey' => 'role_id', diff --git a/app/Model/CorrelationExclusion.php b/app/Model/CorrelationExclusion.php index 0c2e92257..3fcb54575 100644 --- a/app/Model/CorrelationExclusion.php +++ b/app/Model/CorrelationExclusion.php @@ -9,6 +9,7 @@ class CorrelationExclusion extends AppModel public $key = 'misp:correlation_exclusions'; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Event.php b/app/Model/Event.php index 17198dee4..d458c3090 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -17,6 +17,7 @@ App::uses('SendEmailTemplate', 'Tools'); class Event extends AppModel { public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/EventBlocklist.php b/app/Model/EventBlocklist.php index 15f33da3e..d110f5b52 100644 --- a/app/Model/EventBlocklist.php +++ b/app/Model/EventBlocklist.php @@ -8,6 +8,7 @@ class EventBlocklist extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/EventDelegation.php b/app/Model/EventDelegation.php index 03e4330db..bd20e88d1 100644 --- a/app/Model/EventDelegation.php +++ b/app/Model/EventDelegation.php @@ -3,7 +3,7 @@ App::uses('AppModel', 'Model'); class EventDelegation extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $validate = array( 'event_id' => array( diff --git a/app/Model/EventReport.php b/app/Model/EventReport.php index 516b22d43..85ca89f80 100644 --- a/app/Model/EventReport.php +++ b/app/Model/EventReport.php @@ -7,6 +7,7 @@ App::uses('AppModel', 'Model'); class EventReport extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', diff --git a/app/Model/EventTag.php b/app/Model/EventTag.php index a04f06ff3..7bb62fc19 100644 --- a/app/Model/EventTag.php +++ b/app/Model/EventTag.php @@ -6,7 +6,7 @@ App::uses('AppModel', 'Model'); */ class EventTag extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $validate = array( 'event_id' => array( diff --git a/app/Model/Feed.php b/app/Model/Feed.php index e9d0b80b3..db3add8c5 100644 --- a/app/Model/Feed.php +++ b/app/Model/Feed.php @@ -5,7 +5,9 @@ App::uses('TmpFileTool', 'Tools'); class Feed extends AppModel { - public $actsAs = array('SysLogLogable.SysLogLogable' => array( + public $actsAs = array( + 'AuditLog', + 'SysLogLogable.SysLogLogable' => array( 'change' => 'full' ), 'Trim', diff --git a/app/Model/Galaxy.php b/app/Model/Galaxy.php index c821bee2d..d6384d51e 100644 --- a/app/Model/Galaxy.php +++ b/app/Model/Galaxy.php @@ -7,6 +7,7 @@ class Galaxy extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/GalaxyCluster.php b/app/Model/GalaxyCluster.php index d5fc533bf..fda10332d 100644 --- a/app/Model/GalaxyCluster.php +++ b/app/Model/GalaxyCluster.php @@ -13,6 +13,7 @@ class GalaxyCluster extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/GalaxyClusterBlocklist.php b/app/Model/GalaxyClusterBlocklist.php index 8834cdd0b..fdf87b1d2 100644 --- a/app/Model/GalaxyClusterBlocklist.php +++ b/app/Model/GalaxyClusterBlocklist.php @@ -7,6 +7,7 @@ class GalaxyClusterBlocklist extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/GalaxyClusterRelation.php b/app/Model/GalaxyClusterRelation.php index 246fad5cd..a90dfb21a 100644 --- a/app/Model/GalaxyClusterRelation.php +++ b/app/Model/GalaxyClusterRelation.php @@ -8,6 +8,7 @@ class GalaxyClusterRelation extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/GalaxyClusterRelationTag.php b/app/Model/GalaxyClusterRelationTag.php index acf8655ce..9a14b0f84 100644 --- a/app/Model/GalaxyClusterRelationTag.php +++ b/app/Model/GalaxyClusterRelationTag.php @@ -4,7 +4,7 @@ App::uses('AppModel', 'Model'); class GalaxyClusterRelationTag extends AppModel { public $useTable = 'galaxy_cluster_relation_tags'; - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $validate = array( 'galaxy_cluster_relation_id' => array( diff --git a/app/Model/GalaxyElement.php b/app/Model/GalaxyElement.php index 43a183bd4..e59081c1d 100644 --- a/app/Model/GalaxyElement.php +++ b/app/Model/GalaxyElement.php @@ -7,6 +7,7 @@ class GalaxyElement extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'Containable', ); diff --git a/app/Model/Log.php b/app/Model/Log.php index 15e47d124..6c8ede690 100644 --- a/app/Model/Log.php +++ b/app/Model/Log.php @@ -206,6 +206,9 @@ class Log extends AppModel */ public function createLogEntry($user, $action, $model, $modelId = 0, $title = '', $change = '') { + if (in_array($action, ['tag', 'galaxy', 'publish', 'publish_sightings'], true) && Configure::read('MISP.log_new_audit')) { + return; // Do not store tag changes when new audit is enabled + } if ($user === 'SYSTEM') { $user = array('Organisation' => array('name' => 'SYSTEM'), 'email' => 'SYSTEM', 'id' => 0); } else if (!is_array($user)) { diff --git a/app/Model/MispObject.php b/app/Model/MispObject.php index 84b0cddfd..dd4eb9fc3 100644 --- a/app/Model/MispObject.php +++ b/app/Model/MispObject.php @@ -15,6 +15,7 @@ class MispObject extends AppModel public $useTable = 'objects'; public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', diff --git a/app/Model/News.php b/app/Model/News.php index 1733b6bab..7e6f50565 100644 --- a/app/Model/News.php +++ b/app/Model/News.php @@ -3,7 +3,7 @@ App::uses('AppModel', 'Model'); class News extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $validate = array( 'message' => array( diff --git a/app/Model/ObjectReference.php b/app/Model/ObjectReference.php index 274f6d2f3..4319c97f6 100644 --- a/app/Model/ObjectReference.php +++ b/app/Model/ObjectReference.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class ObjectReference extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', diff --git a/app/Model/ObjectRelationship.php b/app/Model/ObjectRelationship.php index a08651b2e..3ac7fb0c7 100644 --- a/app/Model/ObjectRelationship.php +++ b/app/Model/ObjectRelationship.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class ObjectRelationship extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', diff --git a/app/Model/ObjectTemplate.php b/app/Model/ObjectTemplate.php index f721d48cb..69856c9de 100644 --- a/app/Model/ObjectTemplate.php +++ b/app/Model/ObjectTemplate.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class ObjectTemplate extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', diff --git a/app/Model/ObjectTemplateElement.php b/app/Model/ObjectTemplateElement.php index 9fec6acb4..120472bbc 100644 --- a/app/Model/ObjectTemplateElement.php +++ b/app/Model/ObjectTemplateElement.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class ObjectTemplateElement extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable' ); diff --git a/app/Model/OrgBlocklist.php b/app/Model/OrgBlocklist.php index a8dfa3df7..ba9c706e7 100644 --- a/app/Model/OrgBlocklist.php +++ b/app/Model/OrgBlocklist.php @@ -7,6 +7,7 @@ class OrgBlocklist extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Organisation.php b/app/Model/Organisation.php index f125049bc..d32b317b6 100644 --- a/app/Model/Organisation.php +++ b/app/Model/Organisation.php @@ -12,6 +12,7 @@ class Organisation extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Organisation', diff --git a/app/Model/Post.php b/app/Model/Post.php index aba8994c5..243e7630b 100644 --- a/app/Model/Post.php +++ b/app/Model/Post.php @@ -8,6 +8,7 @@ App::uses('AppModel', 'Model'); class Post extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Post', diff --git a/app/Model/Regexp.php b/app/Model/Regexp.php index f29b7043d..cbbbebac9 100644 --- a/app/Model/Regexp.php +++ b/app/Model/Regexp.php @@ -9,6 +9,7 @@ App::uses('AppModel', 'Model'); class Regexp extends AppModel { public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Role', 'roleKey' => 'role_id', diff --git a/app/Model/Role.php b/app/Model/Role.php index a48e7703e..394a1afa1 100644 --- a/app/Model/Role.php +++ b/app/Model/Role.php @@ -39,6 +39,7 @@ class Role extends AppModel ); public $actsAs = array( + 'AuditLog', 'Trim', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Role', diff --git a/app/Model/Server.php b/app/Model/Server.php index 2efd51651..d756da537 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -14,7 +14,9 @@ class Server extends AppModel public $name = 'Server'; - public $actsAs = array('SysLogLogable.SysLogLogable' => array( + public $actsAs = array( + 'AuditLog', + 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', 'change' => 'full' @@ -469,6 +471,7 @@ class Server extends AppModel public function pull($user, $id = null, $technique=false, $server, $jobId = false, $force = false) { if ($jobId) { + Configure::write('CurrentUserId', $user['id']); $job = ClassRegistry::init('Job'); $job->read(null, $jobId); $email = "Scheduled job"; diff --git a/app/Model/ShadowAttribute.php b/app/Model/ShadowAttribute.php index 26ca56687..19badca31 100644 --- a/app/Model/ShadowAttribute.php +++ b/app/Model/ShadowAttribute.php @@ -20,6 +20,7 @@ class ShadowAttribute extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/SharingGroup.php b/app/Model/SharingGroup.php index 266ab9c3d..950fa5971 100644 --- a/app/Model/SharingGroup.php +++ b/app/Model/SharingGroup.php @@ -9,6 +9,7 @@ App::uses('AppModel', 'Model'); class SharingGroup extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'SharingGroup', diff --git a/app/Model/SharingGroupOrg.php b/app/Model/SharingGroupOrg.php index 5580fc501..373150253 100644 --- a/app/Model/SharingGroupOrg.php +++ b/app/Model/SharingGroupOrg.php @@ -2,7 +2,7 @@ App::uses('AppModel', 'Model'); class SharingGroupOrg extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $belongsTo = array( 'SharingGroup' => array( diff --git a/app/Model/SharingGroupServer.php b/app/Model/SharingGroupServer.php index 6a1e56e51..7b162b428 100644 --- a/app/Model/SharingGroupServer.php +++ b/app/Model/SharingGroupServer.php @@ -3,7 +3,7 @@ App::uses('AppModel', 'Model'); class SharingGroupServer extends AppModel { - public $actsAs = array('Containable'); + public $actsAs = array('AuditLog', 'Containable'); public $belongsTo = array( 'SharingGroup' => array( diff --git a/app/Model/Sightingdb.php b/app/Model/Sightingdb.php index 4043b4b7b..834544039 100644 --- a/app/Model/Sightingdb.php +++ b/app/Model/Sightingdb.php @@ -15,6 +15,7 @@ class Sightingdb extends AppModel ); public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/SightingdbOrg.php b/app/Model/SightingdbOrg.php index b0b1729ca..0268d109e 100644 --- a/app/Model/SightingdbOrg.php +++ b/app/Model/SightingdbOrg.php @@ -17,6 +17,7 @@ class SightingdbOrg extends AppModel ); public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Tag.php b/app/Model/Tag.php index 8bf3a6c34..83972df44 100644 --- a/app/Model/Tag.php +++ b/app/Model/Tag.php @@ -12,6 +12,7 @@ class Tag extends AppModel public $displayField = 'name'; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Tag', 'roleKey' => 'tag_id', diff --git a/app/Model/TagCollection.php b/app/Model/TagCollection.php index 6b57e9085..19815f8d7 100644 --- a/app/Model/TagCollection.php +++ b/app/Model/TagCollection.php @@ -9,6 +9,7 @@ class TagCollection extends AppModel public $displayField = 'name'; public $actsAs = array( + 'AuditLog', 'Trim', 'SysLogLogable.SysLogLogable' => array( 'roleModel' => 'Role', diff --git a/app/Model/TagCollectionTag.php b/app/Model/TagCollectionTag.php index fde336c09..8baf03cb5 100644 --- a/app/Model/TagCollectionTag.php +++ b/app/Model/TagCollectionTag.php @@ -7,6 +7,7 @@ class TagCollectionTag extends AppModel public $useTable = 'tag_collection_tags'; public $actsAs = array( + 'AuditLog', 'Trim', 'SysLogLogable.SysLogLogable' => array( 'roleModel' => 'Role', diff --git a/app/Model/Taxonomy.php b/app/Model/Taxonomy.php index 6717f4030..df1808167 100644 --- a/app/Model/Taxonomy.php +++ b/app/Model/Taxonomy.php @@ -11,6 +11,7 @@ class Taxonomy extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'Containable', ); diff --git a/app/Model/Thread.php b/app/Model/Thread.php index 52dbb8904..bf283277e 100644 --- a/app/Model/Thread.php +++ b/app/Model/Thread.php @@ -5,6 +5,7 @@ App::uses('AppModel', 'Model'); class Thread extends AppModel { public $actsAs = array( + 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable 'roleModel' => 'Thread', diff --git a/app/Model/User.php b/app/Model/User.php index 19bb222a5..1c5260bfe 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -216,6 +216,7 @@ class User extends AppModel ); public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/UserSetting.php b/app/Model/UserSetting.php index 20c112e5f..2ab3bcfd8 100644 --- a/app/Model/UserSetting.php +++ b/app/Model/UserSetting.php @@ -7,6 +7,7 @@ class UserSetting extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'SysLogLogable.SysLogLogable' => array( 'userModel' => 'User', 'userKey' => 'user_id', diff --git a/app/Model/Warninglist.php b/app/Model/Warninglist.php index 91006a3f0..686fbe55c 100644 --- a/app/Model/Warninglist.php +++ b/app/Model/Warninglist.php @@ -13,6 +13,7 @@ class Warninglist extends AppModel public $recursive = -1; public $actsAs = array( + 'AuditLog', 'Containable', ); diff --git a/app/Plugin/SysLogLogable/Model/Behavior/SysLogLogableBehavior.php b/app/Plugin/SysLogLogable/Model/Behavior/SysLogLogableBehavior.php index ebd18fa96..61d7783c1 100644 --- a/app/Plugin/SysLogLogable/Model/Behavior/SysLogLogableBehavior.php +++ b/app/Plugin/SysLogLogable/Model/Behavior/SysLogLogableBehavior.php @@ -2,9 +2,15 @@ App::import('Lib', 'SysLog.SysLog'); // Audit, syslogd, extra -class SysLogLogableBehavior extends LogableBehavior { +class SysLogLogableBehavior extends LogableBehavior +{ + public function __construct() + { + parent::__construct(); + $this->defaults['enabled'] = !Configure::read('MISP.log_new_audit'); + } - function afterSave(Model $Model, $created, $options = array()) { + function afterSave(Model $Model, $created, $options = array()) { if (!$this->settings[$Model->alias]['enabled']) { return true; } @@ -264,6 +270,10 @@ class SysLogLogableBehavior extends LogableBehavior { $this->settings[$Model->alias] = array_merge($this->defaults, $config); $this->settings[$Model->alias]['ignore'][] = $Model->primaryKey; + if (!$this->settings[$Model->alias]['enabled']) { + return; + } + $this->Log = ClassRegistry::init('Log'); if ($this->settings[$Model->alias]['userModel'] != $Model->alias) { $this->UserModel = ClassRegistry::init($this->settings[$Model->alias]['userModel']); diff --git a/app/View/AuditLogs/admin_index.ctp b/app/View/AuditLogs/admin_index.ctp new file mode 100644 index 000000000..1f7a306f9 --- /dev/null +++ b/app/View/AuditLogs/admin_index.ctp @@ -0,0 +1,390 @@ +' . h($date) . ''; + } + } else if ($field === 'last_seen' || $field === 'first_seen') { + $ls_sec = intval($value / 1000000); // $ls is in micro (10^6) + $ls_micro = $value % 1000000; + $ls_micro = str_pad($ls_micro, 6, "0", STR_PAD_LEFT); + $ls = $ls_sec . '.' . $ls_micro; + $date = DateTime::createFromFormat('U.u', $ls)->format('Y-m-d\TH:i:s.u'); + return '' . h($date) . ''; + } + + if (mb_strlen($value) > 64) { + $value = mb_substr($value, 0, 64) . '...'; + } + return h(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); +}; + +$removeActions = [ + AuditLog::ACTION_DELETE => true, + AuditLog::ACTION_REMOVE_GALAXY_LOCAL => true, + AuditLog::ACTION_REMOVE_GALAXY => true, + AuditLog::ACTION_REMOVE_TAG => true, + AuditLog::ACTION_REMOVE_TAG_LOCAL => true, +]; + +?>
+

+
+
+
+ + +
+
+ Html->script('moment-with-locales'); + echo $this->Html->script('doT'); + echo $this->Html->script('extendext'); + echo $this->Html->css('query-builder.default'); + echo $this->Html->script('query-builder'); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('created') ?>Paginator->sort('user_id', __('User')) ?>Paginator->sort('ip', __('IP')) ?>Paginator->sort('org_id', __('Org')) ?>Paginator->sort('action') ?>ModelTitleChange
'; + } else if (isset($item['User']['email'])) { + echo '' . h($item['User']['email']) . ''; + + if ($item['AuditLog']['request_type'] == AuditLog::REQUEST_TYPE_CLI) { + echo ' '; + } else if ($item['AuditLog']['request_type'] == AuditLog::REQUEST_TYPE_API) { + $key = $item['AuditLog']['authkey_id'] ? ' ' . __('by auth key %s', h($item['AuditLog']['authkey_id'])) : ''; + echo ' '; + } + } else { + echo __('Deleted user #%s', h($item['AuditLog']['user_id'])); + } ?> + OrgImg->getOrgLogo($item, 24); + } else if ($item['AuditLog']['org_id'] != 0) { + echo __('Deleted org #%s', h($item['AuditLog']['org_id'])); + } + ?> + + + ' : '' ?> + + ' : '' ?> + $values) { + echo '' . h($field) . ': '; + if (isset($removeActions[$item['AuditLog']['action']])) { + echo '' . $formatValue($field, $values) . '
'; + } else { + if (is_array($values)) { + echo '' . $formatValue($field, $values[0]) . ' '; + $value = $values[1]; + } else { + $value = $values; + } + echo ' ' . $formatValue($field, $value) . '
'; + } + } + } + ?>
+

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

+ +
+ +element('/genericElements/SideMenu/side_menu', ['menuList' => 'logs', 'menuItem' => 'listAuditLogs']); + diff --git a/app/View/AuditLogs/event_index.ctp b/app/View/AuditLogs/event_index.ctp new file mode 100644 index 000000000..9fdbbdf0c --- /dev/null +++ b/app/View/AuditLogs/event_index.ctp @@ -0,0 +1,88 @@ + 64) { + $value = mb_substr($value, 0, 64) . '...'; + } + return h(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); +}; + +$removeActions = [ + AuditLog::ACTION_DELETE => true, + AuditLog::ACTION_REMOVE_GALAXY_LOCAL => true, + AuditLog::ACTION_REMOVE_GALAXY => true, + AuditLog::ACTION_REMOVE_TAG => true, + AuditLog::ACTION_REMOVE_TAG_LOCAL => true, +]; + +?>
+

+ + + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('created');?>Paginator->sort('user_id', __('User'));?>Paginator->sort('org_id', __('Org'));?>Paginator->sort('action');?>ModelTitleChange
OrgImg->getOrgLogo($item, 24) : '' ?> $values) { + echo '' . h($field) . ': '; + if (isset($removeActions[$item['AuditLog']['action']])) { + echo '' . $formatValue($values) . '
'; + } else { + if (is_array($values)) { + echo '' . $formatValue($values[0]) . ' '; + $value = $values[1]; + } else { + $value = $values; + } + echo ' ' . $formatValue($value) . '
'; + } + } + } + ?>
+

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

+ +
+element('/genericElements/SideMenu/side_menu', ['menuList' => 'event', 'menuItem' => 'eventLog', 'event' => $event, 'mayModify' => $mayModify]); + diff --git a/app/View/Elements/genericElements/SideMenu/side_menu.ctp b/app/View/Elements/genericElements/SideMenu/side_menu.ctp index 0f71f2329..8b1af2521 100644 --- a/app/View/Elements/genericElements/SideMenu/side_menu.ctp +++ b/app/View/Elements/genericElements/SideMenu/side_menu.ctp @@ -94,7 +94,7 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); )); echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'element_id' => 'eventLog', - 'url' => $baseurl . '/logs/event_index/' . $eventId, + 'url' => $baseurl . (Configure::read('MISP.log_new_audit') ? '/audit_logs/eventIndex/' : '/logs/event_index/') . $eventId, 'text' => __('View Event History') )); echo $divider; @@ -1024,6 +1024,12 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); 'url' => $baseurl . '/admin/logs/index', 'text' => __('List Logs') )); + echo $this->element('/genericElements/SideMenu/side_menu_link', array( + 'element_id' => 'listAuditLogs', + 'url' => $baseurl . '/admin/audit_logs/index', + 'text' => __('List Audit Logs'), + 'requirement' => Configure::read('MISP.log_new_audit'), + )); echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'url' => $baseurl . '/admin/logs/search', 'text' => __('Search Logs') diff --git a/app/View/Elements/global_menu.ctp b/app/View/Elements/global_menu.ctp index 3d272fc49..7a05cf6a4 100755 --- a/app/View/Elements/global_menu.ctp +++ b/app/View/Elements/global_menu.ctp @@ -459,13 +459,18 @@ ), array( 'type' => 'root', - 'text' => __('Audit'), + 'text' => __('Logs'), 'requirement' => $isAclAudit, 'children' => array( array( 'text' => __('List Logs'), 'url' => $baseurl . '/admin/logs/index' ), + array( + 'text' => __('List Audit Logs'), + 'url' => $baseurl . '/admin/audit_logs/index', + 'requirement' => Configure::read('MISP.log_new_audit'), + ), array( 'text' => __('Search Logs'), 'url' => $baseurl . '/admin/logs/search' diff --git a/app/View/Events/view.ctp b/app/View/Events/view.ctp index 3643c798c..6326b0f41 100644 --- a/app/View/Events/view.ctp +++ b/app/View/Events/view.ctp @@ -82,8 +82,12 @@ $contributorsContent = []; foreach ($contributors as $organisationId => $name) { $org = ['Organisation' => ['id' => $organisationId, 'name' => $name]]; - $link = $baseurl . "/logs/event_index/" . $event['Event']['id'] . '/' . h($name); - $contributorsContent[] = $this->OrgImg->getNameWithImg($org, $link); + if (Configure::read('MISP.log_new_audit')) { + $link = $baseurl . "/audit_logs/eventIndex/" . h($event['Event']['id']) . '/' . h($organisationId); + } else { + $link = $baseurl . "/logs/event_index/" . h($event['Event']['id']) . '/' . h($name); + } + $contributorsContent[] = $this->OrgImg->getNameWithImg($org, $link); } $table_data[] = array( 'key' => __('Contributors'), diff --git a/app/webroot/css/main.css b/app/webroot/css/main.css index 1b90dfb4c..ea257ba01 100644 --- a/app/webroot/css/main.css +++ b/app/webroot/css/main.css @@ -2732,3 +2732,8 @@ Query builder margin-top: 0; } +/* Fix text input for query builder */ +.query-builder .rule-value-container input[type="text"] { + padding: 4px !important; + height: 30px; +} diff --git a/db_schema.json b/db_schema.json index c8ff7461a..84760bf6d 100644 --- a/db_schema.json +++ b/db_schema.json @@ -384,6 +384,162 @@ "extra": "" } ], + "audit_logs": [ + { + "column_name": "id", + "is_nullable": "NO", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": null, + "extra": "auto_increment" + }, + { + "column_name": "created", + "is_nullable": "NO", + "data_type": "datetime", + "character_maximum_length": null, + "numeric_precision": null, + "collation_name": null, + "column_type": "datetime", + "column_default": null, + "extra": "" + }, + { + "column_name": "user_id", + "is_nullable": "NO", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": null, + "extra": "" + }, + { + "column_name": "org_id", + "is_nullable": "NO", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": null, + "extra": "" + }, + { + "column_name": "authkey_id", + "is_nullable": "NO", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": null, + "extra": "" + }, + { + "column_name": "ip", + "is_nullable": "YES", + "data_type": "varbinary", + "character_maximum_length": "16", + "numeric_precision": null, + "collation_name": null, + "column_type": "varbinary(16)", + "column_default": "NULL", + "extra": "" + }, + { + "column_name": "request_type", + "is_nullable": "NO", + "data_type": "tinyint", + "character_maximum_length": null, + "numeric_precision": "3", + "collation_name": null, + "column_type": "tinyint(4)", + "column_default": null, + "extra": "" + }, + { + "column_name": "request_id", + "is_nullable": "YES", + "data_type": "varchar", + "character_maximum_length": "255", + "numeric_precision": null, + "collation_name": "utf8mb4_unicode_ci", + "column_type": "varchar(255)", + "column_default": "NULL", + "extra": "" + }, + { + "column_name": "action", + "is_nullable": "NO", + "data_type": "varchar", + "character_maximum_length": "20", + "numeric_precision": null, + "collation_name": "utf8mb4_unicode_ci", + "column_type": "varchar(20)", + "column_default": null, + "extra": "" + }, + { + "column_name": "model", + "is_nullable": "NO", + "data_type": "varchar", + "character_maximum_length": "80", + "numeric_precision": null, + "collation_name": "utf8mb4_unicode_ci", + "column_type": "varchar(80)", + "column_default": null, + "extra": "" + }, + { + "column_name": "model_id", + "is_nullable": "NO", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": null, + "extra": "" + }, + { + "column_name": "model_title", + "is_nullable": "YES", + "data_type": "text", + "character_maximum_length": "65535", + "numeric_precision": null, + "collation_name": "utf8mb4_unicode_ci", + "column_type": "text", + "column_default": "NULL", + "extra": "" + }, + { + "column_name": "event_id", + "is_nullable": "YES", + "data_type": "int", + "character_maximum_length": null, + "numeric_precision": "10", + "collation_name": null, + "column_type": "int(11)", + "column_default": "NULL", + "extra": "" + }, + { + "column_name": "change", + "is_nullable": "YES", + "data_type": "blob", + "character_maximum_length": "65535", + "numeric_precision": null, + "collation_name": null, + "column_type": "blob", + "column_default": "NULL", + "extra": "" + } + ], "auth_keys": [ { "column_name": "id", @@ -7591,6 +7747,11 @@ "event_id": false, "tag_id": false }, + "audit_logs": { + "id": true, + "event_id": false, + "model_id": false + }, "auth_keys": { "id": true, "authkey_start": false, @@ -7997,5 +8158,5 @@ "id": true } }, - "db_version": "68" + "db_version": "69" }