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'); $this->pubToZmq = $this->pubToZmq('audit'); $this->elasticLogging = Configure::read('Plugin.ElasticSearch_logging_enable'); $this->logClientIp = Configure::read('MISP.log_client_ip'); } public function afterFind($results, $primary = false) { foreach ($results as &$result) { if (isset($result['AuditLog']['ip'])) { $result['AuditLog']['ip'] = inet_ntop($result['AuditLog']['ip']); } if (isset($result['AuditLog']['change']) && $result['AuditLog']['change']) { $result['AuditLog']['change'] = $this->decodeChange($result['AuditLog']['change']); } if (isset($result['AuditLog']['action']) && isset($result['AuditLog']['model']) && isset($result['AuditLog']['model_id'])) { $result['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']); } if (isset($auditLog['model_title']) && $auditLog['model_title']) { if (isset($title)) { $title .= ": {$auditLog['model_title']}"; return $title; } else { return $auditLog['model_title']; } } return ''; } /** * @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()) { $auditLog = &$this->data['AuditLog']; if (!isset($auditLog['ip']) && $this->logClientIp) { $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; if (isset($_SERVER[$ipHeader])) { $auditLog['ip'] = $_SERVER[$ipHeader]; } } if (!isset($auditLog['user_id'])) { $auditLog['user_id'] = $this->userInfo()['id']; } if (!isset($auditLog['org_id'])) { $auditLog['org_id'] = $this->userInfo()['org_id']; } if (!isset($auditLog['request_type'])) { $auditLog['request_type'] = $this->userInfo()['request_type']; } if (!isset($auditLog['authkey_id'])) { $auditLog['authkey_id'] = $this->userInfo()['authkey_id']; } if (!isset($auditLog['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) { $auditLog['request_id'] = $_SERVER['HTTP_X_REQUEST_ID']; } // Truncate request_id if (isset($auditLog['request_id']) && strlen($auditLog['request_id']) > 255) { $auditLog['request_id'] = substr($auditLog['request_id'], 0, 255); } // Truncate model title if (isset($auditLog['model_title']) && mb_strlen($auditLog['model_title']) > 255) { $auditLog['model_title'] = mb_substr($auditLog['model_title'], 0, 252) . '...'; } $this->logData($this->data); if (isset($auditLog['ip'])) { $auditLog['ip'] = inet_pton($auditLog['ip']); // convert to binary form to save into database } if (isset($auditLog['change'])) { $change = JsonTool::encode($auditLog['change']); if ($this->compressionEnabled && strlen($change) >= self::BROTLI_MIN_LENGTH) { $change = self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT); } $auditLog['change'] = $change; } } /** * @param array $data * @return bool */ private function logData(array $data) { if ($this->pubToZmq) { $pubSubTool = $this->getPubSubTool(); $pubSubTool->publish($data, 'audit', 'log'); } $this->publishKafkaNotification('audit', $data, 'log'); if ($this->elasticLogging) { // 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(LOG_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; } /** * @throws Exception */ public function insert(array $data) { try { $this->create(); } catch (Exception $e) { return; // Table is missing when updating, so this is intentional } if ($this->save(['AuditLog' => $data], ['atomic' => false]) === 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']); } } /** * @param string|int $org * @return array */ public function returnDates($org = 'all') { $conditions = []; if ($org !== 'all') { $org = $this->Organisation->fetchOrg($org); if (empty($org)) { throw new NotFoundException('Invalid organisation.'); } $conditions['org_id'] = $org['id']; } $dataSource = ConnectionManager::getDataSource('default')->config['datasource']; if ($dataSource === 'Database/Mysql' || $dataSource === 'Database/MysqlObserver') { $validDates = $this->find('all', [ 'recursive' => -1, 'fields' => ['DISTINCT UNIX_TIMESTAMP(DATE(created)) AS Date', 'count(id) AS count'], 'conditions' => $conditions, 'group' => ['Date'], 'order' => ['Date'], 'callbacks' => false, ]); } elseif ($dataSource === 'Database/Postgres') { if (!empty($conditions['org_id'])) { $condOrg = sprintf('WHERE org_id = %s', intval($conditions['org_id'])); } else { $condOrg = ''; } $sql = 'SELECT DISTINCT EXTRACT(EPOCH FROM CAST(created AS DATE)) AS "Date", COUNT(id) AS count FROM audit_logs ' . $condOrg . ' GROUP BY "Date" ORDER BY "Date"'; $validDates = $this->query($sql); } $data = []; foreach ($validDates as $date) { $data[(int)$date[0]['Date']] = (int)$date[0]['count']; } return $data; } }