2021-01-22 13:01:23 +01:00
|
|
|
<?php
|
|
|
|
App::uses('AppModel', 'Model');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @property Event $Event
|
|
|
|
* @property User $User
|
|
|
|
* @property Organisation $Organisation
|
|
|
|
*/
|
|
|
|
class AuditLog extends AppModel
|
|
|
|
{
|
2022-11-12 19:59:09 +01:00
|
|
|
const BROTLI_HEADER = "\xce\xb2\xcf\x81",
|
|
|
|
ZSTD_HEADER = "\x28\xb5\x2f\xfd";
|
|
|
|
const COMPRESS_MIN_LENGTH = 200;
|
2021-01-22 13:01:23 +01:00
|
|
|
|
|
|
|
const ACTION_ADD = 'add',
|
|
|
|
ACTION_EDIT = 'edit',
|
|
|
|
ACTION_SOFT_DELETE = 'soft_delete',
|
|
|
|
ACTION_DELETE = 'delete',
|
|
|
|
ACTION_UNDELETE = 'undelete',
|
|
|
|
ACTION_TAG = 'tag',
|
|
|
|
ACTION_TAG_LOCAL = 'tag_local',
|
|
|
|
ACTION_REMOVE_TAG = 'remove_tag',
|
|
|
|
ACTION_REMOVE_TAG_LOCAL = 'remove_local_tag',
|
|
|
|
ACTION_GALAXY = 'galaxy',
|
|
|
|
ACTION_GALAXY_LOCAL = 'galaxy_local',
|
|
|
|
ACTION_REMOVE_GALAXY = 'remove_galaxy',
|
|
|
|
ACTION_REMOVE_GALAXY_LOCAL = 'remove_local_galaxy',
|
|
|
|
ACTION_PUBLISH = 'publish',
|
|
|
|
ACTION_PUBLISH_SIGHTINGS = 'publish_sightings';
|
|
|
|
|
|
|
|
const REQUEST_TYPE_DEFAULT = 0,
|
|
|
|
REQUEST_TYPE_API = 1,
|
|
|
|
REQUEST_TYPE_CLI = 2;
|
|
|
|
|
|
|
|
public $actsAs = [
|
|
|
|
'Containable',
|
2022-07-20 15:56:30 +02:00
|
|
|
'LightPaginator'
|
2021-01-22 13:01:23 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
/** @var array|null */
|
|
|
|
private $user = null;
|
|
|
|
|
|
|
|
/** @var bool */
|
|
|
|
private $compressionEnabled;
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
/** @var bool */
|
|
|
|
private $pubToZmq;
|
|
|
|
|
|
|
|
/** @var bool */
|
|
|
|
private $logClientIp;
|
|
|
|
|
2021-01-22 13:01:23 +01:00
|
|
|
/**
|
|
|
|
* Null when not defined, false when not enabled
|
|
|
|
* @var Syslog|null|false
|
|
|
|
*/
|
|
|
|
private $syslog;
|
|
|
|
|
|
|
|
public $compressionStats = [
|
|
|
|
'compressed' => 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);
|
2022-11-12 19:59:09 +01:00
|
|
|
$this->compressionEnabled = Configure::read('MISP.log_new_audit_compress') &&
|
|
|
|
(function_exists('brotli_compress') || function_exists('zstd_compress'));
|
2021-10-31 10:46:28 +01:00
|
|
|
$this->pubToZmq = $this->pubToZmq('audit');
|
|
|
|
$this->logClientIp = Configure::read('MISP.log_client_ip');
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function afterFind($results, $primary = false)
|
|
|
|
{
|
2021-10-31 10:46:28 +01:00
|
|
|
foreach ($results as &$result) {
|
2021-01-22 13:01:23 +01:00
|
|
|
if (isset($result['AuditLog']['ip'])) {
|
2021-10-31 10:46:28 +01:00
|
|
|
$result['AuditLog']['ip'] = inet_ntop($result['AuditLog']['ip']);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
if (isset($result['AuditLog']['change']) && $result['AuditLog']['change']) {
|
2021-10-31 10:46:28 +01:00
|
|
|
$result['AuditLog']['change'] = $this->decodeChange($result['AuditLog']['change']);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
if (isset($result['AuditLog']['action']) && isset($result['AuditLog']['model']) && isset($result['AuditLog']['model_id'])) {
|
2021-10-31 10:46:28 +01:00
|
|
|
$result['AuditLog']['title'] = $this->generateUserFriendlyTitle($result['AuditLog']);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
2021-07-09 13:27:18 +02:00
|
|
|
$local = ($auditLog['action'] === self::ACTION_TAG_LOCAL || $auditLog['action'] === self::ACTION_REMOVE_TAG_LOCAL) ? __('local') : __('global');
|
2021-01-22 13:01:23 +01:00
|
|
|
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);
|
2021-07-09 13:27:18 +02:00
|
|
|
$local = ($auditLog['action'] === self::ACTION_GALAXY_LOCAL || $auditLog['action'] === self::ACTION_REMOVE_GALAXY_LOCAL) ? __('local') : __('global');
|
2021-01-22 13:01:23 +01:00
|
|
|
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']);
|
|
|
|
}
|
2021-11-05 21:44:19 +01:00
|
|
|
|
2021-01-22 13:01:23 +01:00
|
|
|
if (isset($auditLog['model_title']) && $auditLog['model_title']) {
|
2021-11-05 21:44:19 +01:00
|
|
|
if (isset($title)) {
|
|
|
|
$title .= ": {$auditLog['model_title']}";
|
|
|
|
return $title;
|
|
|
|
} else {
|
|
|
|
return $auditLog['model_title'];
|
|
|
|
}
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
2021-11-05 21:44:19 +01:00
|
|
|
return '';
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $change
|
|
|
|
* @return array|string
|
|
|
|
* @throws JsonException
|
|
|
|
*/
|
|
|
|
private function decodeChange($change)
|
|
|
|
{
|
2022-11-12 19:59:09 +01:00
|
|
|
$header = substr($change, 0, 4);
|
|
|
|
if ($header === self::ZSTD_HEADER) {
|
|
|
|
$this->compressionStats['compressed']++;
|
|
|
|
if (function_exists('zstd_uncompress')) {
|
|
|
|
$this->compressionStats['bytes_compressed'] += strlen($change);
|
|
|
|
$change = zstd_uncompress($change);
|
|
|
|
$this->compressionStats['bytes_uncompressed'] += strlen($change);
|
|
|
|
if ($change === false) {
|
|
|
|
return 'Compressed';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return 'Compressed';
|
|
|
|
}
|
|
|
|
} else if ($header === self::BROTLI_HEADER) {
|
2021-01-22 13:01:23 +01:00
|
|
|
$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';
|
|
|
|
}
|
|
|
|
}
|
2022-10-23 10:09:24 +02:00
|
|
|
return JsonTool::decode($change);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
{
|
2021-10-31 10:46:28 +01:00
|
|
|
$auditLog = &$this->data['AuditLog'];
|
|
|
|
if (!isset($auditLog['ip']) && $this->logClientIp) {
|
2021-01-22 13:01:23 +01:00
|
|
|
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR';
|
|
|
|
if (isset($_SERVER[$ipHeader])) {
|
2022-01-15 19:58:57 +01:00
|
|
|
$auditLog['ip'] = $_SERVER[$ipHeader];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (!isset($auditLog['user_id'])) {
|
|
|
|
$auditLog['user_id'] = $this->userInfo()['id'];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (!isset($auditLog['org_id'])) {
|
|
|
|
$auditLog['org_id'] = $this->userInfo()['org_id'];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (!isset($auditLog['request_type'])) {
|
|
|
|
$auditLog['request_type'] = $this->userInfo()['request_type'];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (!isset($auditLog['authkey_id'])) {
|
|
|
|
$auditLog['authkey_id'] = $this->userInfo()['authkey_id'];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (!isset($auditLog['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) {
|
|
|
|
$auditLog['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'];
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Truncate request_id
|
2021-10-31 10:46:28 +01:00
|
|
|
if (isset($auditLog['request_id']) && strlen($auditLog['request_id']) > 255) {
|
|
|
|
$auditLog['request_id'] = substr($auditLog['request_id'], 0, 255);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Truncate model title
|
2021-10-31 10:46:28 +01:00
|
|
|
if (isset($auditLog['model_title']) && mb_strlen($auditLog['model_title']) > 255) {
|
|
|
|
$auditLog['model_title'] = mb_substr($auditLog['model_title'], 0, 252) . '...';
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->logData($this->data);
|
|
|
|
|
2022-01-15 19:58:57 +01:00
|
|
|
if (isset($auditLog['ip'])) {
|
|
|
|
$auditLog['ip'] = inet_pton($auditLog['ip']); // convert to binary form to save into database
|
|
|
|
}
|
|
|
|
|
2021-10-31 10:46:28 +01:00
|
|
|
if (isset($auditLog['change'])) {
|
2022-01-15 19:58:57 +01:00
|
|
|
$change = JsonTool::encode($auditLog['change']);
|
2022-11-12 19:59:09 +01:00
|
|
|
if ($this->compressionEnabled && strlen($change) >= self::COMPRESS_MIN_LENGTH) {
|
|
|
|
if (function_exists('zstd_compress')) {
|
|
|
|
$change = zstd_compress($change, 4);
|
|
|
|
} else {
|
|
|
|
$change = self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT);
|
|
|
|
}
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
2021-10-31 10:46:28 +01:00
|
|
|
$auditLog['change'] = $change;
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $data
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
private function logData(array $data)
|
|
|
|
{
|
2021-10-31 10:46:28 +01:00
|
|
|
if ($this->pubToZmq) {
|
2021-01-22 13:01:23 +01:00
|
|
|
$pubSubTool = $this->getPubSubTool();
|
|
|
|
$pubSubTool->publish($data, 'audit', 'log');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->publishKafkaNotification('audit', $data, 'log');
|
|
|
|
|
2022-11-12 20:42:27 +01:00
|
|
|
// In future add support for sending logs to elastic
|
2021-01-22 13:01:23 +01:00
|
|
|
|
|
|
|
// 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";
|
|
|
|
}
|
2022-03-27 13:05:33 +02:00
|
|
|
$this->syslog->write(LOG_INFO, $entry);
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-02-27 09:23:19 +01:00
|
|
|
/**
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
2021-01-22 13:01:23 +01:00
|
|
|
public function insert(array $data)
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
$this->create();
|
|
|
|
} catch (Exception $e) {
|
|
|
|
return; // Table is missing when updating, so this is intentional
|
|
|
|
}
|
2022-02-27 09:23:19 +01:00
|
|
|
if ($this->save(['AuditLog' => $data], ['atomic' => false]) === false) {
|
2021-01-22 13:01:23 +01:00
|
|
|
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']);
|
|
|
|
}
|
|
|
|
}
|
2021-01-30 10:04:44 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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'];
|
|
|
|
}
|
|
|
|
|
2022-05-14 10:17:06 +02:00
|
|
|
if ($this->isMysql()) {
|
2021-01-30 10:04:44 +01:00
|
|
|
$validDates = $this->find('all', [
|
|
|
|
'recursive' => -1,
|
|
|
|
'fields' => ['DISTINCT UNIX_TIMESTAMP(DATE(created)) AS Date', 'count(id) AS count'],
|
|
|
|
'conditions' => $conditions,
|
|
|
|
'group' => ['Date'],
|
|
|
|
'order' => ['Date'],
|
2021-10-31 10:46:28 +01:00
|
|
|
'callbacks' => false,
|
2021-01-30 10:04:44 +01:00
|
|
|
]);
|
2022-05-14 10:17:06 +02:00
|
|
|
} else {
|
2021-01-30 10:04:44 +01:00
|
|
|
if (!empty($conditions['org_id'])) {
|
2021-08-19 14:58:29 +02:00
|
|
|
$condOrg = sprintf('WHERE org_id = %s', intval($conditions['org_id']));
|
2021-01-30 10:04:44 +01:00
|
|
|
} 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 = [];
|
2021-09-01 11:01:39 +02:00
|
|
|
foreach ($validDates as $date) {
|
2021-01-30 10:04:44 +01:00
|
|
|
$data[(int)$date[0]['Date']] = (int)$date[0]['count'];
|
|
|
|
}
|
|
|
|
return $data;
|
|
|
|
}
|
2021-01-22 13:01:23 +01:00
|
|
|
}
|