mirror of https://github.com/MISP/MISP
new: [logging] Access log
parent
4adbb2166b
commit
4aabc2d097
|
@ -33,6 +33,7 @@
|
|||
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('/access_logs/admin_index/*', array('controller' => 'access_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));
|
||||
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
App::uses('AppController', 'Controller');
|
||||
|
||||
/**
|
||||
* @property AccessLog $AccessLog
|
||||
*/
|
||||
class AccessLogsController extends AppController
|
||||
{
|
||||
public $components = [
|
||||
'RequestHandler',
|
||||
];
|
||||
|
||||
public $paginate = [
|
||||
'recursive' => -1,
|
||||
'limit' => 60,
|
||||
'fields' => ['id', 'created', 'user_id', 'org_id', 'authkey_id', 'ip', 'request_method', 'request_id', 'controller', 'action', 'url', 'response_code', 'memory_usage', 'duration'],
|
||||
'contain' => [
|
||||
'User' => ['fields' => ['id', 'email', 'org_id']],
|
||||
'Organisation' => ['fields' => ['id', 'name', 'uuid']],
|
||||
],
|
||||
'order' => [
|
||||
'AccessLog.id' => 'DESC'
|
||||
],
|
||||
];
|
||||
|
||||
public function admin_index()
|
||||
{
|
||||
$params = $this->IndexFilter->harvestParameters([
|
||||
'created',
|
||||
'ip',
|
||||
'user',
|
||||
'org',
|
||||
'request_id',
|
||||
'authkey_id',
|
||||
'api_request',
|
||||
'request_method',
|
||||
'controller',
|
||||
'action',
|
||||
'url',
|
||||
'response_code',
|
||||
]);
|
||||
|
||||
$conditions = $this->__searchConditions($params);
|
||||
|
||||
if ($this->_isRest()) {
|
||||
$list = $this->AccessLog->find('all', [
|
||||
'conditions' => $conditions,
|
||||
'contain' => $this->paginate['contain'],
|
||||
]);
|
||||
return $this->RestResponse->viewData($list, 'json');
|
||||
}
|
||||
|
||||
$this->paginate['conditions'] = $conditions;
|
||||
$list = $this->paginate();
|
||||
|
||||
$this->set('list', $list);
|
||||
$this->set('title_for_layout', __('Access logs'));
|
||||
}
|
||||
|
||||
public function admin_request($id)
|
||||
{
|
||||
$request = $this->AccessLog->find('first', [
|
||||
'conditions' => ['AccessLog.id' => $id],
|
||||
'fields' => ['AccessLog.request'],
|
||||
]);
|
||||
if (empty($request)) {
|
||||
throw new NotFoundException(__('Access log not found'));
|
||||
}
|
||||
|
||||
list($contentType, $encoding, $data) = explode("\n", $request['AccessLog']['request'], 3);
|
||||
$contentType = explode(';', $contentType, 2)[0];
|
||||
|
||||
if ($contentType === 'application/x-www-form-urlencoded') {
|
||||
parse_str($data, $output);
|
||||
$data = var_export($output, true);
|
||||
}
|
||||
|
||||
$this->set('request', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
private function __searchConditions(array $params)
|
||||
{
|
||||
$qbRules = [];
|
||||
foreach ($params as $key => $value) {
|
||||
if ($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 (is_numeric($params['user'])) {
|
||||
$conditions['AccessLog.user_id'] = $params['user'];
|
||||
} else {
|
||||
$user = $this->User->find('first', [
|
||||
'conditions' => ['User.email' => $params['user']],
|
||||
'fields' => ['id'],
|
||||
]);
|
||||
if (!empty($user)) {
|
||||
$conditions['AccessLog.user_id'] = $user['User']['id'];
|
||||
} else {
|
||||
$conditions['AccessLog.user_id'] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($params['ip'])) {
|
||||
$conditions['AccessLog.ip'] = inet_pton($params['ip']);
|
||||
}
|
||||
foreach (['authkey_id', 'request_id', 'controller', 'action'] as $field) {
|
||||
if (isset($params[$field])) {
|
||||
$conditions['AccessLog.' . $field] = $params[$field];
|
||||
}
|
||||
}
|
||||
if (isset($params['url'])) {
|
||||
$conditions['AccessLog.url LIKE'] = "%{$params['url']}%";
|
||||
}
|
||||
if (isset($params['request_method'])) {
|
||||
$methodId = array_flip(AccessLog::REQUEST_TYPES)[$params['request_method']] ?? -1;
|
||||
$conditions['AccessLog.request_method'] = $methodId;
|
||||
}
|
||||
if (isset($params['org'])) {
|
||||
if (is_numeric($params['org'])) {
|
||||
$conditions['AccessLog.org_id'] = $params['org'];
|
||||
} else {
|
||||
$org = $this->AccessLog->Organisation->fetchOrg($params['org']);
|
||||
if ($org) {
|
||||
$conditions['AccessLog.org_id'] = $org['id'];
|
||||
} else {
|
||||
$conditions['AccessLog.org_id'] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($params['created'])) {
|
||||
$tempData = is_array($params['created']) ? $params['created'] : [$params['created']];
|
||||
foreach ($tempData as $k => $v) {
|
||||
$tempData[$k] = $this->AccessLog->resolveTimeDelta($v);
|
||||
}
|
||||
if (count($tempData) === 1) {
|
||||
$conditions['AccessLog.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'][] = ['AccessLog.created <=' => date("Y-m-d H:i:s", $tempData[0])];
|
||||
$conditions['AND'][] = ['AccessLog.created >=' => date("Y-m-d H:i:s", $tempData[1])];
|
||||
}
|
||||
}
|
||||
return $conditions;
|
||||
}
|
||||
}
|
|
@ -665,27 +665,22 @@ class AppController extends Controller
|
|||
{
|
||||
$userMonitoringEnabled = Configure::read('Security.user_monitoring_enabled');
|
||||
if ($userMonitoringEnabled) {
|
||||
$redis = $this->User->setupRedis();
|
||||
$userMonitoringEnabled = $redis && $redis->sismember('misp:monitored_users', $user['id']);
|
||||
try {
|
||||
$userMonitoringEnabled = RedisTool::init()->sismember('misp:monitored_users', $user['id']);
|
||||
} catch (Exception $e) {
|
||||
$userMonitoringEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Configure::read('MISP.log_paranoid') || $userMonitoringEnabled) {
|
||||
$change = 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->request->here;
|
||||
if (
|
||||
(
|
||||
$this->request->is('post') ||
|
||||
$this->request->is('put')
|
||||
) &&
|
||||
(
|
||||
!empty(Configure::read('MISP.log_paranoid_include_post_body')) ||
|
||||
$userMonitoringEnabled
|
||||
)
|
||||
) {
|
||||
$payload = $this->request->input();
|
||||
$change .= PHP_EOL . 'Request body: ' . $payload;
|
||||
}
|
||||
$this->Log = ClassRegistry::init('Log');
|
||||
$this->Log->createLogEntry($user, 'request', 'User', $user['id'], 'Paranoid log entry', $change);
|
||||
$shouldBeLogged = $userMonitoringEnabled ||
|
||||
Configure::read('MISP.log_paranoid') ||
|
||||
(Configure::read('MISP.log_paranoid_api') && $user['logged_by_authkey']);
|
||||
|
||||
if ($shouldBeLogged) {
|
||||
$includeRequestBody = !empty(Configure::read('MISP.log_paranoid_include_post_body')) || $userMonitoringEnabled;
|
||||
/** @var AccessLog $accessLog */
|
||||
$accessLog = ClassRegistry::init('AccessLog');
|
||||
$accessLog->logRequest($user, $this->_remoteIp(), $this->request, $includeRequestBody);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -386,16 +386,20 @@ class ACLComponent extends Component
|
|||
'testForStolenAttributes' => array(),
|
||||
'pruneUpdateLogs' => array()
|
||||
),
|
||||
'auditLogs' => [
|
||||
'admin_index' => ['perm_audit'],
|
||||
'fullChange' => ['perm_audit'],
|
||||
'eventIndex' => ['*'],
|
||||
'returnDates' => ['*'],
|
||||
],
|
||||
'modules' => array(
|
||||
'index' => array('perm_auth'),
|
||||
'queryEnrichment' => array('perm_auth'),
|
||||
),
|
||||
'auditLogs' => [
|
||||
'admin_index' => ['perm_audit'],
|
||||
'fullChange' => ['perm_audit'],
|
||||
'eventIndex' => ['*'],
|
||||
'returnDates' => ['*'],
|
||||
],
|
||||
'accessLogs' => [
|
||||
'admin_index' => [],
|
||||
'admin_request' => [],
|
||||
],
|
||||
'modules' => array(
|
||||
'index' => array('perm_auth'),
|
||||
'queryEnrichment' => array('perm_auth'),
|
||||
),
|
||||
'news' => array(
|
||||
'add' => array(),
|
||||
'edit' => array(),
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
App::uses('AppModel', 'Model');
|
||||
|
||||
/**
|
||||
* @property Organisation $Organisation
|
||||
* @property User $User
|
||||
*/
|
||||
class AccessLog extends AppModel
|
||||
{
|
||||
const BROTLI_HEADER = "\xce\xb2\xcf\x81",
|
||||
ZSTD_HEADER = "\x28\xb5\x2f\xfd";
|
||||
const COMPRESS_MIN_LENGTH = 200;
|
||||
|
||||
const REQUEST_TYPES = [
|
||||
0 => 'Unknown',
|
||||
1 => 'GET',
|
||||
2 => 'HEAD',
|
||||
3 => 'POST',
|
||||
4 => 'PUT',
|
||||
5 => 'DELETE',
|
||||
6 => 'OPTIONS',
|
||||
7 => 'TRACE',
|
||||
8 => 'PATCH',
|
||||
];
|
||||
|
||||
public $actsAs = [
|
||||
'Containable',
|
||||
];
|
||||
|
||||
public $compressionStats = [
|
||||
'compressed' => 0,
|
||||
'bytes_compressed' => 0,
|
||||
'bytes_uncompressed' => 0,
|
||||
];
|
||||
|
||||
public $belongsTo = [
|
||||
'User' => [
|
||||
'className' => 'User',
|
||||
'foreignKey' => 'user_id',
|
||||
],
|
||||
'Organisation' => [
|
||||
'className' => 'Organisation',
|
||||
'foreignKey' => 'org_id',
|
||||
],
|
||||
];
|
||||
|
||||
public function afterFind($results, $primary = false)
|
||||
{
|
||||
foreach ($results as &$result) {
|
||||
if (isset($result['AccessLog']['ip'])) {
|
||||
$result['AccessLog']['ip'] = inet_ntop($result['AccessLog']['ip']);
|
||||
}
|
||||
if (isset($result['AccessLog']['request_method'])) {
|
||||
$result['AccessLog']['request_method'] = self::REQUEST_TYPES[$result['AccessLog']['request_method']];
|
||||
}
|
||||
if (!empty($result['AccessLog']['request'])) {
|
||||
$result['AccessLog']['request'] = $this->decodeRequest($result['AccessLog']['request']);
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function beforeSave($options = [])
|
||||
{
|
||||
$accessLog = &$this->data['AccessLog'];
|
||||
|
||||
$this->externalLog($accessLog);
|
||||
|
||||
if (Configure::read('MISP.log_paranoid_skip_db')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate
|
||||
foreach (['request_id', 'user_agent', 'url'] as $field) {
|
||||
if (isset($accessLog[$field]) && strlen($accessLog[$field]) > 255) {
|
||||
$accessLog[$field] = substr($accessLog[$field], 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($accessLog['ip'])) {
|
||||
$accessLog['ip'] = inet_pton($accessLog['ip']);
|
||||
}
|
||||
|
||||
if (isset($accessLog['request_method'])) {
|
||||
$requestMethodIds = array_flip(self::REQUEST_TYPES);
|
||||
$accessLog['request_method'] = $requestMethodIds[$accessLog['request_method']] ?? 0;
|
||||
}
|
||||
|
||||
if (isset($accessLog['request'])) {
|
||||
$accessLog['request'] = $this->encodeRequest($accessLog['request']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $user
|
||||
* @param string $remoteIp
|
||||
* @param CakeRequest $request
|
||||
* @param bool $includeRequestBody
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function logRequest(array $user, $remoteIp, CakeRequest $request, $includeRequestBody = true)
|
||||
{
|
||||
$requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
|
||||
$now = DateTime::createFromFormat('U.u', $requestTime);
|
||||
$logClientIp = Configure::read('MISP.log_client_ip');
|
||||
|
||||
$dataToSave = [
|
||||
'created' => $now->format('Y-m-d H:i:s.u'),
|
||||
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null,
|
||||
'user_id' => (int)$user['id'],
|
||||
'org_id' => (int)$user['org_id'],
|
||||
'authkey_id' => isset($user['authkey_id']) ? (int)$user['authkey_id'] : null,
|
||||
'ip' => $logClientIp ? $remoteIp : null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'],
|
||||
'controller' => $request->params['controller'],
|
||||
'action' => $request->params['action'],
|
||||
'url' => $request->here,
|
||||
];
|
||||
|
||||
if ($includeRequestBody && $request->is(['post', 'put', 'delete'])) {
|
||||
$requestContentType = $_SERVER['CONTENT_TYPE'] ?? null;
|
||||
$requestEncoding = $_SERVER['HTTP_CONTENT_ENCODING'] ?? null;
|
||||
$dataToSave['request'] = "$requestContentType\n$requestEncoding\n{$request->input()}";
|
||||
}
|
||||
|
||||
// Save data on shutdown
|
||||
register_shutdown_function(function () use ($dataToSave, $requestTime) {
|
||||
session_write_close(); // close session to allow concurrent requests
|
||||
$this->saveOnShutdown($dataToSave, $requestTime);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param float $requestTime
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveOnShutdown(array $data, $requestTime)
|
||||
{
|
||||
$data['response_code'] = http_response_code();
|
||||
$data['memory_usage'] = memory_get_peak_usage();
|
||||
$data['duration'] = (int)((microtime(true) - $requestTime) * 1000);
|
||||
|
||||
try {
|
||||
return $this->save($data, ['atomic' => false]);
|
||||
} catch (Exception $e) {
|
||||
$this->logException("Could not insert access log to database", $e, LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function externalLog(array $data)
|
||||
{
|
||||
if ($this->pubToZmq('audit')) {
|
||||
$this->getPubSubTool()->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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $request
|
||||
* @return string
|
||||
*/
|
||||
private function decodeRequest($request)
|
||||
{
|
||||
$header = substr($request, 0, 4);
|
||||
if ($header === self::BROTLI_HEADER) {
|
||||
$this->compressionStats['compressed']++;
|
||||
if (function_exists('brotli_uncompress')) {
|
||||
$this->compressionStats['bytes_compressed'] += strlen($request);
|
||||
$request = brotli_uncompress(substr($request, 4));
|
||||
$this->compressionStats['bytes_uncompressed'] += strlen($request);
|
||||
if ($request === false) {
|
||||
return 'Compressed';
|
||||
}
|
||||
} else {
|
||||
return 'Compressed';
|
||||
}
|
||||
} elseif ($header === self::ZSTD_HEADER) {
|
||||
$this->compressionStats['compressed']++;
|
||||
if (function_exists('zstd_uncompress')) {
|
||||
$this->compressionStats['bytes_compressed'] += strlen($request);
|
||||
$request = zstd_uncompress($request);
|
||||
$this->compressionStats['bytes_uncompressed'] += strlen($request);
|
||||
if ($request === false) {
|
||||
return 'Compressed';
|
||||
}
|
||||
} else {
|
||||
return 'Compressed';
|
||||
}
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $request
|
||||
* @return string
|
||||
*/
|
||||
private function encodeRequest($request)
|
||||
{
|
||||
$compressionEnabled = Configure::read('MISP.log_new_audit_compress') &&
|
||||
(function_exists('brotli_compress') || function_exists('zstd_compress'));
|
||||
|
||||
if ($compressionEnabled && strlen($request) >= self::COMPRESS_MIN_LENGTH) {
|
||||
if (function_exists('zstd_compress')) {
|
||||
return zstd_compress($request, 4);
|
||||
} else {
|
||||
return self::BROTLI_HEADER . brotli_compress($request, 4, BROTLI_TEXT);
|
||||
}
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ class AppModel extends Model
|
|||
81 => false, 82 => false, 83 => false, 84 => false, 85 => false, 86 => false,
|
||||
87 => false, 88 => false, 89 => false, 90 => false, 91 => false, 92 => false,
|
||||
93 => false, 94 => false, 95 => true, 96 => false, 97 => true, 98 => false,
|
||||
99 => false
|
||||
99 => false, 100 => false,
|
||||
);
|
||||
|
||||
const ADVANCED_UPDATES_DESCRIPTION = array(
|
||||
|
@ -1882,6 +1882,28 @@ class AppModel extends Model
|
|||
$sqlArray[] = "ALTER TABLE `event_tags` ADD `relationship_type` varchar(191) NULL DEFAULT '';";
|
||||
$sqlArray[] = "ALTER TABLE `attribute_tags` ADD `relationship_type` varchar(191) NULL DEFAULT '';";
|
||||
break;
|
||||
case 100:
|
||||
$sqlArray[] = "CREATE TABLE IF NOT EXISTS `access_logs` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime(4) 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_method` tinyint NOT NULL,
|
||||
`user_agent` varchar(255) DEFAULT NULL,
|
||||
`request_id` varchar(255) DEFAULT NULL,
|
||||
`controller` varchar(20) NOT NULL,
|
||||
`action` varchar(20) NOT NULL,
|
||||
`url` varchar(255) NOT NULL,
|
||||
`request` blob,
|
||||
`response_code` smallint NOT NULL,
|
||||
`memory_usage` int(11) NOT NULL,
|
||||
`duration` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `user_id` (`user_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;';
|
||||
|
|
|
@ -145,9 +145,6 @@ class Log extends AppModel
|
|||
}
|
||||
}
|
||||
$this->logData($this->data);
|
||||
if ($this->data['Log']['action'] === 'request' && !empty(Configure::read('MISP.log_paranoid_skip_db'))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -243,9 +240,6 @@ class Log extends AppModel
|
|||
]]);
|
||||
|
||||
if (!$result) {
|
||||
if ($action === 'request' && !empty(Configure::read('MISP.log_paranoid_skip_db'))) {
|
||||
return null;
|
||||
}
|
||||
if (!empty(Configure::read('MISP.log_skip_db_logs_completely'))) {
|
||||
return null;
|
||||
}
|
||||
|
@ -349,9 +343,8 @@ class Log extends AppModel
|
|||
|
||||
public function logData($data)
|
||||
{
|
||||
if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) {
|
||||
$pubSubTool = $this->getPubSubTool();
|
||||
$pubSubTool->publish($data, 'audit', 'log');
|
||||
if ($this->pubToZmq('audit')) {
|
||||
$this->getPubSubTool()->publish($data, 'audit', 'log');
|
||||
}
|
||||
|
||||
$this->publishKafkaNotification('audit', $data, 'log');
|
||||
|
@ -363,11 +356,6 @@ class Log extends AppModel
|
|||
$elasticSearchClient->pushDocument($logIndex, "log", $data);
|
||||
}
|
||||
|
||||
// Do not save request action logs to syslog, because they contain no information
|
||||
if ($data['Log']['action'] === 'request') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// write to syslogd as well if enabled
|
||||
if ($this->syslog === null) {
|
||||
if (Configure::read('Security.syslog')) {
|
||||
|
|
|
@ -5576,6 +5576,14 @@ class Server extends AppModel
|
|||
'type' => 'boolean',
|
||||
'null' => true
|
||||
),
|
||||
'log_paranoid_api' => array(
|
||||
'level' => 0,
|
||||
'description' => __('If this functionality is enabled all API requests will be logged.'),
|
||||
'value' => false,
|
||||
'test' => 'testBoolFalse',
|
||||
'type' => 'boolean',
|
||||
'null' => true
|
||||
),
|
||||
'log_paranoid_skip_db' => array(
|
||||
'level' => 0,
|
||||
'description' => __('You can decide to skip the logging of the paranoid logs to the database.'),
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
<div class="logs index">
|
||||
<h2><?= __('Access logs') ?></h2>
|
||||
<div>
|
||||
<div id="builder"></div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 5px;">
|
||||
<button id="qbSubmit" type="button" class="btn btn-success" style="margin-right: 5px;"> <i class="fa fa-filter"></i> <?= __('Filter'); ?></button>
|
||||
<button id="qbClear" type="button" class="btn btn-xs btn-danger" title="<?= __('Clear filtering rules'); ?>"> <i class="fa fa-times"></i> <?= __('Clear'); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
echo $this->Html->script('moment.min');
|
||||
echo $this->Html->script('doT');
|
||||
echo $this->Html->script('extendext');
|
||||
echo $this->Html->css('query-builder.default');
|
||||
echo $this->Html->script('query-builder');
|
||||
?>
|
||||
<script>
|
||||
var qbOptions = {
|
||||
plugins: {
|
||||
'unique-filter': null,
|
||||
'filter-description' : {
|
||||
mode: 'inline'
|
||||
},
|
||||
},
|
||||
conditions: ['AND'],
|
||||
allow_empty: true,
|
||||
filters: [
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
type: 'date',
|
||||
operators: ['greater_or_equal', 'between'],
|
||||
validation: {
|
||||
format: 'YYYY-MM-DD'
|
||||
},
|
||||
plugin: 'datepicker',
|
||||
plugin_config: {
|
||||
format: 'yyyy-mm-dd',
|
||||
todayBtn: 'linked',
|
||||
todayHighlight: true,
|
||||
autoclose: true
|
||||
}
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "ip",
|
||||
label: "IP",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "user",
|
||||
label: "User",
|
||||
description: "User ID or mail",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "integer",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "authkey_id",
|
||||
label: "Authentication key ID",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "request_id",
|
||||
label: "Request ID",
|
||||
description: "Request ID from X-Request-ID HTTP header",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "org",
|
||||
label: "Organisation",
|
||||
description: "Organisation ID, UUID or name",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "request_method",
|
||||
label: "HTTP request method",
|
||||
values: ["GET", "HEAD", "POST", "PUT", "DELETE"],
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "integer",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "response_code",
|
||||
label: "HTTP response code",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "controller",
|
||||
label: "Controller",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"equal",
|
||||
],
|
||||
unique: true,
|
||||
id: "action",
|
||||
label: "Action",
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
type: "string",
|
||||
operators: [
|
||||
"contains",
|
||||
],
|
||||
unique: true,
|
||||
id: "url",
|
||||
label: "URL",
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
condition: 'AND',
|
||||
not: false,
|
||||
rules: <?= JsonTool::encode($qbRules) ?>,
|
||||
flags: {
|
||||
no_add_group: true,
|
||||
condition_readonly: true,
|
||||
}
|
||||
},
|
||||
icons: {
|
||||
add_group: 'fa fa-plus-square',
|
||||
add_rule: 'fa fa-plus-circle',
|
||||
remove_group: 'fa fa-minus-square',
|
||||
remove_rule: 'fa fa-minus-circle',
|
||||
error: 'fa fa-exclamation-triangle'
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
var $builder = $('#builder');
|
||||
|
||||
// Fix for Bootstrap Datepicker
|
||||
$builder.on('afterUpdateRuleValue.queryBuilder', function (e, rule) {
|
||||
if (rule.filter.plugin === 'datepicker') {
|
||||
rule.$el.find('.rule-value-container input').datepicker('update');
|
||||
}
|
||||
});
|
||||
|
||||
var queryBuilder = $builder.queryBuilder(qbOptions);
|
||||
queryBuilder = queryBuilder[0].queryBuilder;
|
||||
|
||||
$('#qbClear').off('click').on('click', function () {
|
||||
queryBuilder.reset();
|
||||
});
|
||||
|
||||
// Submit on enter
|
||||
$builder.on('keyup', 'input[type=text], select', function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
$('#qbSubmit').click();
|
||||
}
|
||||
});
|
||||
|
||||
$('#qbSubmit').off('click').on('click', function () {
|
||||
var rules = queryBuilder.getRules({skip_empty: true});
|
||||
passedArgs = [];
|
||||
for (var key in rules.rules) {
|
||||
var rule = rules.rules[key];
|
||||
var k = rule.id;
|
||||
var v = rule.value;
|
||||
if (Array.isArray(v)) {
|
||||
v = v.join('||');
|
||||
}
|
||||
passedArgs[k] = v;
|
||||
}
|
||||
|
||||
var url = here;
|
||||
for (var key in passedArgs) {
|
||||
if (typeof key === 'number') {
|
||||
url += "/" + passedArgs[key];
|
||||
} else if (key !== 'page') {
|
||||
url += "/" + key + ":" + encodeURIComponent(passedArgs[key]);
|
||||
}
|
||||
}
|
||||
window.location.href = url;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="pagination">
|
||||
<ul>
|
||||
<?php
|
||||
$paginator = $this->LightPaginator->prev('« ' . __('previous'), array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'prev disabled', 'escape' => false, 'disabledTag' => 'span'));
|
||||
$paginator .= $this->LightPaginator->numbers(array('modulus' => 20, 'separator' => '', 'tag' => 'li', 'currentClass' => 'active', 'currentTag' => 'span'));
|
||||
$paginator .= $this->LightPaginator->next(__('next') . ' »', array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'next disabled', 'escape' => false, 'disabledTag' => 'span'));
|
||||
echo $paginator;
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-striped table-hover table-condensed">
|
||||
<tr>
|
||||
<th><?= $this->LightPaginator->sort('created') ?></th>
|
||||
<th><?= $this->LightPaginator->sort('user_id', __('User')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('ip', __('IP')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('org_id', __('Org')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('request_method', __('Method')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('url', __('URL')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('response_code', __('Code')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('memory_usage', __('Memory')) ?></th>
|
||||
<th><?= $this->LightPaginator->sort('duration', __('Duration')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($list as $item): ?>
|
||||
<tr>
|
||||
<td class="short"><?= $this->Time->time($item['AccessLog']['created']); ?></td>
|
||||
<td class="short" data-search="user" data-search-value="<?= h($item['AccessLog']['user_id']) ?>"><?php
|
||||
if (isset($item['User']['email'])) {
|
||||
echo '<a href="' . $baseurl . '/admin/users/view/' . h($item['User']['id']) . '">' . h($item['User']['email']) . '</a>';
|
||||
} else {
|
||||
echo __('<i>Deleted user #%s</i>', h($item['AccessLog']['user_id']));
|
||||
}
|
||||
|
||||
if (!empty($item['AccessLog']['authkey_id'])) {
|
||||
echo ' <i class="fas fa-cogs" title="' . __('Request trough API by auth key #%s', h($item['AccessLog']['authkey_id'])) . '"></i>';
|
||||
}
|
||||
?></td>
|
||||
<td class="short" data-search="ip" data-search-value="<?= h($item['AccessLog']['ip']) ?>"><?= h($item['AccessLog']['ip']) ?></td>
|
||||
<td class="short" data-search="org" data-search-value="<?= h($item['AccessLog']['org_id']) ?>">
|
||||
<?php if (isset($item['Organisation']) && $item['Organisation']['id']) {
|
||||
echo $this->OrgImg->getOrgLogo($item, 24);
|
||||
} else if ($item['AccessLog']['org_id'] != 0) {
|
||||
echo __('<i>Deleted org #%s</i>', h($item['AccessLog']['org_id']));
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td class="short" data-search="request_method" data-search-value="<?= h($item['AccessLog']['request_method']) ?>">
|
||||
<?= h($item['AccessLog']['request_method']) ?>
|
||||
<?= in_array($item['AccessLog']['request_method'], ['POST', 'PUT']) ? ' <a href="#" class="far fa-file request" data-log-id="' . h($item['AccessLog']['id']) . '"></i>' : '' ?>
|
||||
</td>
|
||||
<td class="short" title="<?= __('Controller: %s, action: %s', h($item['AccessLog']['controller']), h($item['AccessLog']['action'])) ?>"><?= h($item['AccessLog']['url']) ?></td>
|
||||
<td class="short" data-search="response_code" data-search-value="<?= h($item['AccessLog']['response_code']) ?>"><?= h($item['AccessLog']['response_code']) ?></td>
|
||||
<td class="short"><?= CakeNumber::toReadableSize($item['AccessLog']['memory_usage']) ?></td>
|
||||
<td class="short"><?= $item['AccessLog']['duration'] ?> ms</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<p>
|
||||
</p>
|
||||
<div class="pagination">
|
||||
<ul>
|
||||
<?= $paginator ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var passedArgs = <?= $passedArgs ?>;
|
||||
|
||||
$('.request').click(function (e) {
|
||||
e.preventDefault();
|
||||
var id = $(this).data('log-id');
|
||||
$.get(baseurl + "/admin/access_logs/request/" + id, function(data) {
|
||||
var $popoverFormLarge = $('#popover_form_large');
|
||||
$popoverFormLarge.html(data);
|
||||
openPopup($popoverFormLarge);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('td[data-search]').mouseenter(function() {
|
||||
var $td = $(this);
|
||||
if ($td.data('search-value').length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$td.find('#quickEditButton').remove(); // clean all similar if exist
|
||||
var $div = $('<div id="quickEditButton"></div>');
|
||||
$div.addClass('quick-edit-row-div');
|
||||
var $span = $('<span></span>');
|
||||
$span.addClass('fa-as-icon fa fa-search-plus');
|
||||
$span.css('font-size', '12px');
|
||||
$span.prop('title', 'Filter by this value');
|
||||
$div.append($span);
|
||||
$td.append($div);
|
||||
|
||||
$span.click(function() {
|
||||
if ($td.data('search') === 'model') {
|
||||
var val = $td.data('search-value').split(":");
|
||||
passedArgs['model'] = encodeURIComponent(val[0]);
|
||||
passedArgs['model_id'] = encodeURIComponent(val[1]);
|
||||
} else {
|
||||
passedArgs[$td.data('search')] = encodeURIComponent($td.data('search-value'));
|
||||
}
|
||||
|
||||
var url = here;
|
||||
for (var key in passedArgs) {
|
||||
if (typeof key === 'number') {
|
||||
url += "/" + passedArgs[key];
|
||||
} else if (key !== 'page') {
|
||||
url += "/" + key + ":" + passedArgs[key];
|
||||
}
|
||||
}
|
||||
window.location.href = url;
|
||||
});
|
||||
|
||||
$td.off('mouseleave').on('mouseleave', function() {
|
||||
$div.remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= $this->element('/genericElements/SideMenu/side_menu', ['menuList' => 'logs', 'menuItem' => 'listAccessLogs']);
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div style="padding: 1em; background: white; word-wrap: break-word; white-space: pre-wrap"><?= h($request) ?></div>
|
|
@ -14,7 +14,7 @@
|
|||
echo $this->Html->css('query-builder.default');
|
||||
echo $this->Html->script('query-builder');
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var qbOptions = {
|
||||
plugins: {
|
||||
'unique-filter': null,
|
||||
|
@ -50,7 +50,7 @@
|
|||
unique: true,
|
||||
id: "action",
|
||||
label: "Action",
|
||||
values: <?= json_encode($actions) ?>
|
||||
values: <?= JsonTool::encode($actions) ?>
|
||||
},
|
||||
{
|
||||
input: "select",
|
||||
|
@ -61,7 +61,7 @@
|
|||
unique: true,
|
||||
id: "model",
|
||||
label: "Model type",
|
||||
values: <?= json_encode($models) ?>
|
||||
values: <?= JsonTool::encode($models) ?>
|
||||
},
|
||||
{
|
||||
input: "text",
|
||||
|
@ -161,7 +161,7 @@
|
|||
rules: {
|
||||
condition: 'AND',
|
||||
not: false,
|
||||
rules: <?= json_encode($qbRules) ?>,
|
||||
rules: <?= JsonTool::encode($qbRules) ?>,
|
||||
flags: {
|
||||
no_add_group: true,
|
||||
condition_readonly: true,
|
||||
|
@ -294,7 +294,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var passedArgs = <?= $passedArgs ?>;
|
||||
|
||||
$('.fullChange').dblclick(function () {
|
||||
|
@ -322,6 +322,7 @@
|
|||
var $span = $('<span></span>');
|
||||
$span.addClass('fa-as-icon fa fa-search-plus');
|
||||
$span.css('font-size', '12px');
|
||||
$span.prop('title', 'Filter by this value');
|
||||
$div.append($span);
|
||||
$td.append($div);
|
||||
|
||||
|
|
|
@ -1092,14 +1092,19 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
|
|||
case 'logs':
|
||||
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||
'url' => $baseurl . '/admin/logs/index',
|
||||
'text' => __('List Logs')
|
||||
'text' => __('Application Logs')
|
||||
));
|
||||
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||
'element_id' => 'listAuditLogs',
|
||||
'url' => $baseurl . '/admin/audit_logs/index',
|
||||
'text' => __('List Audit Logs'),
|
||||
'text' => __('Audit Logs'),
|
||||
'requirement' => Configure::read('MISP.log_new_audit'),
|
||||
));
|
||||
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||
'element_id' => 'listAccessLogs',
|
||||
'url' => $baseurl . '/admin/access_logs/index',
|
||||
'text' => __('Access Logs'),
|
||||
));
|
||||
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||
'url' => $baseurl . '/admin/logs/search',
|
||||
'text' => __('Search Logs')
|
||||
|
|
|
@ -481,20 +481,20 @@
|
|||
'requirement' => $isAclAudit,
|
||||
'children' => array(
|
||||
array(
|
||||
'text' => __('List Logs'),
|
||||
'text' => __('Application Logs'),
|
||||
'url' => $baseurl . '/admin/logs/index'
|
||||
),
|
||||
array(
|
||||
'text' => __('List Audit Logs'),
|
||||
'text' => __('Audit Logs'),
|
||||
'url' => $baseurl . '/admin/audit_logs/index',
|
||||
'requirement' => Configure::read('MISP.log_new_audit'),
|
||||
),
|
||||
array(
|
||||
'text' => __('Search Logs'),
|
||||
'url' => $baseurl . '/admin/logs/search'
|
||||
)
|
||||
)
|
||||
'text' => __('Access Logs'),
|
||||
'url' => $baseurl . '/admin/access_logs/index',
|
||||
),
|
||||
)
|
||||
),
|
||||
array(
|
||||
'type' => 'root',
|
||||
'text' => __('API'),
|
||||
|
|
|
@ -9150,5 +9150,5 @@
|
|||
"uuid": false
|
||||
}
|
||||
},
|
||||
"db_version": "99"
|
||||
"db_version": "100"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue