mirror of https://github.com/MISP/MISP
commit
471840ce33
|
@ -269,13 +269,16 @@ jobs:
|
||||||
- name: Check requirements.txt
|
- name: Check requirements.txt
|
||||||
run: python tests/check_requirements.py
|
run: python tests/check_requirements.py
|
||||||
|
|
||||||
- name: Logs
|
- name: System logs
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
# update logs_test.sh when adding more logsources here
|
# update logs_test.sh when adding more logsources here
|
||||||
run: |
|
run: |
|
||||||
tail -n +1 `pwd`/app/tmp/logs/*
|
tail -n +1 `pwd`/app/tmp/logs/*
|
||||||
tail -n +1 /var/log/apache2/*.log
|
tail -n +1 /var/log/apache2/*.log
|
||||||
|
|
||||||
|
- name: Application logs
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
app/Console/cake Log export /tmp/logs.json.gz --without-changes
|
app/Console/cake Log export /tmp/logs.json.gz --without-changes
|
||||||
zcat /tmp/logs.json.gz
|
zcat /tmp/logs.json.gz
|
||||||
|
|
||||||
|
|
2
PyMISP
2
PyMISP
|
@ -1 +1 @@
|
||||||
Subproject commit 60aa6b9a0fce69507776429fcaaf3e0e3962a36c
|
Subproject commit 8b4f98ac4c2e6c8cc1dba064f937dac816b67d0f
|
|
@ -1 +1 @@
|
||||||
{"major":2, "minor":4, "hotfix":189}
|
{"major":2, "minor":4, "hotfix":190}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
App::uses('AppModel', 'Model');
|
App::uses('AppModel', 'Model');
|
||||||
App::uses('BackgroundJobsTool', 'Tools');
|
App::uses('BackgroundJobsTool', 'Tools');
|
||||||
|
App::uses('BenchmarkTool', 'Tools');
|
||||||
|
|
||||||
require_once dirname(__DIR__) . '/../Model/Attribute.php'; // FIXME workaround bug where Vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php is loaded instead
|
require_once dirname(__DIR__) . '/../Model/Attribute.php'; // FIXME workaround bug where Vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php is loaded instead
|
||||||
|
|
||||||
|
@ -38,7 +39,18 @@ abstract class AppShell extends Shell
|
||||||
{
|
{
|
||||||
$configLoad = $this->Tasks->load('ConfigLoad');
|
$configLoad = $this->Tasks->load('ConfigLoad');
|
||||||
$configLoad->execute();
|
$configLoad->execute();
|
||||||
|
if (Configure::read('Plugin.Benchmarking_enable')) {
|
||||||
|
$Benchmark = new BenchmarkTool(ClassRegistry::init('User'));
|
||||||
|
$start_time = $Benchmark->startBenchmark();
|
||||||
|
register_shutdown_function(function () use ($start_time, $Benchmark) {
|
||||||
|
$Benchmark->stopBenchmark([
|
||||||
|
'user' => 0,
|
||||||
|
'controller' => 'Shell::' . $this->modelClass,
|
||||||
|
'action' => $this->command,
|
||||||
|
'start_time' => $start_time
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
parent::initialize();
|
parent::initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,6 @@ class ServerShell extends AppShell
|
||||||
if (empty($this->args[0]) || empty($this->args[1])) {
|
if (empty($this->args[0]) || empty($this->args[1])) {
|
||||||
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Pull'] . PHP_EOL);
|
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Pull'] . PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = $this->args[0];
|
$userId = $this->args[0];
|
||||||
$user = $this->getUser($userId);
|
$user = $this->getUser($userId);
|
||||||
$serverId = $this->args[1];
|
$serverId = $this->args[1];
|
||||||
|
@ -145,6 +144,10 @@ class ServerShell extends AppShell
|
||||||
if (!empty($this->args[4]) && $this->args[4] === 'force') {
|
if (!empty($this->args[4]) && $this->args[4] === 'force') {
|
||||||
$force = true;
|
$force = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to enable garbage collector as pulling events can use a lot of memory
|
||||||
|
gc_enable();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $this->Server->pull($user, $technique, $server, $jobId, $force);
|
$result = $this->Server->pull($user, $technique, $server, $jobId, $force);
|
||||||
if (is_array($result)) {
|
if (is_array($result)) {
|
||||||
|
@ -735,6 +738,7 @@ class ServerShell extends AppShell
|
||||||
|
|
||||||
public function sendPeriodicSummaryToUsers()
|
public function sendPeriodicSummaryToUsers()
|
||||||
{
|
{
|
||||||
|
|
||||||
$periods = $this->__getPeriodsForToday();
|
$periods = $this->__getPeriodsForToday();
|
||||||
$start_time = time();
|
$start_time = time();
|
||||||
echo __n('Started periodic summary generation for the %s period', 'Started periodic summary generation for periods: %s', count($periods), implode(', ', $periods)) . PHP_EOL;
|
echo __n('Started periodic summary generation for the %s period', 'Started periodic summary generation for periods: %s', count($periods), implode(', ', $periods)) . PHP_EOL;
|
||||||
|
|
|
@ -320,6 +320,11 @@ class AnalystDataController extends AppController
|
||||||
$this->AnalystData = $this->{$vt};
|
$this->AnalystData = $this->{$vt};
|
||||||
$this->modelClass = $vt;
|
$this->modelClass = $vt;
|
||||||
$this->{$vt}->current_user = $this->Auth->user();
|
$this->{$vt}->current_user = $this->Auth->user();
|
||||||
|
if (!empty($this->request->data)) {
|
||||||
|
if (!isset($this->request->data[$type])) {
|
||||||
|
$this->request->data = [$type => $this->request->data];
|
||||||
|
}
|
||||||
|
}
|
||||||
return $vt;
|
return $vt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,19 @@ class AppController extends Controller
|
||||||
|
|
||||||
public $helpers = array('OrgImg', 'FontAwesome', 'UserName');
|
public $helpers = array('OrgImg', 'FontAwesome', 'UserName');
|
||||||
|
|
||||||
private $__queryVersion = '159';
|
private $__queryVersion = '161';
|
||||||
public $pyMispVersion = '2.4.188';
|
public $pyMispVersion = '2.4.190';
|
||||||
public $phpmin = '7.2';
|
public $phpmin = '7.2';
|
||||||
public $phprec = '7.4';
|
public $phprec = '7.4';
|
||||||
public $phptoonew = '8.0';
|
public $phptoonew = '8.0';
|
||||||
private $isApiAuthed = false;
|
private $isApiAuthed = false;
|
||||||
|
|
||||||
|
/** @var redis */
|
||||||
|
private $redis = null;
|
||||||
|
|
||||||
|
/** @var benchmark_results */
|
||||||
|
private $benchmark_results = null;
|
||||||
|
|
||||||
public $baseurl = '';
|
public $baseurl = '';
|
||||||
|
|
||||||
public $restResponsePayload = null;
|
public $restResponsePayload = null;
|
||||||
|
@ -57,9 +63,14 @@ class AppController extends Controller
|
||||||
/** @var ACLComponent */
|
/** @var ACLComponent */
|
||||||
public $ACL;
|
public $ACL;
|
||||||
|
|
||||||
|
/** @var BenchmarkComponent */
|
||||||
|
public $Benchmark;
|
||||||
|
|
||||||
/** @var RestResponseComponent */
|
/** @var RestResponseComponent */
|
||||||
public $RestResponse;
|
public $RestResponse;
|
||||||
|
|
||||||
|
public $start_time;
|
||||||
|
|
||||||
public function __construct($request = null, $response = null)
|
public function __construct($request = null, $response = null)
|
||||||
{
|
{
|
||||||
parent::__construct($request, $response);
|
parent::__construct($request, $response);
|
||||||
|
@ -97,6 +108,12 @@ class AppController extends Controller
|
||||||
|
|
||||||
public function beforeFilter()
|
public function beforeFilter()
|
||||||
{
|
{
|
||||||
|
$this->User = ClassRegistry::init('User');
|
||||||
|
if (Configure::read('Plugin.Benchmarking_enable')) {
|
||||||
|
App::uses('BenchmarkTool', 'Tools');
|
||||||
|
$this->Benchmark = new BenchmarkTool($this->User);
|
||||||
|
$this->start_time = $this->Benchmark->startBenchmark();
|
||||||
|
}
|
||||||
$controller = $this->request->params['controller'];
|
$controller = $this->request->params['controller'];
|
||||||
$action = $this->request->params['action'];
|
$action = $this->request->params['action'];
|
||||||
|
|
||||||
|
@ -147,8 +164,6 @@ class AppController extends Controller
|
||||||
Configure::write('Config.language', 'eng');
|
Configure::write('Config.language', 'eng');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->User = ClassRegistry::init('User');
|
|
||||||
|
|
||||||
if (!empty($this->request->params['named']['disable_background_processing'])) {
|
if (!empty($this->request->params['named']['disable_background_processing'])) {
|
||||||
Configure::write('MISP.background_jobs', 0);
|
Configure::write('MISP.background_jobs', 0);
|
||||||
}
|
}
|
||||||
|
@ -863,6 +878,21 @@ class AppController extends Controller
|
||||||
|
|
||||||
public function afterFilter()
|
public function afterFilter()
|
||||||
{
|
{
|
||||||
|
// benchmarking
|
||||||
|
if (Configure::read('Plugin.Benchmarking_enable')) {
|
||||||
|
$this->Benchmark->stopBenchmark([
|
||||||
|
'user' => $this->Auth->user('id'),
|
||||||
|
'controller' => $this->request->params['controller'],
|
||||||
|
'action' => $this->request->params['action'],
|
||||||
|
'start_time' => $this->start_time
|
||||||
|
]);
|
||||||
|
|
||||||
|
//if ($redis && !$redis->exists('misp:auth_fail_throttling:' . $key)) {
|
||||||
|
//$redis->setex('misp:auth_fail_throttling:' . $key, 3600, 1);
|
||||||
|
//return true;
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
||||||
if ($this->isApiAuthed && $this->_isRest() && !Configure::read('Security.authkey_keep_session')) {
|
if ($this->isApiAuthed && $this->_isRest() && !Configure::read('Security.authkey_keep_session')) {
|
||||||
$this->Session->destroy();
|
$this->Session->destroy();
|
||||||
}
|
}
|
||||||
|
@ -1033,7 +1063,19 @@ class AppController extends Controller
|
||||||
$data = array_merge($data, $temp);
|
$data = array_merge($data, $temp);
|
||||||
} else {
|
} else {
|
||||||
foreach ($options['paramArray'] as $param) {
|
foreach ($options['paramArray'] as $param) {
|
||||||
if (isset($temp[$param])) {
|
if (substr($param, -1) == '*') {
|
||||||
|
$root = substr($param, 0, strlen($param)-1);
|
||||||
|
foreach ($temp as $existingParamKey => $v) {
|
||||||
|
$leftover = substr($existingParamKey, strlen($param)-1);
|
||||||
|
if (
|
||||||
|
$root == substr($existingParamKey, 0, strlen($root)) &&
|
||||||
|
preg_match('/^[\w_-. ]+$/', $leftover) == 1
|
||||||
|
) {
|
||||||
|
$data[$existingParamKey] = $temp[$existingParamKey];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isset($temp[$param])) {
|
||||||
$data[$param] = $temp[$param];
|
$data[$param] = $temp[$param];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1580,6 +1580,7 @@ class AttributesController extends AppController
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->paginate['conditions'] = $params['conditions'];
|
$this->paginate['conditions'] = $params['conditions'];
|
||||||
|
$this->paginate['ignoreIndexHint'] = 'deleted';
|
||||||
$attributes = $this->paginate();
|
$attributes = $this->paginate();
|
||||||
$this->Attribute->attachTagsToAttributes($attributes, ['includeAllTags' => true]);
|
$this->Attribute->attachTagsToAttributes($attributes, ['includeAllTags' => true]);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
App::uses('AppController', 'Controller');
|
||||||
|
|
||||||
|
class BenchmarksController extends AppController
|
||||||
|
{
|
||||||
|
public $components = array('Session', 'RequestHandler');
|
||||||
|
|
||||||
|
public $paginate = [
|
||||||
|
'limit' => 60,
|
||||||
|
'maxLimit' => 9999,
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
public function beforeFilter()
|
||||||
|
{
|
||||||
|
parent::beforeFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->set('menuData', ['menuList' => 'admin', 'menuItem' => 'index']);
|
||||||
|
$this->loadModel('User');
|
||||||
|
App::uses('BenchmarkTool', 'Tools');
|
||||||
|
$this->Benchmark = new BenchmarkTool($this->User);
|
||||||
|
$passedArgs = $this->passedArgs;
|
||||||
|
$this->paginate['order'] = 'value';
|
||||||
|
$defaults = [
|
||||||
|
'days' => null,
|
||||||
|
'average' => false,
|
||||||
|
'aggregate' => false,
|
||||||
|
'scope' => null,
|
||||||
|
'field' => null,
|
||||||
|
'key' => null,
|
||||||
|
'quickFilter' => null
|
||||||
|
];
|
||||||
|
$filters = $this->IndexFilter->harvestParameters(array_keys($defaults));
|
||||||
|
foreach ($defaults as $key => $value) {
|
||||||
|
if (!isset($filters[$key])) {
|
||||||
|
$filters[$key] = $defaults[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$temp = $this->Benchmark->getAllTopLists(
|
||||||
|
$filters['days'] ?? null,
|
||||||
|
$filters['limit'] ?? 100,
|
||||||
|
$filters['average'] ?? null,
|
||||||
|
$filters['aggregate'] ?? null
|
||||||
|
);
|
||||||
|
$settings = $this->Benchmark->getSettings();
|
||||||
|
$units = $this->Benchmark->getUnits();
|
||||||
|
$this->set('settings', $settings);
|
||||||
|
$data = [];
|
||||||
|
$userLookup = [];
|
||||||
|
foreach ($temp as $scope => $t) {
|
||||||
|
if (!empty($filters['scope']) && $filters['scope'] !== 'all' && $scope !== $filters['scope']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($t as $field => $t2) {
|
||||||
|
if (!empty($filters['field']) && $filters['field'] !== 'all' && $field !== $filters['field']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($t2 as $date => $t3) {
|
||||||
|
foreach ($t3 as $key => $value) {
|
||||||
|
if ($scope == 'user') {
|
||||||
|
if ($key === 'SYSTEM') {
|
||||||
|
$text = 'SYSTEM';
|
||||||
|
} else if (isset($userLookup[$key])) {
|
||||||
|
$text = $userLookup[$key];
|
||||||
|
} else {
|
||||||
|
$user = $this->User->find('first', [
|
||||||
|
'fields' => ['User.id', 'User.email'],
|
||||||
|
'recursive' => -1,
|
||||||
|
'conditions' => ['User.id' => $key]
|
||||||
|
]);
|
||||||
|
if (empty($user)) {
|
||||||
|
$text = '(' . $key . ') ' . __('Invalid user');
|
||||||
|
} else {
|
||||||
|
$text = '(' . $key . ') ' . $user['User']['email'];
|
||||||
|
}
|
||||||
|
$userLookup[$key] = $text;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$text = $key;
|
||||||
|
}
|
||||||
|
if (!empty($filters['quickFilter'])) {
|
||||||
|
$q = strtolower($filters['quickFilter']);
|
||||||
|
if (
|
||||||
|
strpos(strtolower($scope), $q) === false &&
|
||||||
|
strpos(strtolower($field), $q) === false &&
|
||||||
|
strpos(strtolower($key), $q) === false &&
|
||||||
|
strpos(strtolower($value), $q) === false &&
|
||||||
|
strpos(strtolower($date), $q) === false &&
|
||||||
|
strpos(strtolower($text), $q) === false
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($filters['key']) || $key == $filters['key']) {
|
||||||
|
$data[] = [
|
||||||
|
'scope' => $scope,
|
||||||
|
'field' => $field,
|
||||||
|
'date' => $date,
|
||||||
|
'key' => $key,
|
||||||
|
'text' => $text,
|
||||||
|
'value' => $value,
|
||||||
|
'unit' => $units[$field]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->_isRest()) {
|
||||||
|
return $this->RestResponse->viewData($data, $this->response->type());
|
||||||
|
}
|
||||||
|
App::uses('CustomPaginationTool', 'Tools');
|
||||||
|
$customPagination = new CustomPaginationTool();
|
||||||
|
$customPagination->truncateAndPaginate($data, $this->params, $this->modelClass, true);
|
||||||
|
$this->set('data', $data);
|
||||||
|
$this->set('passedArgs', json_encode($passedArgs));
|
||||||
|
$this->set('filters', $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -95,6 +95,9 @@ class ACLComponent extends Component
|
||||||
'index' => ['perm_auth'],
|
'index' => ['perm_auth'],
|
||||||
'view' => ['perm_auth'],
|
'view' => ['perm_auth'],
|
||||||
],
|
],
|
||||||
|
'benchmarks' => [
|
||||||
|
'index' => []
|
||||||
|
],
|
||||||
'cerebrates' => [
|
'cerebrates' => [
|
||||||
'add' => [],
|
'add' => [],
|
||||||
'delete' => [],
|
'delete' => [],
|
||||||
|
|
|
@ -144,7 +144,11 @@ class RestSearchComponent extends Component
|
||||||
'retry',
|
'retry',
|
||||||
'expiry',
|
'expiry',
|
||||||
'minimum_ttl',
|
'minimum_ttl',
|
||||||
'ttl'
|
'ttl',
|
||||||
|
'org.sector',
|
||||||
|
'org.local',
|
||||||
|
'org.nationality',
|
||||||
|
'galaxy.*',
|
||||||
],
|
],
|
||||||
'Object' => [
|
'Object' => [
|
||||||
'returnFormat',
|
'returnFormat',
|
||||||
|
|
|
@ -213,10 +213,13 @@ class EventReportsController extends AppController
|
||||||
|
|
||||||
public function extractAllFromReport($reportId)
|
public function extractAllFromReport($reportId)
|
||||||
{
|
{
|
||||||
if (!$this->request->is('ajax')) {
|
if (!$this->request->is('ajax') && !$this->_isRest()) {
|
||||||
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
|
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
|
||||||
}
|
}
|
||||||
if ($this->request->is('post')) {
|
if ($this->request->is('post')) {
|
||||||
|
if (!isset($this->data['EventReport'])) {
|
||||||
|
$this->data = ['EventReport' => $this->data];
|
||||||
|
}
|
||||||
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false);
|
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false);
|
||||||
$results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report);
|
$results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report);
|
||||||
$report['EventReport']['content'] = $results['replacementResult']['contentWithReplacements'];
|
$report['EventReport']['content'] = $results['replacementResult']['contentWithReplacements'];
|
||||||
|
@ -299,13 +302,16 @@ class EventReportsController extends AppController
|
||||||
|
|
||||||
public function importReportFromUrl($event_id)
|
public function importReportFromUrl($event_id)
|
||||||
{
|
{
|
||||||
if (!$this->request->is('ajax')) {
|
if (!$this->request->is('ajax') && !$this->_isRest()) {
|
||||||
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
|
throw new MethodNotAllowedException(__('This function can only be reached via AJAX and via the API.'));
|
||||||
}
|
}
|
||||||
$fetcherModule = $this->EventReport->isFetchURLModuleEnabled();
|
$fetcherModule = $this->EventReport->isFetchURLModuleEnabled();
|
||||||
if ($this->request->is('post')) {
|
if ($this->request->is('post')) {
|
||||||
|
if (empty($this->data['EventReport'])) {
|
||||||
|
$this->data = ['EventReport' => $this->data];
|
||||||
|
}
|
||||||
if (empty($this->data['EventReport']['url'])) {
|
if (empty($this->data['EventReport']['url'])) {
|
||||||
throw new MethodNotAllowedException(__('An URL must be provided'));
|
throw new MethodNotAllowedException(__('A URL must be provided'));
|
||||||
}
|
}
|
||||||
$url = $this->data['EventReport']['url'];
|
$url = $this->data['EventReport']['url'];
|
||||||
$format = 'html';
|
$format = 'html';
|
||||||
|
@ -316,7 +322,6 @@ class EventReportsController extends AppController
|
||||||
$format = $parsed_format;
|
$format = $parsed_format;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = $this->EventReport->downloadMarkdownFromURL($event_id, $url, $format);
|
$content = $this->EventReport->downloadMarkdownFromURL($event_id, $url, $format);
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
|
@ -497,6 +497,11 @@ class EventsController extends AppController
|
||||||
continue 2;
|
continue 2;
|
||||||
}
|
}
|
||||||
$pieces = is_array($v) ? $v : explode('|', $v);
|
$pieces = is_array($v) ? $v : explode('|', $v);
|
||||||
|
$isANDed = false;
|
||||||
|
if (count($pieces) == 1 && strpos($pieces[0], '&') !== -1) {
|
||||||
|
$pieces = explode('&', $v);
|
||||||
|
$isANDed = count($pieces) > 1;
|
||||||
|
}
|
||||||
$filterString = "";
|
$filterString = "";
|
||||||
$expectOR = false;
|
$expectOR = false;
|
||||||
$tagRules = [];
|
$tagRules = [];
|
||||||
|
@ -563,10 +568,19 @@ class EventsController extends AppController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($tagRules['include'])) {
|
if (!empty($tagRules['include'])) {
|
||||||
$include = $this->Event->EventTag->find('column', array(
|
if ($isANDed) {
|
||||||
'conditions' => array('EventTag.tag_id' => $tagRules['include']),
|
$include = $this->Event->EventTag->find('column', array(
|
||||||
'fields' => ['EventTag.event_id'],
|
'conditions' => ['EventTag.tag_id' => $tagRules['include']],
|
||||||
));
|
'fields' => ['EventTag.event_id'],
|
||||||
|
'group' => ['EventTag.event_id'],
|
||||||
|
'having' => ['COUNT(*) =' => count($tagRules['include'])],
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$include = $this->Event->EventTag->find('column', array(
|
||||||
|
'conditions' => array('EventTag.tag_id' => $tagRules['include']),
|
||||||
|
'fields' => ['EventTag.event_id'],
|
||||||
|
));
|
||||||
|
}
|
||||||
if (!empty($include)) {
|
if (!empty($include)) {
|
||||||
$this->paginate['conditions']['AND'][] = 'Event.id IN (' . implode(",", $include) . ')';
|
$this->paginate['conditions']['AND'][] = 'Event.id IN (' . implode(",", $include) . ')';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -74,6 +74,8 @@ class FeedsController extends AppController
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$loggedUser = $this->Auth->user();
|
||||||
|
$this->loadModel('TagCollection');
|
||||||
|
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'filters' => [
|
'filters' => [
|
||||||
|
@ -92,7 +94,7 @@ class FeedsController extends AppController
|
||||||
'source_format'
|
'source_format'
|
||||||
],
|
],
|
||||||
'conditions' => $conditions,
|
'conditions' => $conditions,
|
||||||
'afterFind' => function (array $feeds) {
|
'afterFind' => function (array $feeds) use ($loggedUser) {
|
||||||
if ($this->_isSiteAdmin()) {
|
if ($this->_isSiteAdmin()) {
|
||||||
$feeds = $this->Feed->attachFeedCacheTimestamps($feeds);
|
$feeds = $this->Feed->attachFeedCacheTimestamps($feeds);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +108,19 @@ class FeedsController extends AppController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($feeds as &$feed) {
|
||||||
|
if (!empty($feed['Feed']['tag_collection_id'])) {
|
||||||
|
$tagCollection = $this->TagCollection->fetchTagCollection($loggedUser, [
|
||||||
|
'conditions' => [
|
||||||
|
'TagCollection.id' => $feed['Feed']['tag_collection_id'],
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
if (!empty($tagCollection)) {
|
||||||
|
$feed['TagCollection'] = $tagCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $feeds;
|
return $feeds;
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -294,6 +309,10 @@ class FeedsController extends AppController
|
||||||
}
|
}
|
||||||
$tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc')));
|
$tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc')));
|
||||||
$tags[0] = 'None';
|
$tags[0] = 'None';
|
||||||
|
$this->loadModel('TagCollection');
|
||||||
|
$tagCollections = $this->TagCollection->fetchTagCollection($this->Auth->user());
|
||||||
|
$tagCollections = Hash::combine($tagCollections, '{n}.TagCollection.id', '{n}.TagCollection.name');
|
||||||
|
$tagCollections[0] = 'None';
|
||||||
|
|
||||||
$this->loadModel('Server');
|
$this->loadModel('Server');
|
||||||
$allTypes = $this->Server->getAllTypes();
|
$allTypes = $this->Server->getAllTypes();
|
||||||
|
@ -304,6 +323,7 @@ class FeedsController extends AppController
|
||||||
'order' => 'LOWER(name)'
|
'order' => 'LOWER(name)'
|
||||||
)),
|
)),
|
||||||
'tags' => $tags,
|
'tags' => $tags,
|
||||||
|
'tag_collections' => $tagCollections,
|
||||||
'feedTypes' => $this->Feed->getFeedTypesOptions(),
|
'feedTypes' => $this->Feed->getFeedTypesOptions(),
|
||||||
'sharingGroups' => $sharingGroups,
|
'sharingGroups' => $sharingGroups,
|
||||||
'distributionLevels' => $distributionLevels,
|
'distributionLevels' => $distributionLevels,
|
||||||
|
@ -340,6 +360,7 @@ class FeedsController extends AppController
|
||||||
'distribution',
|
'distribution',
|
||||||
'sharing_group_id',
|
'sharing_group_id',
|
||||||
'tag_id',
|
'tag_id',
|
||||||
|
'tag_collection_id',
|
||||||
'event_id',
|
'event_id',
|
||||||
'publish',
|
'publish',
|
||||||
'delta_merge',
|
'delta_merge',
|
||||||
|
@ -442,8 +463,17 @@ class FeedsController extends AppController
|
||||||
if (empty(Configure::read('Security.disable_local_feed_access'))) {
|
if (empty(Configure::read('Security.disable_local_feed_access'))) {
|
||||||
$inputSources['local'] = 'Local';
|
$inputSources['local'] = 'Local';
|
||||||
}
|
}
|
||||||
|
$tags = $this->Event->EventTag->Tag->find('all', [
|
||||||
|
'recursive' => -1,
|
||||||
|
'fields' => ['Tag.name', 'Tag.id'],
|
||||||
|
'order' => ['lower(Tag.name) asc']
|
||||||
|
]);
|
||||||
$tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc')));
|
$tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc')));
|
||||||
$tags[0] = 'None';
|
$tags[0] = 'None';
|
||||||
|
$this->loadModel('TagCollection');
|
||||||
|
$tagCollections = $this->TagCollection->fetchTagCollection($this->Auth->user());
|
||||||
|
$tagCollections = Hash::combine($tagCollections, '{n}.TagCollection.id', '{n}.TagCollection.name');
|
||||||
|
$tagCollections[0] = 'None';
|
||||||
|
|
||||||
$this->loadModel('Server');
|
$this->loadModel('Server');
|
||||||
$allTypes = $this->Server->getAllTypes();
|
$allTypes = $this->Server->getAllTypes();
|
||||||
|
@ -457,6 +487,7 @@ class FeedsController extends AppController
|
||||||
'order' => 'LOWER(name)'
|
'order' => 'LOWER(name)'
|
||||||
)),
|
)),
|
||||||
'tags' => $tags,
|
'tags' => $tags,
|
||||||
|
'tag_collections' => $tagCollections,
|
||||||
'feedTypes' => $this->Feed->getFeedTypesOptions(),
|
'feedTypes' => $this->Feed->getFeedTypesOptions(),
|
||||||
'sharingGroups' => $sharingGroups,
|
'sharingGroups' => $sharingGroups,
|
||||||
'distributionLevels' => $distributionLevels,
|
'distributionLevels' => $distributionLevels,
|
||||||
|
|
|
@ -357,7 +357,7 @@ class TagCollectionsController extends AppController
|
||||||
if (!$tagCollection) {
|
if (!$tagCollection) {
|
||||||
throw new NotFoundException(__('Invalid tag collection.'));
|
throw new NotFoundException(__('Invalid tag collection.'));
|
||||||
}
|
}
|
||||||
if ($this->ACL->canModifyTagCollection($this->Auth->user(), $tagCollection)) {
|
if (!$this->ACL->canModifyTagCollection($this->Auth->user(), $tagCollection)) {
|
||||||
throw new ForbiddenException(__('You dont have a permission to do that'));
|
throw new ForbiddenException(__('You dont have a permission to do that'));
|
||||||
}
|
}
|
||||||
$tagCollectionTag = $this->TagCollection->TagCollectionTag->find('first', [
|
$tagCollectionTag = $this->TagCollection->TagCollectionTag->find('first', [
|
||||||
|
|
|
@ -2071,7 +2071,7 @@ class UsersController extends AppController
|
||||||
|
|
||||||
$stats['attribute_count'] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.deleted' => 0), 'recursive' => -1));
|
$stats['attribute_count'] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.deleted' => 0), 'recursive' => -1));
|
||||||
$stats['attribute_count_month'] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.timestamp >' => $this_month, 'Attribute.deleted' => 0), 'recursive' => -1));
|
$stats['attribute_count_month'] = $this->User->Event->Attribute->find('count', array('conditions' => array('Attribute.timestamp >' => $this_month, 'Attribute.deleted' => 0), 'recursive' => -1));
|
||||||
$stats['attributes_per_event'] = round($stats['attribute_count'] / $stats['event_count']);
|
$stats['attributes_per_event'] = $stats['event_count'] != 0 ? round($stats['attribute_count'] / $stats['event_count']) : 0;
|
||||||
|
|
||||||
$stats['correlation_count'] = $this->User->Event->Attribute->Correlation->find('count', array('recursive' => -1));
|
$stats['correlation_count'] = $this->User->Event->Attribute->Correlation->find('count', array('recursive' => -1));
|
||||||
|
|
||||||
|
@ -2082,7 +2082,7 @@ class UsersController extends AppController
|
||||||
$stats['org_count'] = count($orgs);
|
$stats['org_count'] = count($orgs);
|
||||||
$stats['local_org_count'] = $local_orgs_count;
|
$stats['local_org_count'] = $local_orgs_count;
|
||||||
$stats['contributing_org_count'] = $this->User->Event->find('count', array('recursive' => -1, 'group' => array('Event.orgc_id')));
|
$stats['contributing_org_count'] = $this->User->Event->find('count', array('recursive' => -1, 'group' => array('Event.orgc_id')));
|
||||||
$stats['average_user_per_org'] = round($stats['user_count'] / $stats['local_org_count'], 1);
|
$stats['average_user_per_org'] = $stats['local_org_count'] != 0 ? round($stats['user_count'] / $stats['local_org_count'], 1) : 0;
|
||||||
|
|
||||||
$this->loadModel('Thread');
|
$this->loadModel('Thread');
|
||||||
$stats['thread_count'] = $this->Thread->find('count', array('conditions' => array('Thread.post_count >' => 0), 'recursive' => -1));
|
$stats['thread_count'] = $this->Thread->find('count', array('conditions' => array('Thread.post_count >' => 0), 'recursive' => -1));
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkTopListWidget
|
||||||
|
{
|
||||||
|
public $title = 'Benchmark top list';
|
||||||
|
public $render = 'MultiLineChart';
|
||||||
|
public $width = 3;
|
||||||
|
public $height = 3;
|
||||||
|
public $description = 'A graph showing the top list for a given scope and field in the captured metrics.';
|
||||||
|
public $cacheLifetime = false;
|
||||||
|
public $autoRefreshDelay = 30;
|
||||||
|
public $params = array(
|
||||||
|
'days' => 'Number of days to consider for the graph. There will be a data entry for each day (assuming the benchmarking has been enabled). Defaults to returning all data.',
|
||||||
|
'weeks' => 'Number of weeks to consider for the graph. There will be a data entry for each day (assuming the benchmarking has been enabled). Defaults to returning all data.',
|
||||||
|
'months' => 'Number of months to consider for the graph. There will be a data entry for each day (assuming the benchmarking has been enabled). Defaults to returning all data.',
|
||||||
|
'scope' => 'The scope of the benchmarking refers to what was being tracked. The following scopes are valid: user, endpoint, user_agent',
|
||||||
|
'field' => 'The individual metric to be queried from the benchmark results. Valid values are: time, sql_time, sql_queries, memory, endpoint',
|
||||||
|
'average' => 'If you wish to view the averages per scope/field, set this variable to true. It will divide the result by the number of executions recorded for the scope/field combination for the given day.'
|
||||||
|
);
|
||||||
|
public $Benchmark;
|
||||||
|
public $User;
|
||||||
|
|
||||||
|
public $placeholder =
|
||||||
|
'{
|
||||||
|
"days": "30",
|
||||||
|
"scope": "endpoints",
|
||||||
|
"field": "sql_time"
|
||||||
|
}';
|
||||||
|
|
||||||
|
public function handler($user, $options = array())
|
||||||
|
{
|
||||||
|
$this->User = ClassRegistry::init('User');
|
||||||
|
$currentTime = strtotime("now");
|
||||||
|
$endOfDay = strtotime("tomorrow", $currentTime) - 1;
|
||||||
|
if (!empty($options['days'])) {
|
||||||
|
$limit = (int)($options['days']);
|
||||||
|
$delta = 'day';
|
||||||
|
} else if (!empty($options['weeks'])) {
|
||||||
|
$limit = (int)($options['weeks']);
|
||||||
|
$delta = 'week';
|
||||||
|
} else if (!empty($options['months'])) {
|
||||||
|
$limit = (int)($options['months']);
|
||||||
|
$delta = 'month';
|
||||||
|
} else {
|
||||||
|
$limit = 30;
|
||||||
|
$delta = 'day';
|
||||||
|
}
|
||||||
|
$axis_info = [
|
||||||
|
'time' => 'Total time taken (ms)',
|
||||||
|
'sql_time' => 'SQL time taken (ms)',
|
||||||
|
'sql_queries' => 'Queries (#)',
|
||||||
|
'memory' => 'Memory (MB)',
|
||||||
|
'endpoint' => 'Queries to endpoint (#)'
|
||||||
|
];
|
||||||
|
$y_axis = $axis_info[isset($options['field']) ? $options['field'] : 'time'];
|
||||||
|
$data = ['y-axis' => $y_axis];
|
||||||
|
$data['data'] = array();
|
||||||
|
// Add total users data for all timestamps
|
||||||
|
|
||||||
|
for ($i = 0; $i < $limit; $i++) {
|
||||||
|
$itemTime = strtotime('- ' . $i . $delta, $endOfDay);
|
||||||
|
$item = array();
|
||||||
|
$date = strftime('%Y-%m-%d', $itemTime);
|
||||||
|
$item = $this->getData($date, $options);
|
||||||
|
if (!empty($item)) {
|
||||||
|
$item['date'] = $date;
|
||||||
|
$data['data'][] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$keys = [];
|
||||||
|
foreach ($data['data'] as $day_data) {
|
||||||
|
foreach ($day_data as $key => $temp) {
|
||||||
|
$keys[$key] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$keys = array_keys($keys);
|
||||||
|
foreach ($data['data'] as $k => $day_data) {
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!isset($day_data[$key])) {
|
||||||
|
$data['data'][$k][$key] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($day_data as $key => $temp) {
|
||||||
|
$keys[$key] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getData($time, $options)
|
||||||
|
{
|
||||||
|
$dates = [$time];
|
||||||
|
$this->Benchmark = new BenchmarkTool($this->User);
|
||||||
|
$result = $this->Benchmark->getTopList(
|
||||||
|
isset($options['scope']) ? $options['scope'] : 'endpoint',
|
||||||
|
isset($options['field']) ? $options['field'] : 'memory',
|
||||||
|
$dates,
|
||||||
|
isset($options['limit']) ? $options['limit'] : 5,
|
||||||
|
isset($options['average']) ? $options['average'] : false,
|
||||||
|
);
|
||||||
|
if (!empty($result)) {
|
||||||
|
return $result[$time];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkPermissions($user)
|
||||||
|
{
|
||||||
|
if (empty($user['Role']['perm_site_admin'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,9 +58,11 @@ class EventEvolutionLineWidget
|
||||||
'recursive' => -1
|
'recursive' => -1
|
||||||
];
|
];
|
||||||
$eparams = [];
|
$eparams = [];
|
||||||
|
$filteringOnOrg = false;
|
||||||
if (!empty($options['filter']) && is_array($options['filter'])) {
|
if (!empty($options['filter']) && is_array($options['filter'])) {
|
||||||
foreach ($this->validFilterKeys as $filterKey) {
|
foreach ($this->validFilterKeys as $filterKey) {
|
||||||
if (!empty($options['filter'][$filterKey])) {
|
if (!empty($options['filter'][$filterKey])) {
|
||||||
|
$filteringOnOrg = true;
|
||||||
if (!is_array($options['filter'][$filterKey])) {
|
if (!is_array($options['filter'][$filterKey])) {
|
||||||
$options['filter'][$filterKey] = [$options['filter'][$filterKey]];
|
$options['filter'][$filterKey] = [$options['filter'][$filterKey]];
|
||||||
}
|
}
|
||||||
|
@ -87,6 +89,9 @@ class EventEvolutionLineWidget
|
||||||
'conditions' => $oparams['conditions'],
|
'conditions' => $oparams['conditions'],
|
||||||
'fields' => ['id']
|
'fields' => ['id']
|
||||||
]);
|
]);
|
||||||
|
if ($filteringOnOrg) {
|
||||||
|
$eparams['conditions']['AND']['Event.orgc_id IN'] = !empty($org_ids) ? $org_ids : [-1];
|
||||||
|
}
|
||||||
$this->Event->virtualFields = [
|
$this->Event->virtualFields = [
|
||||||
'published_date' => null
|
'published_date' => null
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filter parameters from index searches
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BenchmarkTool
|
||||||
|
{
|
||||||
|
/** @var Model */
|
||||||
|
public $Model;
|
||||||
|
|
||||||
|
/** @var redis */
|
||||||
|
public $redis;
|
||||||
|
|
||||||
|
/** @var retention */
|
||||||
|
private $retention = 0;
|
||||||
|
|
||||||
|
/** @var start_time */
|
||||||
|
public $start_timexxx;
|
||||||
|
|
||||||
|
const BENCHMARK_SCOPES = ['user', 'endpoint', 'user_agent'];
|
||||||
|
const BENCHMARK_FIELDS = ['time', 'sql_time', 'sql_queries', 'memory'];
|
||||||
|
const BENCHMARK_UNITS = [
|
||||||
|
'time' => 's',
|
||||||
|
'sql_time' => 'ms',
|
||||||
|
'sql_queries' => '',
|
||||||
|
'memory' => 'MB'
|
||||||
|
];
|
||||||
|
|
||||||
|
public $namespace = 'misp:benchmark:';
|
||||||
|
|
||||||
|
function __construct(Model $model) {
|
||||||
|
$this->Model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scope' => self::BENCHMARK_SCOPES,
|
||||||
|
'field' => self::BENCHMARK_FIELDS,
|
||||||
|
'average' => [0, 1],
|
||||||
|
'aggregate' => [0, 1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnits()
|
||||||
|
{
|
||||||
|
return self::BENCHMARK_UNITS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startBenchmark()
|
||||||
|
{
|
||||||
|
$start_time = microtime(true);
|
||||||
|
$this->redis = $this->Model->setupRedis();
|
||||||
|
$this->retention = Configure::check('Plugin.benchmark_retention') ? Configure::read('Plugin.benchmark_retention') : 0;
|
||||||
|
return $start_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stopBenchmark(array $options)
|
||||||
|
{
|
||||||
|
$start_time = $options['start_time'];
|
||||||
|
if (!empty($options['user'])) {
|
||||||
|
$sql = $this->Model->getDataSource()->getLog(false, false);
|
||||||
|
$benchmarkData = [
|
||||||
|
'user' => $options['user'],
|
||||||
|
'endpoint' => $options['controller'] . '/' . $options['action'],
|
||||||
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
'sql_queries' => $sql['count'],
|
||||||
|
'sql_time' => $sql['time'],
|
||||||
|
'time' => (microtime(true) - $start_time),
|
||||||
|
'memory' => (int)(memory_get_peak_usage(true) / 1024 / 1024),
|
||||||
|
//'date' => date('Y-m-d', strtotime("-3 days"))
|
||||||
|
'date' => date('Y-m-d')
|
||||||
|
];
|
||||||
|
$this->pushBenchmarkDataToRedis($benchmarkData);
|
||||||
|
} else {
|
||||||
|
$sql = $this->Model->getDataSource()->getLog(false, false);
|
||||||
|
$benchmarkData = [
|
||||||
|
'user' => 'SYSTEM',
|
||||||
|
'endpoint' => $options['controller'] . '/' . $options['action'],
|
||||||
|
'user_agent' => 'CLI',
|
||||||
|
'sql_queries' => $sql['count'],
|
||||||
|
'sql_time' => $sql['time'],
|
||||||
|
'time' => (microtime(true) - $start_time),
|
||||||
|
'memory' => (int)(memory_get_peak_usage(true) / 1024 / 1024),
|
||||||
|
//'date' => date('Y-m-d', strtotime("-3 days"))
|
||||||
|
'date' => date('Y-m-d')
|
||||||
|
];
|
||||||
|
$this->pushBenchmarkDataToRedis($benchmarkData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pushBenchmarkDataToRedis($benchmarkData)
|
||||||
|
{
|
||||||
|
$this->redis = $this->Model->setupRedis();
|
||||||
|
$this->redis->pipeline();
|
||||||
|
$this->redis->sAdd(
|
||||||
|
$this->namespace . 'days',
|
||||||
|
$benchmarkData['date']
|
||||||
|
);
|
||||||
|
foreach (self::BENCHMARK_SCOPES as $scope) {
|
||||||
|
$this->redis->sAdd(
|
||||||
|
$this->namespace . $scope . ':list',
|
||||||
|
$benchmarkData[$scope]
|
||||||
|
);
|
||||||
|
$this->redis->zIncrBy(
|
||||||
|
$this->namespace . $scope . ':count:' . $benchmarkData['date'],
|
||||||
|
1,
|
||||||
|
$benchmarkData[$scope]
|
||||||
|
);
|
||||||
|
foreach (self::BENCHMARK_FIELDS as $field) {
|
||||||
|
$this->redis->zIncrBy(
|
||||||
|
$this->namespace . $scope . ':' . $field . ':' . $benchmarkData['date'],
|
||||||
|
$benchmarkData[$field],
|
||||||
|
$benchmarkData[$scope]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->redis->zIncrBy(
|
||||||
|
$this->namespace . $scope . ':endpoint:' . $benchmarkData['date'] . ':' . $benchmarkData['user'],
|
||||||
|
1,
|
||||||
|
$benchmarkData['endpoint']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->redis->exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTopList(string $scope, string $field, array $days = [], $limit = 10, $average = false, $aggregate = false)
|
||||||
|
{
|
||||||
|
if (empty($this->redis)) {
|
||||||
|
$this->redis = $this->Model->setupRedis();
|
||||||
|
}
|
||||||
|
$results = [];
|
||||||
|
if (is_string($days)) {
|
||||||
|
$days = [$days];
|
||||||
|
}
|
||||||
|
foreach ($days as $day) {
|
||||||
|
$temp = $this->redis->zrevrange($this->namespace . $scope . ':' . $field . ':' . $day, 0, $limit, true);
|
||||||
|
foreach ($temp as $k => $v) {
|
||||||
|
if ($average) {
|
||||||
|
$divisor = $this->redis->zscore($this->namespace . $scope . ':count:' . $day, $k);
|
||||||
|
if ($aggregate) {
|
||||||
|
$results['aggregate'][$k] = empty($results['aggregate'][$k]) ? ($v / $divisor) : ($results['aggregate'][$k] + ($v / $divisor));
|
||||||
|
} else {
|
||||||
|
$results[$day][$k] = (int)($v / $divisor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($aggregate) {
|
||||||
|
$results['aggregate'][$k] = empty($results['aggregate'][$k]) ? $v : ($results['aggregate'][$k] + $v);
|
||||||
|
} else {
|
||||||
|
$results[$day][$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($aggregate && $average) {
|
||||||
|
$count_days = count($days);
|
||||||
|
foreach ($results['aggregate'] as $k => $result) {
|
||||||
|
$results['aggregate'][$k] = (int)($result / $count_days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllTopLists(array $days = null, $limit = 10, $average = false, $aggregate = false, $scope_filter = [])
|
||||||
|
{
|
||||||
|
if (empty($this->redis)) {
|
||||||
|
$this->redis = $this->Model->setupRedis();
|
||||||
|
}
|
||||||
|
if ($days === null) {
|
||||||
|
$days = $this->redis->smembers($this->namespace . 'days');
|
||||||
|
}
|
||||||
|
foreach (self::BENCHMARK_SCOPES as $scope) {
|
||||||
|
if (empty($scope_filter) || in_array($scope, $scope_filter)) {
|
||||||
|
foreach (self::BENCHMARK_FIELDS as $field) {
|
||||||
|
$results[$scope][$field] = $this->getTopList($scope, $field, $days, $limit, $average, $aggregate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,8 +6,12 @@ class CurlClient extends HttpSocketExtended
|
||||||
/** @var resource */
|
/** @var resource */
|
||||||
private $ch;
|
private $ch;
|
||||||
|
|
||||||
/** @var int */
|
/**
|
||||||
private $timeout = 10800;
|
* Maximum time the transfer is allowed to complete in seconds
|
||||||
|
* 300 seconds is recommended timeout for MISP servers
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $timeout = 300;
|
||||||
|
|
||||||
/** @var string|null */
|
/** @var string|null */
|
||||||
private $caFile;
|
private $caFile;
|
||||||
|
@ -30,6 +34,9 @@ class CurlClient extends HttpSocketExtended
|
||||||
/** @var array */
|
/** @var array */
|
||||||
private $proxy = [];
|
private $proxy = [];
|
||||||
|
|
||||||
|
/** @var array */
|
||||||
|
private $defaultOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $params
|
* @param array $params
|
||||||
* @noinspection PhpMissingParentConstructorInspection
|
* @noinspection PhpMissingParentConstructorInspection
|
||||||
|
@ -38,8 +45,6 @@ class CurlClient extends HttpSocketExtended
|
||||||
{
|
{
|
||||||
if (isset($params['timeout'])) {
|
if (isset($params['timeout'])) {
|
||||||
$this->timeout = $params['timeout'];
|
$this->timeout = $params['timeout'];
|
||||||
} else {
|
|
||||||
$this->timeout = Configure::check('MISP.curl_request_timeout') ? Configure::read('MISP.curl_request_timeout') : 10800;
|
|
||||||
}
|
}
|
||||||
if (isset($params['ssl_cafile'])) {
|
if (isset($params['ssl_cafile'])) {
|
||||||
$this->caFile = $params['ssl_cafile'];
|
$this->caFile = $params['ssl_cafile'];
|
||||||
|
@ -59,6 +64,7 @@ class CurlClient extends HttpSocketExtended
|
||||||
if (isset($params['ssl_verify_peer'])) {
|
if (isset($params['ssl_verify_peer'])) {
|
||||||
$this->verifyPeer = $params['ssl_verify_peer'];
|
$this->verifyPeer = $params['ssl_verify_peer'];
|
||||||
}
|
}
|
||||||
|
$this->defaultOptions = $this->generateDefaultOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,6 +172,7 @@ class CurlClient extends HttpSocketExtended
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->proxy = compact('host', 'port', 'method', 'user', 'pass');
|
$this->proxy = compact('host', 'port', 'method', 'user', 'pass');
|
||||||
|
$this->defaultOptions = $this->generateDefaultOptions(); // regenerate default options in case proxy setting is changed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -196,7 +203,7 @@ class CurlClient extends HttpSocketExtended
|
||||||
$url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
|
$url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
|
||||||
}
|
}
|
||||||
|
|
||||||
$options = $this->generateOptions();
|
$options = $this->defaultOptions; // this will copy default options
|
||||||
$options[CURLOPT_URL] = $url;
|
$options[CURLOPT_URL] = $url;
|
||||||
$options[CURLOPT_CUSTOMREQUEST] = $method;
|
$options[CURLOPT_CUSTOMREQUEST] = $method;
|
||||||
|
|
||||||
|
@ -303,7 +310,7 @@ class CurlClient extends HttpSocketExtended
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function generateOptions()
|
private function generateDefaultOptions()
|
||||||
{
|
{
|
||||||
$options = [
|
$options = [
|
||||||
CURLOPT_FOLLOWLOCATION => true, // Allows to follow redirect
|
CURLOPT_FOLLOWLOCATION => true, // Allows to follow redirect
|
||||||
|
|
|
@ -24,7 +24,7 @@ class HttpSocketHttpException extends Exception
|
||||||
$message .= " for URL $url";
|
$message .= " for URL $url";
|
||||||
}
|
}
|
||||||
if ($response->body) {
|
if ($response->body) {
|
||||||
$message .= ': ' . substr($response->body, 0, 100);
|
$message .= ': ' . substr(ltrim($response->body), 0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
parent::__construct($message, (int)$response->code);
|
parent::__construct($message, (int)$response->code);
|
||||||
|
@ -121,7 +121,8 @@ class HttpSocketResponseExtended extends HttpSocketResponse
|
||||||
try {
|
try {
|
||||||
return JsonTool::decode($this->body);
|
return JsonTool::decode($this->body);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
throw new HttpSocketJsonException('Could not parse response as JSON.', $this, $e);
|
$contentType = $this->getHeader('content-type');
|
||||||
|
throw new HttpSocketJsonException("Could not parse HTTP response as JSON. Received Content-Type $contentType.", $this, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,29 +297,24 @@ class ServerSyncTool
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $eventUuids
|
* @param array $eventUuids
|
||||||
|
* @param array $blockedOrgs Blocked organisation UUIDs
|
||||||
* @return array
|
* @return array
|
||||||
* @throws HttpSocketHttpException
|
* @throws HttpSocketHttpException
|
||||||
* @throws HttpSocketJsonException
|
* @throws HttpSocketJsonException
|
||||||
* @throws JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
public function fetchSightingsForEvents(array $eventUuids)
|
public function fetchSightingsForEvents(array $eventUuids, array $blockedOrgs = [])
|
||||||
{
|
{
|
||||||
$SightingBlocklist = ClassRegistry::init('SightingBlocklist');
|
|
||||||
$blocked_sightings = $SightingBlocklist->find('column', [
|
|
||||||
'recursive' => -1,
|
|
||||||
'fields' => ['org_uuid']
|
|
||||||
]);
|
|
||||||
foreach ($blocked_sightings as $k => $uuid) {
|
|
||||||
$blocked_sightings[$k] = '!' . $uuid;
|
|
||||||
}
|
|
||||||
$postParams = [
|
$postParams = [
|
||||||
'returnFormat' => 'json',
|
'returnFormat' => 'json',
|
||||||
'last' => 0, // fetch all
|
'last' => 0, // fetch all
|
||||||
'includeUuid' => true,
|
'includeUuid' => true,
|
||||||
'uuid' => $eventUuids,
|
'uuid' => $eventUuids,
|
||||||
];
|
];
|
||||||
if (!empty($blocked_sightings)) {
|
if (!empty($blockedOrgs)) {
|
||||||
$postParams['org_id'] = $blocked_sightings;
|
$postParams['org_id'] = array_map(function ($uuid) {
|
||||||
|
return "!$uuid";
|
||||||
|
}, $blockedOrgs);
|
||||||
}
|
}
|
||||||
return $this->post('/sightings/restSearch/event', $postParams)->json()['response'];
|
return $this->post('/sightings/restSearch/event', $postParams)->json()['response'];
|
||||||
}
|
}
|
||||||
|
@ -511,6 +506,16 @@ class ServerSyncTool
|
||||||
return $this->socket->getMetaData();
|
return $this->socket->getMetaData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function debug($message)
|
||||||
|
{
|
||||||
|
$memoryUsage = round(memory_get_usage() / 1024 / 1024, 2);
|
||||||
|
CakeLog::debug("[Server sync #{$this->serverId()}]: $message. Memory: $memoryUsage MB");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @params string $url Relative URL
|
* @params string $url Relative URL
|
||||||
* @return HttpSocketResponseExtended
|
* @return HttpSocketResponseExtended
|
||||||
|
@ -561,6 +566,7 @@ class ServerSyncTool
|
||||||
|
|
||||||
if ($etag) {
|
if ($etag) {
|
||||||
// Remove compression marks that adds Apache for compressed content
|
// Remove compression marks that adds Apache for compressed content
|
||||||
|
// This can be removed in future as this is already checked by MISP itself since 2024-03
|
||||||
$etagWithoutQuotes = trim($etag, '"');
|
$etagWithoutQuotes = trim($etag, '"');
|
||||||
$dashPos = strrpos($etagWithoutQuotes, '-');
|
$dashPos = strrpos($etagWithoutQuotes, '-');
|
||||||
if ($dashPos && in_array(substr($etagWithoutQuotes, $dashPos + 1), ['br', 'gzip'], true)) {
|
if ($dashPos && in_array(substr($etagWithoutQuotes, $dashPos + 1), ['br', 'gzip'], true)) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
class SyncTool
|
class SyncTool
|
||||||
{
|
{
|
||||||
|
|
||||||
const ALLOWED_CERT_FILE_EXTENSIONS = ['pem', 'crt'];
|
const ALLOWED_CERT_FILE_EXTENSIONS = ['pem', 'crt'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +49,7 @@ class SyncTool
|
||||||
* @return HttpSocketExtended
|
* @return HttpSocketExtended
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function createHttpSocket($params = array())
|
public function createHttpSocket(array $params = [])
|
||||||
{
|
{
|
||||||
// Use own CA PEM file
|
// Use own CA PEM file
|
||||||
$caPath = Configure::read('MISP.ca_path');
|
$caPath = Configure::read('MISP.ca_path');
|
||||||
|
@ -82,10 +81,11 @@ class SyncTool
|
||||||
}
|
}
|
||||||
$params['ssl_crypto_method'] = $version;
|
$params['ssl_crypto_method'] = $version;
|
||||||
}
|
}
|
||||||
if (!isset($params['timeout'])) {
|
|
||||||
$params['timeout'] = Configure::check('MISP.curl_request_timeout') ? Configure::read('MISP.curl_request_timeout') : 10800;
|
|
||||||
}
|
|
||||||
if (function_exists('curl_init')) {
|
if (function_exists('curl_init')) {
|
||||||
|
if (!isset($params['timeout']) && Configure::check('MISP.curl_request_timeout')) {
|
||||||
|
$params['timeout'] = (int)Configure::read('MISP.curl_request_timeout');
|
||||||
|
}
|
||||||
App::uses('CurlClient', 'Tools');
|
App::uses('CurlClient', 'Tools');
|
||||||
$HttpSocket = new CurlClient($params);
|
$HttpSocket = new CurlClient($params);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -43,7 +43,7 @@ class AnalystData extends AppModel
|
||||||
'distribution',
|
'distribution',
|
||||||
'sharing_group_id',
|
'sharing_group_id',
|
||||||
];
|
];
|
||||||
protected $EDITABLE_FIELDS = [];
|
public const EDITABLE_FIELDS = [];
|
||||||
|
|
||||||
/** @var object|null */
|
/** @var object|null */
|
||||||
protected $Note;
|
protected $Note;
|
||||||
|
@ -185,7 +185,7 @@ class AnalystData extends AppModel
|
||||||
|
|
||||||
public function getEditableFields(): array
|
public function getEditableFields(): array
|
||||||
{
|
{
|
||||||
return array_merge(self::BASE_EDITABLE_FIELDS, $this->EDITABLE_FIELDS);
|
return array_merge(static::BASE_EDITABLE_FIELDS, static::EDITABLE_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -641,9 +641,8 @@ class AnalystData extends AppModel
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$this->Server = ClassRegistry::init('Server');
|
$this->Server = ClassRegistry::init('Server');
|
||||||
$this->AnalystData = ClassRegistry::init('AnalystData');
|
|
||||||
|
|
||||||
$this->log("Starting Analyst Data sync with server #{$server['Server']['id']}", LOG_INFO);
|
$serverSync->debug("Starting Analyst Data sync");
|
||||||
|
|
||||||
$analystData = $this->collectDataForPush($serverSync->server());
|
$analystData = $this->collectDataForPush($serverSync->server());
|
||||||
$keyedAnalystData = [];
|
$keyedAnalystData = [];
|
||||||
|
@ -1018,7 +1017,6 @@ class AnalystData extends AppModel
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->Server = ClassRegistry::init('Server');
|
$this->Server = ClassRegistry::init('Server');
|
||||||
$this->AnalystData = ClassRegistry::init('AnalystData');
|
|
||||||
try {
|
try {
|
||||||
$filterRules = $this->buildPullFilterRules($serverSync->server());
|
$filterRules = $this->buildPullFilterRules($serverSync->server());
|
||||||
$remoteData = $serverSync->fetchIndexMinimal($filterRules)->json();
|
$remoteData = $serverSync->fetchIndexMinimal($filterRules)->json();
|
||||||
|
|
|
@ -91,7 +91,7 @@ class AppModel extends Model
|
||||||
105 => false, 106 => false, 107 => false, 108 => false, 109 => false, 110 => false,
|
105 => false, 106 => false, 107 => false, 108 => false, 109 => false, 110 => false,
|
||||||
111 => false, 112 => false, 113 => true, 114 => false, 115 => false, 116 => false,
|
111 => false, 112 => false, 113 => true, 114 => false, 115 => false, 116 => false,
|
||||||
117 => false, 118 => false, 119 => false, 120 => false, 121 => false, 122 => false,
|
117 => false, 118 => false, 119 => false, 120 => false, 121 => false, 122 => false,
|
||||||
123 => false, 124 => false,
|
123 => false, 124 => false, 125 => false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ADVANCED_UPDATES_DESCRIPTION = array(
|
const ADVANCED_UPDATES_DESCRIPTION = array(
|
||||||
|
@ -2176,6 +2176,9 @@ class AppModel extends Model
|
||||||
INDEX `org_name` (`org_name`)
|
INDEX `org_name` (`org_name`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;';
|
||||||
break;
|
break;
|
||||||
|
case 125:
|
||||||
|
$sqlArray[] = "ALTER TABLE `feeds` ADD COLUMN `tag_collection_id` INT(11) NOT NULL DEFAULT 0;";
|
||||||
|
break;
|
||||||
case 'fixNonEmptySharingGroupID':
|
case 'fixNonEmptySharingGroupID':
|
||||||
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
|
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
|
||||||
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
|
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
|
||||||
|
|
|
@ -3163,6 +3163,7 @@ class Attribute extends AppModel
|
||||||
'includeFullModel' => !empty($filters['includeFullModel']) ? $filters['includeFullModel'] : 0,
|
'includeFullModel' => !empty($filters['includeFullModel']) ? $filters['includeFullModel'] : 0,
|
||||||
'allow_proposal_blocking' => !empty($filters['allow_proposal_blocking']) ? $filters['allow_proposal_blocking'] : 0
|
'allow_proposal_blocking' => !empty($filters['allow_proposal_blocking']) ? $filters['allow_proposal_blocking'] : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($filters['attackGalaxy'])) {
|
if (!empty($filters['attackGalaxy'])) {
|
||||||
$params['attackGalaxy'] = $filters['attackGalaxy'];
|
$params['attackGalaxy'] = $filters['attackGalaxy'];
|
||||||
}
|
}
|
||||||
|
@ -3388,20 +3389,60 @@ class Attribute extends AppModel
|
||||||
if (!empty($params['uuid'])) {
|
if (!empty($params['uuid'])) {
|
||||||
$params['uuid'] = $this->convert_filters($params['uuid']);
|
$params['uuid'] = $this->convert_filters($params['uuid']);
|
||||||
if (!empty($params['uuid']['OR'])) {
|
if (!empty($params['uuid']['OR'])) {
|
||||||
$conditions['AND'][] = array(
|
if ($options['scope'] == 'Attribute') {
|
||||||
'OR' => array(
|
$subQuery = [
|
||||||
'Event.uuid' => $params['uuid']['OR'],
|
'conditions' => ['uuid' => $params['uuid']['OR']],
|
||||||
'Attribute.uuid' => $params['uuid']['OR']
|
'fields' => ['id']
|
||||||
)
|
];
|
||||||
);
|
$pre_lookup = $this->Event->find('first', [
|
||||||
|
'conditions' => ['Event.uuid' => $params['uuid']['OR']],
|
||||||
|
'recursive' => -1,
|
||||||
|
'fields' => ['Event.id']
|
||||||
|
]);
|
||||||
|
if (empty($pre_lookup)) {
|
||||||
|
$conditions['AND'][] = array(
|
||||||
|
'OR' => array(
|
||||||
|
'Attribute.uuid' => $params['uuid']['OR']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$conditions['AND'][] = array(
|
||||||
|
'OR' => array(
|
||||||
|
$this->subQueryGenerator($this->Event, $subQuery, 'Attribute.event_id'),
|
||||||
|
'Attribute.uuid' => $params['uuid']['OR']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$conditions['AND'][] = array(
|
||||||
|
'OR' => array(
|
||||||
|
'Event.uuid' => $params['uuid']['OR'],
|
||||||
|
'Attribute.uuid' => $params['uuid']['OR']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!empty($params['uuid']['NOT'])) {
|
if (!empty($params['uuid']['NOT'])) {
|
||||||
$conditions['AND'][] = array(
|
if ($options['scope'] == 'Attribute') {
|
||||||
'NOT' => array(
|
$subQuery = [
|
||||||
'Event.uuid' => $params['uuid']['NOT'],
|
'conditions' => ['uuid' => $params['uuid']['OR']],
|
||||||
'Attribute.uuid' => $params['uuid']['NOT']
|
'fields' => ['id']
|
||||||
)
|
];
|
||||||
);
|
$conditions['AND'][] = [
|
||||||
|
'NOT' => [
|
||||||
|
$this->subQueryGenerator($this->Event, $subQuery, 'Attribute.event_id'),
|
||||||
|
'Attribute.uuid' => $params['uuid']['NOT']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$conditions['AND'][] = array(
|
||||||
|
'NOT' => array(
|
||||||
|
'Event.uuid' => $params['uuid']['NOT'],
|
||||||
|
'Attribute.uuid' => $params['uuid']['NOT']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $conditions;
|
return $conditions;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
App::uses('AppModel', 'Model');
|
||||||
|
|
||||||
|
class Benchmark extends AppModel
|
||||||
|
{
|
||||||
|
}
|
|
@ -710,7 +710,7 @@ class EventReport extends AppModel
|
||||||
'category' => $typeToCategoryMapping[$complexTypeToolEntry['default_type']][0],
|
'category' => $typeToCategoryMapping[$complexTypeToolEntry['default_type']][0],
|
||||||
'type' => $complexTypeToolEntry['default_type'],
|
'type' => $complexTypeToolEntry['default_type'],
|
||||||
'value' => $textToBeReplaced,
|
'value' => $textToBeReplaced,
|
||||||
'to_ids' => $complexTypeToolEntry['to_ids'],
|
'to_ids' => $complexTypeToolEntry['to_ids'] ?? 0,
|
||||||
];
|
];
|
||||||
$replacedContent = str_replace($complexTypeToolEntry['original_value'], $textToInject, $replacedContent);
|
$replacedContent = str_replace($complexTypeToolEntry['original_value'], $textToInject, $replacedContent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1032,7 +1032,7 @@ class Feed extends AppModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($feed['Feed']['tag_id']) {
|
if ($feed['Feed']['tag_id'] || $feed['Feed']['tag_collection_id']) {
|
||||||
if (empty($feed['Tag']['name'])) {
|
if (empty($feed['Tag']['name'])) {
|
||||||
$feed_tag = $this->Tag->find('first', [
|
$feed_tag = $this->Tag->find('first', [
|
||||||
'conditions' => [
|
'conditions' => [
|
||||||
|
@ -1041,23 +1041,42 @@ class Feed extends AppModel
|
||||||
'recursive' => -1,
|
'recursive' => -1,
|
||||||
'fields' => ['Tag.name', 'Tag.colour', 'Tag.id']
|
'fields' => ['Tag.name', 'Tag.colour', 'Tag.id']
|
||||||
]);
|
]);
|
||||||
$feed['Tag'] = $feed_tag['Tag'];
|
if (!empty($feed_tag)) {
|
||||||
|
$feed['Tag'] = $feed_tag['Tag'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!isset($event['Event']['Tag'])) {
|
if (!isset($event['Event']['Tag'])) {
|
||||||
$event['Event']['Tag'] = array();
|
$event['Event']['Tag'] = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$feedTag = $this->Tag->find('first', array('conditions' => array('Tag.id' => $feed['Feed']['tag_id']), 'recursive' => -1, 'fields' => array('Tag.name', 'Tag.colour', 'Tag.exportable')));
|
if (!empty($feed['Feed']['tag_collection_id'])) {
|
||||||
if (!empty($feedTag)) {
|
$this->TagCollection = ClassRegistry::init('TagCollection');
|
||||||
$found = false;
|
$tagCollectionID = $feed['Feed']['tag_collection_id'];
|
||||||
foreach ($event['Event']['Tag'] as $tag) {
|
$tagCollection = $this->TagCollection->find('first', [
|
||||||
if (strtolower($tag['name']) === strtolower($feedTag['Tag']['name'])) {
|
'recursive' => -1,
|
||||||
$found = true;
|
'conditions' => [
|
||||||
break;
|
'TagCollection.id' => $tagCollectionID,
|
||||||
}
|
],
|
||||||
|
'contain' => [
|
||||||
|
'TagCollectionTag' => ['Tag'],
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
foreach ($tagCollection['TagCollectionTag'] as $collectionTag) {
|
||||||
|
$event['Event']['Tag'][] = $collectionTag['Tag'];
|
||||||
}
|
}
|
||||||
if (!$found) {
|
} else {
|
||||||
$event['Event']['Tag'][] = $feedTag['Tag'];
|
$feedTag = $this->Tag->find('first', array('conditions' => array('Tag.id' => $feed['Feed']['tag_id']), 'recursive' => -1, 'fields' => array('Tag.name', 'Tag.colour', 'Tag.exportable')));
|
||||||
|
if (!empty($feedTag)) {
|
||||||
|
$found = false;
|
||||||
|
foreach ($event['Event']['Tag'] as $tag) {
|
||||||
|
if (strtolower($tag['name']) === strtolower($feedTag['Tag']['name'])) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$found) {
|
||||||
|
$event['Event']['Tag'][] = $feedTag['Tag'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1068,6 +1087,9 @@ class Feed extends AppModel
|
||||||
if (!empty($feed['Feed']['settings']['disable_correlation'])) {
|
if (!empty($feed['Feed']['settings']['disable_correlation'])) {
|
||||||
$event['Event']['disable_correlation'] = (bool) $feed['Feed']['settings']['disable_correlation'];
|
$event['Event']['disable_correlation'] = (bool) $feed['Feed']['settings']['disable_correlation'];
|
||||||
}
|
}
|
||||||
|
if (!empty($feed['Feed']['settings']['unpublish_event'])) {
|
||||||
|
$event['Event']['published'] = (bool) $feed['Feed']['settings']['unpublish_event'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $event;
|
return $event;
|
||||||
}
|
}
|
||||||
|
@ -1128,9 +1150,13 @@ class Feed extends AppModel
|
||||||
*/
|
*/
|
||||||
private function __updateEventFromFeed(HttpSocket $HttpSocket = null, $feed, $uuid, $user, $filterRules)
|
private function __updateEventFromFeed(HttpSocket $HttpSocket = null, $feed, $uuid, $user, $filterRules)
|
||||||
{
|
{
|
||||||
$event = $this->downloadAndParseEventFromFeed($feed, $uuid, $HttpSocket);
|
$event = $this->downloadAndParseEventFromFeed($feed, $uuid, $HttpSocket);
|
||||||
$event = $this->__prepareEvent($event, $feed, $filterRules);
|
$event = $this->__prepareEvent($event, $feed, $filterRules);
|
||||||
return $this->Event->_edit($event, $user, $uuid, $jobId = null);
|
if (is_array($event)) {
|
||||||
|
return $this->Event->_edit($event, $user, $uuid, $jobId = null);
|
||||||
|
} else {
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addDefaultFeeds($newFeeds)
|
public function addDefaultFeeds($newFeeds)
|
||||||
|
@ -1374,8 +1400,25 @@ class Feed extends AppModel
|
||||||
if ($feed['Feed']['publish']) {
|
if ($feed['Feed']['publish']) {
|
||||||
$this->Event->publishRouter($event['Event']['id'], null, $user);
|
$this->Event->publishRouter($event['Event']['id'], null, $user);
|
||||||
}
|
}
|
||||||
if ($feed['Feed']['tag_id']) {
|
if ($feed['Feed']['tag_id'] || $feed['Feed']['tag_collection_id']) {
|
||||||
$this->Event->EventTag->attachTagToEvent($event['Event']['id'], ['id' => $feed['Feed']['tag_id']]);
|
if (!empty($feed['Feed']['tag_collection_id'])) {
|
||||||
|
$this->TagCollection = ClassRegistry::init('TagCollection');
|
||||||
|
$tagCollectionID = $feed['Feed']['tag_collection_id'];
|
||||||
|
$tagCollection = $this->TagCollection->find('first', [
|
||||||
|
'recursive' => -1,
|
||||||
|
'conditions' => [
|
||||||
|
'TagCollection.id' => $tagCollectionID,
|
||||||
|
],
|
||||||
|
'contain' => [
|
||||||
|
'TagCollectionTag',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
foreach ($tagCollection['TagCollectionTag'] as $collectionTag) {
|
||||||
|
$this->Event->EventTag->attachTagToEvent($event['Event']['id'], ['id' => $collectionTag['tag_id']]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->Event->EventTag->attachTagToEvent($event['Event']['id'], ['id' => $feed['Feed']['tag_id']]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1845,6 +1845,9 @@ class GalaxyCluster extends AppModel
|
||||||
if (!$compatible) {
|
if (!$compatible) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$serverSync->debug("Pulling galaxy clusters with technique $technique");
|
||||||
|
|
||||||
$clusterIds = $this->getClusterIdListBasedOnPullTechnique($user, $technique, $serverSync);
|
$clusterIds = $this->getClusterIdListBasedOnPullTechnique($user, $technique, $serverSync);
|
||||||
$successes = 0;
|
$successes = 0;
|
||||||
// now process the $clusterIds to pull each of the events sequentially
|
// now process the $clusterIds to pull each of the events sequentially
|
||||||
|
|
|
@ -431,15 +431,14 @@ class Log extends AppModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$entry = $data['Log']['action'];
|
$entry = sprintf(
|
||||||
if (!empty($data['Log']['title'])) {
|
'%s -- %s -- %s',
|
||||||
$entry .= " -- {$data['Log']['title']}";
|
$data['Log']['action'],
|
||||||
}
|
empty($data['Log']['title']) ? '' : $formatted_title = preg_replace('/\s+/', " ", $data['Log']['title']),
|
||||||
if (!empty($data['Log']['description'])) {
|
empty($data['Log']['description']) ?
|
||||||
$entry .= " -- {$data['Log']['description']}";
|
(empty($data['Log']['change']) ? '' : preg_replace('/\s+/', " ", $data['Log']['change'])) :
|
||||||
} else if (!empty($data['Log']['change'])) {
|
preg_replace('/\s+/', " ", $data['Log']['description'])
|
||||||
$entry .= " -- " . JsonTool::encode($data['Log']['change']);
|
);
|
||||||
}
|
|
||||||
$this->syslog->write($action, $entry);
|
$this->syslog->write($action, $entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -604,6 +604,7 @@ class Server extends AppModel
|
||||||
* @throws HttpSocketHttpException
|
* @throws HttpSocketHttpException
|
||||||
* @throws HttpSocketJsonException
|
* @throws HttpSocketJsonException
|
||||||
* @throws JsonException
|
* @throws JsonException
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function pull(array $user, $technique, array $server, $jobId = false, $force = false)
|
public function pull(array $user, $technique, array $server, $jobId = false, $force = false)
|
||||||
{
|
{
|
||||||
|
@ -619,7 +620,7 @@ class Server extends AppModel
|
||||||
try {
|
try {
|
||||||
$server['Server']['version'] = $serverSync->info()['version'];
|
$server['Server']['version'] = $serverSync->info()['version'];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logException("Could not get remote server `{$server['Server']['name']}` version.", $e);
|
$this->logException("Could not get remote server `{$serverSync->serverName()}` version.", $e);
|
||||||
if ($e instanceof HttpSocketHttpException && $e->getCode() === 403) {
|
if ($e instanceof HttpSocketHttpException && $e->getCode() === 403) {
|
||||||
$message = __('Not authorised. This is either due to an invalid auth key, or due to the sync user not having authentication permissions enabled on the remote server. Another reason could be an incorrect sync server setting.');
|
$message = __('Not authorised. This is either due to an invalid auth key, or due to the sync user not having authentication permissions enabled on the remote server. Another reason could be an incorrect sync server setting.');
|
||||||
} else {
|
} else {
|
||||||
|
@ -648,6 +649,8 @@ class Server extends AppModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$serverSync->debug("Pulling event list with technique $technique");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$eventIds = $this->__getEventIdListBasedOnPullTechnique($technique, $serverSync, $force);
|
$eventIds = $this->__getEventIdListBasedOnPullTechnique($technique, $serverSync, $force);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
@ -673,26 +676,29 @@ class Server extends AppModel
|
||||||
$job->saveProgress($jobId, __n('Pulling %s event.', 'Pulling %s events.', count($eventIds), count($eventIds)));
|
$job->saveProgress($jobId, __n('Pulling %s event.', 'Pulling %s events.', count($eventIds), count($eventIds)));
|
||||||
}
|
}
|
||||||
foreach ($eventIds as $k => $eventId) {
|
foreach ($eventIds as $k => $eventId) {
|
||||||
|
$serverSync->debug("Pulling event $eventId");
|
||||||
$this->__pullEvent($eventId, $successes, $fails, $eventModel, $serverSync, $user, $jobId, $force);
|
$this->__pullEvent($eventId, $successes, $fails, $eventModel, $serverSync, $user, $jobId, $force);
|
||||||
if ($jobId && $k % 10 === 0) {
|
if ($jobId && $k % 10 === 0) {
|
||||||
$job->saveProgress($jobId, null, 10 + 40 * (($k + 1) / count($eventIds)));
|
$job->saveProgress($jobId, null, 10 + 40 * (($k + 1) / count($eventIds)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($fails as $eventid => $message) {
|
foreach ($fails as $eventid => $message) {
|
||||||
$this->loadLog()->createLogEntry($user, 'pull', 'Server', $server['Server']['id'], "Failed to pull event #$eventid.", 'Reason: ' . $message);
|
$this->loadLog()->createLogEntry($user, 'pull', 'Server', $serverSync->serverId(), "Failed to pull event #$eventid.", 'Reason: ' . $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($jobId) {
|
if ($jobId) {
|
||||||
$job->saveProgress($jobId, 'Pulling proposals.', 50);
|
$job->saveProgress($jobId, 'Pulling proposals.', 50);
|
||||||
}
|
}
|
||||||
$pulledProposals = $pulledSightings = 0;
|
$pulledProposals = $pulledSightings = $pulledAnalystData = 0;
|
||||||
if ($technique === 'full' || $technique === 'update') {
|
if ($technique === 'full' || $technique === 'update') {
|
||||||
$pulledProposals = $eventModel->ShadowAttribute->pullProposals($user, $serverSync);
|
$pulledProposals = $eventModel->ShadowAttribute->pullProposals($user, $serverSync);
|
||||||
|
|
||||||
if ($jobId) {
|
if ($jobId) {
|
||||||
$job->saveProgress($jobId, 'Pulling sightings.', 75);
|
$job->saveProgress($jobId, 'Pulling sightings.', 75);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pulledSightings = $eventModel->Sighting->pullSightings($user, $serverSync);
|
$pulledSightings = $eventModel->Sighting->pullSightings($user, $serverSync);
|
||||||
|
|
||||||
$this->AnalystData = ClassRegistry::init('AnalystData');
|
$this->AnalystData = ClassRegistry::init('AnalystData');
|
||||||
$pulledAnalystData = $this->AnalystData->pull($user, $serverSync);
|
$pulledAnalystData = $this->AnalystData->pull($user, $serverSync);
|
||||||
}
|
}
|
||||||
|
@ -819,7 +825,7 @@ class Server extends AppModel
|
||||||
*/
|
*/
|
||||||
public function getElligibleClusterIdsFromServerForPull(ServerSyncTool $serverSync, $onlyUpdateLocalCluster=true, array $eligibleClusters=array(), array $conditions=array())
|
public function getElligibleClusterIdsFromServerForPull(ServerSyncTool $serverSync, $onlyUpdateLocalCluster=true, array $eligibleClusters=array(), array $conditions=array())
|
||||||
{
|
{
|
||||||
$this->log("Fetching eligible clusters from server #{$serverSync->serverId()} for pull: " . JsonTool::encode($conditions), LOG_INFO);
|
$serverSync->debug("Fetching eligible clusters for pull: " . JsonTool::encode($conditions));
|
||||||
|
|
||||||
if ($onlyUpdateLocalCluster && empty($eligibleClusters)) {
|
if ($onlyUpdateLocalCluster && empty($eligibleClusters)) {
|
||||||
return []; // no clusters for update
|
return []; // no clusters for update
|
||||||
|
@ -875,7 +881,7 @@ class Server extends AppModel
|
||||||
*/
|
*/
|
||||||
private function getElligibleClusterIdsFromServerForPush(ServerSyncTool $serverSync, array $localClusters=array(), array $conditions=array())
|
private function getElligibleClusterIdsFromServerForPush(ServerSyncTool $serverSync, array $localClusters=array(), array $conditions=array())
|
||||||
{
|
{
|
||||||
$this->log("Fetching eligible clusters from server #{$serverSync->serverId()} for push: " . JsonTool::encode($conditions), LOG_INFO);
|
$serverSync->debug("Fetching eligible clusters for push: " . JsonTool::encode($conditions));
|
||||||
$clusterArray = $this->fetchCustomClusterIdsFromServer($serverSync, $conditions=$conditions);
|
$clusterArray = $this->fetchCustomClusterIdsFromServer($serverSync, $conditions=$conditions);
|
||||||
$keyedClusterArray = Hash::combine($clusterArray, '{n}.GalaxyCluster.uuid', '{n}.GalaxyCluster.version');
|
$keyedClusterArray = Hash::combine($clusterArray, '{n}.GalaxyCluster.uuid', '{n}.GalaxyCluster.version');
|
||||||
if (!empty($localClusters)) {
|
if (!empty($localClusters)) {
|
||||||
|
@ -915,9 +921,14 @@ class Server extends AppModel
|
||||||
|
|
||||||
// Fetch event index from cache if exists and is not modified
|
// Fetch event index from cache if exists and is not modified
|
||||||
$redis = RedisTool::init();
|
$redis = RedisTool::init();
|
||||||
$indexFromCache = $redis->get("misp:event_index:{$serverSync->serverId()}");
|
$indexFromCache = $redis->get("misp:event_index_cache:{$serverSync->serverId()}");
|
||||||
if ($indexFromCache) {
|
if ($indexFromCache) {
|
||||||
list($etag, $eventIndex) = RedisTool::deserialize(RedisTool::decompress($indexFromCache));
|
$etagPos = strpos($indexFromCache, "\n");
|
||||||
|
if ($etagPos === false) {
|
||||||
|
throw new RuntimeException("Could not find etag in cache fro server {$serverSync->serverId()}");
|
||||||
|
}
|
||||||
|
$etag = substr($indexFromCache, 0, $etagPos);
|
||||||
|
$serverSync->debug("Event index loaded from Redis cache with etag $etag containing");
|
||||||
} else {
|
} else {
|
||||||
$etag = '""'; // Provide empty ETag, so MISP will compute ETag for returned data
|
$etag = '""'; // Provide empty ETag, so MISP will compute ETag for returned data
|
||||||
}
|
}
|
||||||
|
@ -925,9 +936,21 @@ class Server extends AppModel
|
||||||
$response = $serverSync->eventIndex($filterRules, $etag);
|
$response = $serverSync->eventIndex($filterRules, $etag);
|
||||||
|
|
||||||
if ($response->isNotModified() && $indexFromCache) {
|
if ($response->isNotModified() && $indexFromCache) {
|
||||||
return $eventIndex;
|
return JsonTool::decode(RedisTool::decompress(substr($indexFromCache, $etagPos + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save to cache for 24 hours if ETag provided
|
||||||
|
$etag = $response->getHeader('etag');
|
||||||
|
if ($etag) {
|
||||||
|
$serverSync->debug("Event index from remote server has different etag $etag, saving to cache");
|
||||||
|
$data = "$etag\n" . RedisTool::compress($response->body);
|
||||||
|
$redis->setex("misp:event_index_cache:{$serverSync->serverId()}", 3600 * 24, $data);
|
||||||
|
} elseif ($indexFromCache) {
|
||||||
|
RedisTool::unlink($redis, "misp:event_index_cache:{$serverSync->serverId()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($indexFromCache); // clean up memory
|
||||||
|
|
||||||
$eventIndex = $response->json();
|
$eventIndex = $response->json();
|
||||||
|
|
||||||
// correct $eventArray if just one event, probably this response returns old MISP
|
// correct $eventArray if just one event, probably this response returns old MISP
|
||||||
|
@ -935,15 +958,6 @@ class Server extends AppModel
|
||||||
$eventIndex = [$eventIndex];
|
$eventIndex = [$eventIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to cache for 24 hours if ETag provided
|
|
||||||
$etag = $response->getHeader('etag');
|
|
||||||
if ($etag) {
|
|
||||||
$data = RedisTool::compress(RedisTool::serialize([$etag, $eventIndex]));
|
|
||||||
$redis->setex("misp:event_index:{$serverSync->serverId()}", 3600 * 24, $data);
|
|
||||||
} elseif ($indexFromCache) {
|
|
||||||
RedisTool::unlink($redis, "misp:event_index:{$serverSync->serverId()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $eventIndex;
|
return $eventIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1372,7 +1386,7 @@ class Server extends AppModel
|
||||||
return []; // pushing clusters is not enabled
|
return []; // pushing clusters is not enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->log("Starting $technique clusters sync with server #{$serverSync->serverId()}", LOG_INFO);
|
$serverSync->debug("Starting $technique clusters sync");
|
||||||
|
|
||||||
$this->GalaxyCluster = ClassRegistry::init('GalaxyCluster');
|
$this->GalaxyCluster = ClassRegistry::init('GalaxyCluster');
|
||||||
$this->Event = ClassRegistry::init('Event');
|
$this->Event = ClassRegistry::init('Event');
|
||||||
|
@ -5125,8 +5139,8 @@ class Server extends AppModel
|
||||||
),
|
),
|
||||||
'curl_request_timeout' => [
|
'curl_request_timeout' => [
|
||||||
'level' => 1,
|
'level' => 1,
|
||||||
'description' => __('Control the timeout of curl requests issued by MISP (during synchronisation, feed fetching, etc.'),
|
'description' => __('Control the default timeout in seconds of curl HTTP requests issued by MISP (during synchronisation, feed fetching, etc.)'),
|
||||||
'value' => 10800,
|
'value' => 300,
|
||||||
'test' => 'testForNumeric',
|
'test' => 'testForNumeric',
|
||||||
'type' => 'numeric',
|
'type' => 'numeric',
|
||||||
'null' => true
|
'null' => true
|
||||||
|
@ -7536,6 +7550,13 @@ class Server extends AppModel
|
||||||
'test' => 'testBool',
|
'test' => 'testBool',
|
||||||
'type' => 'boolean'
|
'type' => 'boolean'
|
||||||
),
|
),
|
||||||
|
'Benchmarking_enable' => [
|
||||||
|
'level' => 2,
|
||||||
|
'description' => __('Enable the benchmarking functionalities to capture information about execution times, SQL query loads and more per user and per endpoint.'),
|
||||||
|
'value' => false,
|
||||||
|
'test' => 'testBool',
|
||||||
|
'type' => 'boolean'
|
||||||
|
],
|
||||||
'Enrichment_services_enable' => array(
|
'Enrichment_services_enable' => array(
|
||||||
'level' => 0,
|
'level' => 0,
|
||||||
'description' => __('Enable/disable the enrichment services'),
|
'description' => __('Enable/disable the enrichment services'),
|
||||||
|
|
|
@ -706,6 +706,8 @@ class ShadowAttribute extends AppModel
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$serverSync->debug("Pulling proposals");
|
||||||
|
|
||||||
$i = 1;
|
$i = 1;
|
||||||
$fetchedCount = 0;
|
$fetchedCount = 0;
|
||||||
$chunkSize = 1000;
|
$chunkSize = 1000;
|
||||||
|
|
|
@ -1418,11 +1418,13 @@ class Sighting extends AppModel
|
||||||
*/
|
*/
|
||||||
public function pullSightings(array $user, ServerSyncTool $serverSync)
|
public function pullSightings(array $user, ServerSyncTool $serverSync)
|
||||||
{
|
{
|
||||||
|
$serverSync->debug("Fetching event index for pulling sightings");
|
||||||
|
|
||||||
$this->Server = ClassRegistry::init('Server');
|
$this->Server = ClassRegistry::init('Server');
|
||||||
try {
|
try {
|
||||||
$remoteEvents = $this->Server->getEventIndexFromServer($serverSync);
|
$remoteEvents = $this->Server->getEventIndexFromServer($serverSync);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logException("Could not fetch event IDs from server {$serverSync->server()['Server']['name']}", $e);
|
$this->logException("Could not fetch event IDs from server {$serverSync->serverName()}", $e);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// Remove events from list that do not have published sightings.
|
// Remove events from list that do not have published sightings.
|
||||||
|
@ -1452,6 +1454,8 @@ class Sighting extends AppModel
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$serverSync->debug("Pulling sightings for " . count($eventUuids) . " events");
|
||||||
|
|
||||||
if ($serverSync->isSupported(ServerSyncTool::FEATURE_SIGHTING_REST_SEARCH)) {
|
if ($serverSync->isSupported(ServerSyncTool::FEATURE_SIGHTING_REST_SEARCH)) {
|
||||||
return $this->pullSightingNewWay($user, $eventUuids, $serverSync);
|
return $this->pullSightingNewWay($user, $eventUuids, $serverSync);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1470,12 +1474,19 @@ class Sighting extends AppModel
|
||||||
*/
|
*/
|
||||||
private function pullSightingNewWay(array $user, array $eventUuids, ServerSyncTool $serverSync)
|
private function pullSightingNewWay(array $user, array $eventUuids, ServerSyncTool $serverSync)
|
||||||
{
|
{
|
||||||
|
$SightingBlocklist = ClassRegistry::init('SightingBlocklist');
|
||||||
|
$blockedSightingsOrgs = $SightingBlocklist->find('column', [
|
||||||
|
'recursive' => -1,
|
||||||
|
'fields' => ['org_uuid']
|
||||||
|
]);
|
||||||
|
|
||||||
$uuids = array_keys($eventUuids);
|
$uuids = array_keys($eventUuids);
|
||||||
|
shuffle($uuids); // shuffle array to avoid keeping events with a lof ot sightings in same batch all the time
|
||||||
$saved = 0;
|
$saved = 0;
|
||||||
$savedEventUuids = [];
|
$savedEventUuids = [];
|
||||||
foreach (array_chunk($uuids, 100) as $chunk) {
|
foreach (array_chunk($uuids, 20) as $chunk) {
|
||||||
try {
|
try {
|
||||||
$sightings = $serverSync->fetchSightingsForEvents($chunk);
|
$sightings = $serverSync->fetchSightingsForEvents($chunk, $blockedSightingsOrgs);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logException("Failed to download sightings from remote server {$serverSync->server()['Server']['name']}.", $e);
|
$this->logException("Failed to download sightings from remote server {$serverSync->server()['Server']['name']}.", $e);
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -213,6 +213,8 @@ class WorkflowBaseModule
|
||||||
if ($operator == 'in_or') {
|
if ($operator == 'in_or') {
|
||||||
return !empty($matching);
|
return !empty($matching);
|
||||||
} elseif ($operator == 'in_and') {
|
} elseif ($operator == 'in_and') {
|
||||||
|
sort($matching);
|
||||||
|
sort($value);
|
||||||
return array_values($matching) == array_values($value);
|
return array_values($matching) == array_values($value);
|
||||||
} elseif ($operator == 'not_in_or') {
|
} elseif ($operator == 'not_in_or') {
|
||||||
return empty($matching);
|
return empty($matching);
|
||||||
|
|
|
@ -6,6 +6,7 @@ class Module_stop_execution extends WorkflowBaseActionModule
|
||||||
public $blocking = true;
|
public $blocking = true;
|
||||||
public $id = 'stop-execution';
|
public $id = 'stop-execution';
|
||||||
public $name = 'Stop execution';
|
public $name = 'Stop execution';
|
||||||
|
public $version = '0.2';
|
||||||
public $description = 'Essentially stops the execution for blocking workflows. Do nothing for non-blocking ones';
|
public $description = 'Essentially stops the execution for blocking workflows. Do nothing for non-blocking ones';
|
||||||
public $icon = 'ban';
|
public $icon = 'ban';
|
||||||
public $inputs = 1;
|
public $inputs = 1;
|
||||||
|
@ -15,12 +16,25 @@ class Module_stop_execution extends WorkflowBaseActionModule
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->params = [
|
||||||
|
[
|
||||||
|
'id' => 'message',
|
||||||
|
'label' => 'Stop message',
|
||||||
|
'type' => 'input',
|
||||||
|
'default' => __('Execution stopped'),
|
||||||
|
'placeholder' => __('Execution stopped'),
|
||||||
|
'jinja_supported' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool
|
public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool
|
||||||
{
|
{
|
||||||
parent::exec($node, $roamingData, $errors);
|
parent::exec($node, $roamingData, $errors);
|
||||||
$errors[] = __('Execution stopped');
|
$rData = $roamingData->getData();
|
||||||
|
$params = $this->getParamsWithValues($node, $rData);
|
||||||
|
$errors[] = empty($params['message']['value']) ? $params['message']['default'] : $params['message']['value'];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
||||||
{
|
{
|
||||||
public $id = 'distribution-if';
|
public $id = 'distribution-if';
|
||||||
public $name = 'IF :: Distribution';
|
public $name = 'IF :: Distribution';
|
||||||
public $version = '0.2';
|
public $version = '0.3';
|
||||||
public $description = 'Distribution IF / ELSE condition block. The `then` output will be used if the encoded conditions is satisfied, otherwise the `else` output will be used.';
|
public $description = 'Distribution IF / ELSE condition block. The `then` output will be used if the encoded conditions is satisfied, otherwise the `else` output will be used.';
|
||||||
public $icon = 'code-branch';
|
public $icon = 'code-branch';
|
||||||
public $inputs = 1;
|
public $inputs = 1;
|
||||||
|
@ -103,12 +103,15 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
||||||
$final_sharing_group = $this->__extractSharingGroupIDs(
|
$final_sharing_group = $this->__extractSharingGroupIDs(
|
||||||
$data['Event'],
|
$data['Event'],
|
||||||
$data['Event']['Attribute'][0]['Object'] ?? [],
|
$data['Event']['Attribute'][0]['Object'] ?? [],
|
||||||
$data['Event']['Attribute'][0]
|
$data['Event']['Attribute'][0],
|
||||||
|
$scope
|
||||||
);
|
);
|
||||||
if ($operator == 'equals') {
|
if ($operator == 'equals') {
|
||||||
return !array_diff($final_sharing_group, $selected_sharing_groups); // All sharing groups are in the selection
|
return empty($selected_sharing_groups) ? !empty($final_sharing_group) :
|
||||||
|
!array_diff($final_sharing_group, $selected_sharing_groups); // All sharing groups are in the selection
|
||||||
} else if ($operator == 'not_equals') {
|
} else if ($operator == 'not_equals') {
|
||||||
return count(array_diff($final_sharing_group, $selected_sharing_groups)) == count($final_sharing_group); // All sharing groups are in the selection
|
return empty($selected_sharing_groups) ? empty($final_sharing_group) :
|
||||||
|
count(array_diff($final_sharing_group, $selected_sharing_groups)) == count($final_sharing_group); // All sharing groups are in the selection
|
||||||
}
|
}
|
||||||
$errors[] = __('Condition operator not supported for that distribution level');
|
$errors[] = __('Condition operator not supported for that distribution level');
|
||||||
return false;
|
return false;
|
||||||
|
@ -159,9 +162,15 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
||||||
return min($distri1, $distri2);
|
return min($distri1, $distri2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function __extractSharingGroupIDs(array $event, array $object=[], array $attribute=[]): array
|
private function __extractSharingGroupIDs(array $event, array $object=[], array $attribute=[], $scope='event'): array
|
||||||
{
|
{
|
||||||
$sgIDs = [];
|
$sgIDs = [];
|
||||||
|
if ($scope == 'event') {
|
||||||
|
if (!empty($event) && $event['distribution'] == 4) {
|
||||||
|
$sgIDs[] = $event['sharing_group_id'];
|
||||||
|
}
|
||||||
|
return $sgIDs;
|
||||||
|
}
|
||||||
if (!empty($event) && $event['distribution'] == 4) {
|
if (!empty($event) && $event['distribution'] == 4) {
|
||||||
$sgIDs[] = $event['sharing_group_id'];
|
$sgIDs[] = $event['sharing_group_id'];
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,15 +120,57 @@ echo $this->element(
|
||||||
);
|
);
|
||||||
|
|
||||||
$object_uuid = Hash::get($data, $modelSelection . '.uuid');
|
$object_uuid = Hash::get($data, $modelSelection . '.uuid');
|
||||||
|
|
||||||
|
$notes = $data[$modelSelection]['Note'] ?? [];
|
||||||
|
$opinions = $data[$modelSelection]['Opinion'] ?? [];
|
||||||
|
$relationships_outbound = $data[$modelSelection]['Relationship'] ?? [];
|
||||||
|
$relationships_inbound = $data[$modelSelection]['RelationshipInbound'] ?? [];
|
||||||
|
$notesOpinions = array_merge($notes, $opinions);
|
||||||
|
if(!function_exists("countNotes")) {
|
||||||
|
function countNotes($notesOpinions) {
|
||||||
|
$notesTotalCount = count($notesOpinions);
|
||||||
|
$notesCount = 0;
|
||||||
|
$relationsCount = 0;
|
||||||
|
foreach ($notesOpinions as $notesOpinion) {
|
||||||
|
if ($notesOpinion['note_type'] == 2) { // relationship
|
||||||
|
$relationsCount += 1;
|
||||||
|
} else {
|
||||||
|
$notesCount += 1;
|
||||||
|
}
|
||||||
|
if (!empty($notesOpinion['Note'])) {
|
||||||
|
$nestedCounts = countNotes($notesOpinion['Note']);
|
||||||
|
$notesTotalCount += $nestedCounts['total'];
|
||||||
|
$notesCount += $nestedCounts['notesOpinions'];
|
||||||
|
$relationsCount += $nestedCounts['relations'];
|
||||||
|
}
|
||||||
|
if (!empty($notesOpinion['Opinion'])) {
|
||||||
|
$nestedCounts = countNotes($notesOpinion['Opinion']);
|
||||||
|
$notesTotalCount += $nestedCounts['total'];
|
||||||
|
$notesCount += $nestedCounts['notesOpinions'];
|
||||||
|
$relationsCount += $nestedCounts['relations'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['total' => $notesTotalCount, 'notesOpinions' => $notesCount, 'relations' => $relationsCount];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$counts = countNotes($notesOpinions);
|
||||||
|
$notesOpinionCount = $counts['notesOpinions'];
|
||||||
|
$allCounts = [
|
||||||
|
'notesOpinions' => $counts['notesOpinions'],
|
||||||
|
'relationships_outbound' => count($relationships_outbound),
|
||||||
|
'relationships_inbound' => count($relationships_inbound),
|
||||||
|
];
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
'container_id' => 'analyst_data_thread',
|
'container_id' => 'analyst_data_thread',
|
||||||
'object_type' => $modelSelection,
|
'object_type' => $modelSelection,
|
||||||
'object_uuid' => $object_uuid,
|
'object_uuid' => $object_uuid,
|
||||||
'shortDist' => $shortDist,
|
'shortDist' => $shortDist,
|
||||||
'notes' => $data[$modelSelection]['Note'] ?? [],
|
'notes' => $notes,
|
||||||
'opinions' => $data[$modelSelection]['Opinion'] ?? [],
|
'opinions' => $opinions,
|
||||||
'relationships_outbound' => $data[$modelSelection]['Relationship'] ?? [],
|
'relationships_outbound' => $relationships_outbound,
|
||||||
'relationships_inbound' => $data[$modelSelection]['RelationshipInbound'] ?? [],
|
'relationships_inbound' => $relationships_inbound,
|
||||||
|
'allCounts' => $allCounts,
|
||||||
];
|
];
|
||||||
|
|
||||||
echo $this->element('genericElements/assetLoader', [
|
echo $this->element('genericElements/assetLoader', [
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
$passedArgsArray = json_decode($passedArgs, true);
|
||||||
|
$fields = [
|
||||||
|
[
|
||||||
|
'name' => __('Date'),
|
||||||
|
'sort' => 'date',
|
||||||
|
'data_path' => 'date'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('scope'),
|
||||||
|
'sort' => 'scope',
|
||||||
|
'data_path' => 'scope'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Key'),
|
||||||
|
'sort' => 'key',
|
||||||
|
'data_path' => 'text'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('field'),
|
||||||
|
'sort' => 'field',
|
||||||
|
'data_path' => 'field'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Value'),
|
||||||
|
'element' => 'custom',
|
||||||
|
'function' => function($row) {
|
||||||
|
return empty($row['unit']) ? h($row['value']) : h($row['value'] . ' ' . $row['unit']);
|
||||||
|
},
|
||||||
|
'sort' => 'value'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$quick_filters = [];
|
||||||
|
foreach ($settings as $key => $setting_data) {
|
||||||
|
$temp = $filters;
|
||||||
|
$url = $baseurl . '/benchmarks/index';
|
||||||
|
foreach ($temp as $s => $v) {
|
||||||
|
if ($v && $s != $key) {
|
||||||
|
if (is_array($v)) {
|
||||||
|
foreach ($v as $multi_v) {
|
||||||
|
$url .= '/' . $s . '[]:' . $multi_v;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$url .= '/' . $s . ':' . $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($key != 'average' && $key != 'aggregate') {
|
||||||
|
$quick_filters[$key]['all'] = [
|
||||||
|
'url' => h($url),
|
||||||
|
'text' => __('All'),
|
||||||
|
'active' => !$filters[$key],
|
||||||
|
'style' => 'display:inline;'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
foreach ($setting_data as $setting_element) {
|
||||||
|
$text = $setting_element;
|
||||||
|
if ($key == 'average') {
|
||||||
|
$text = $setting_element ? 'average / request' : 'total';
|
||||||
|
}
|
||||||
|
if ($key == 'aggregate') {
|
||||||
|
$text = $setting_element ? 'aggregate' : 'daily';
|
||||||
|
}
|
||||||
|
$quick_filters[$key][] = [
|
||||||
|
'url' => h($url . '/' . $key . ':' . $setting_element),
|
||||||
|
'text' => $text,
|
||||||
|
'active' => $filters[$key] == $setting_element,
|
||||||
|
'style' => 'display:inline;'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo $this->element('genericElements/IndexTable/scaffold', [
|
||||||
|
'scaffold_data' => [
|
||||||
|
'passedArgsArray' => $passedArgsArray,
|
||||||
|
'data' => [
|
||||||
|
'persistUrlParams' => array_keys($settings),
|
||||||
|
'data' => $data,
|
||||||
|
'top_bar' => [
|
||||||
|
'pull' => 'right',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'children' => $quick_filters['scope']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'children' => $quick_filters['field']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'children' => $quick_filters['average']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'children' => $quick_filters['aggregate']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'search',
|
||||||
|
'button' => __('Filter'),
|
||||||
|
'placeholder' => __('Enter value to search'),
|
||||||
|
'data' => '',
|
||||||
|
'searchKey' => 'quickFilter'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'fields' => $fields,
|
||||||
|
'title' => empty($ajax) ? __('Benchmark results') : false,
|
||||||
|
'description' => empty($ajax) ? __('Results of the collected benchmarks. You can filter it further by passing the limit, scope, field parameters.') : false,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
?>
|
|
@ -23,7 +23,7 @@ $(function () {
|
||||||
saveDashboardState();
|
saveDashboardState();
|
||||||
});
|
});
|
||||||
grid.on('added', function(event, items) {
|
grid.on('added', function(event, items) {
|
||||||
resetDashboardGrid(grid);
|
resetDashboardGrid(grid, false);
|
||||||
});
|
});
|
||||||
grid.on('gsresizestop', function(event, element) {
|
grid.on('gsresizestop', function(event, element) {
|
||||||
$(element).find('.widgetContentInner').trigger('widget-resized')
|
$(element).find('.widgetContentInner').trigger('widget-resized')
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
h($data['formula'])
|
h($data['formula'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
$y_axis = $data['y-axis'] ?? 'Count';
|
||||||
?>
|
?>
|
||||||
<div id="chartContainer-<?= $seed ?>" style="flex-grow: 1; position:relative;"></div>
|
<div id="chartContainer-<?= $seed ?>" style="flex-grow: 1; position:relative;"></div>
|
||||||
<script>
|
<script>
|
||||||
|
@ -50,7 +51,7 @@ function init<?= $seed ?>() { // variables and functions have their own scope (n
|
||||||
show_legend: true,
|
show_legend: true,
|
||||||
style: {
|
style: {
|
||||||
xlabel: "Date",
|
xlabel: "Date",
|
||||||
ylabel: "Count",
|
ylabel: "<?= h($y_axis) ?>",
|
||||||
hideXAxis: false,
|
hideXAxis: false,
|
||||||
hideYAxis: false,
|
hideYAxis: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -65,20 +65,8 @@ $allCounts = [
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('.node-opener-<?= $seed ?>').click(function() {
|
$('.node-opener-<?= $seed ?>').click(function() {
|
||||||
openNotes(this)
|
openNotes<?= $seed ?>(this)
|
||||||
})
|
})
|
||||||
|
|
||||||
function adjustPopoverPosition() {
|
|
||||||
var $popover = $('.popover:last');
|
|
||||||
$popover.css('top', Math.max($popover.position().top, 50) + 'px')
|
|
||||||
}
|
|
||||||
|
|
||||||
function openNotes(clicked) {
|
|
||||||
openPopover(clicked, renderedNotes<?= $seed ?>, undefined, undefined, function() {
|
|
||||||
adjustPopoverPosition()
|
|
||||||
$(clicked).removeClass('have-a-popover') // avoid closing the popover if a confirm popover (like the delete one) is called
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
$URL_DELETE = '/analystData/delete/';
|
$URL_DELETE = '/analystData/delete/';
|
||||||
|
|
||||||
$seed = isset($seed) ? $seed : mt_rand();
|
$seed = isset($seed) ? $seed : mt_rand();
|
||||||
|
$injectInPage = !empty($container_id) ? true : false;
|
||||||
|
|
||||||
$notes = !empty($notes) ? $notes : [];
|
$notes = !empty($notes) ? $notes : [];
|
||||||
$opinions = !empty($opinions) ? $opinions : [];
|
$opinions = !empty($opinions) ? $opinions : [];
|
||||||
|
@ -41,7 +42,28 @@
|
||||||
if (!window.shortDist) {
|
if (!window.shortDist) {
|
||||||
var shortDist = <?= json_encode($shortDist) ?>;
|
var shortDist = <?= json_encode($shortDist) ?>;
|
||||||
}
|
}
|
||||||
var renderedNotes<?= $seed ?> = null
|
|
||||||
|
var container_id = false
|
||||||
|
<?php if (isset($container_id)): ?>
|
||||||
|
container_id = '<?= h($container_id) ?>'
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
function adjustPopoverPosition() {
|
||||||
|
var $popover = $('.popover:last');
|
||||||
|
$popover.css('top', Math.max($popover.position().top, 50) + 'px')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNotes<?= $seed ?>(clicked) {
|
||||||
|
var notes = <?= json_encode($notesOpinions) ?>;
|
||||||
|
var relationships = <?= json_encode($relationshipsOutbound) ?>;
|
||||||
|
var relationships_inbound = <?= json_encode($relationshipsInbound) ?>;
|
||||||
|
var relationship_related_object = <?= json_encode($related_objects) ?>;
|
||||||
|
var renderedNotes = renderAllNotesWithForm<?= $seed ?>(notes, relationships, relationships_inbound, relationship_related_object)
|
||||||
|
openPopover(clicked, renderedNotes, undefined, undefined, function() {
|
||||||
|
adjustPopoverPosition()
|
||||||
|
$(clicked).removeClass('have-a-popover') // avoid closing the popover if a confirm popover (like the delete one) is called
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function renderNotes(notes, relationship_related_object, emptyMessage='<?= __('Empty') ?>', isInbound=false) {
|
function renderNotes(notes, relationship_related_object, emptyMessage='<?= __('Empty') ?>', isInbound=false) {
|
||||||
var renderedNotesArray = []
|
var renderedNotesArray = []
|
||||||
|
@ -406,18 +428,7 @@ function fetchMoreNotes(clicked, noteType, uuid) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nodeContainerTemplate<?= $seed ?> = doT.template('\
|
||||||
(function() {
|
|
||||||
var notes = <?= json_encode($notesOpinions) ?>;
|
|
||||||
var relationships = <?= json_encode($relationshipsOutbound) ?>;
|
|
||||||
var relationships_inbound = <?= json_encode($relationshipsInbound) ?>;
|
|
||||||
var relationship_related_object = <?= json_encode($related_objects) ?>;
|
|
||||||
var container_id = false
|
|
||||||
<?php if (isset($container_id)): ?>
|
|
||||||
container_id = '<?= h($container_id) ?>'
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
var nodeContainerTemplate = doT.template('\
|
|
||||||
<div> \
|
<div> \
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 10px;"> \
|
<ul class="nav nav-tabs" style="margin-bottom: 10px;"> \
|
||||||
<li class="active"><a href="#notes-<?= $seed ?>" data-toggle="tab"><i class="<?= $this->FontAwesome->getClass('sticky-note') ?>"></i> <?= __('Notes & Opinions') ?> <span class="label label-secondary"><?= $allCounts['notesOpinions'] ?></span></a></li> \
|
<li class="active"><a href="#notes-<?= $seed ?>" data-toggle="tab"><i class="<?= $this->FontAwesome->getClass('sticky-note') ?>"></i> <?= __('Notes & Opinions') ?> <span class="label label-secondary"><?= $allCounts['notesOpinions'] ?></span></a></li> \
|
||||||
|
@ -439,18 +450,6 @@ function fetchMoreNotes(clicked, noteType, uuid) {
|
||||||
</div> \
|
</div> \
|
||||||
')
|
')
|
||||||
|
|
||||||
function renderAllNotesWithForm(relationship_related_object) {
|
|
||||||
var buttonContainer = '<div id="add-button-container" style="margin-top: 0.5rem;">' + addNoteButton + addOpinionButton + '</div>'
|
|
||||||
renderedNotes<?= $seed ?> = nodeContainerTemplate({
|
|
||||||
content_notes: renderNotes(notes.filter(function(note) { return note.note_type != 2}), relationship_related_object, '<?= __('No notes for this UUID.') ?>') + buttonContainer,
|
|
||||||
content_relationships_outbound: renderNotes(relationships, relationship_related_object, '<?= __('No relationship from this UUID') ?>') + addRelationshipButton,
|
|
||||||
content_relationships_inbound: renderNotes(relationships_inbound, relationship_related_object, '<?= __('No element are referencing this UUID') ?>', true),
|
|
||||||
})
|
|
||||||
if (container_id) {
|
|
||||||
$('#' + container_id).html(renderedNotes<?= $seed ?>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var addNoteButton = '<button class="btn btn-small btn-block btn-primary" type="button" onclick="createNewNote(this, \'<?= $object_type ?>\', \'<?= $object_uuid ?>\')"> \
|
var addNoteButton = '<button class="btn btn-small btn-block btn-primary" type="button" onclick="createNewNote(this, \'<?= $object_type ?>\', \'<?= $object_uuid ?>\')"> \
|
||||||
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add a note') ?> \
|
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add a note') ?> \
|
||||||
</button>'
|
</button>'
|
||||||
|
@ -461,10 +460,15 @@ function fetchMoreNotes(clicked, noteType, uuid) {
|
||||||
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add a relationship') ?> \
|
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add a relationship') ?> \
|
||||||
</button>'
|
</button>'
|
||||||
|
|
||||||
$(document).ready(function() {
|
function renderAllNotesWithForm<?= $seed ?>(notes, relationships, relationships_inbound, relationship_related_object) {
|
||||||
renderAllNotesWithForm(relationship_related_object)
|
var buttonContainer = '<div id="add-button-container" style="margin-top: 0.5rem;">' + addNoteButton + addOpinionButton + '</div>'
|
||||||
})
|
var renderedNotes = nodeContainerTemplate<?= $seed ?>({
|
||||||
})()
|
content_notes: renderNotes(notes.filter(function(note) { return note.note_type != 2}), relationship_related_object, '<?= __('No notes for this UUID.') ?>') + buttonContainer,
|
||||||
|
content_relationships_outbound: renderNotes(relationships, relationship_related_object, '<?= __('No relationship from this UUID') ?>') + addRelationshipButton,
|
||||||
|
content_relationships_inbound: renderNotes(relationships_inbound, relationship_related_object, '<?= __('No element are referencing this UUID') ?>', true),
|
||||||
|
})
|
||||||
|
return renderedNotes
|
||||||
|
}
|
||||||
|
|
||||||
function createNewNote(clicked, object_type, object_uuid) {
|
function createNewNote(clicked, object_type, object_uuid) {
|
||||||
note_type = 'Note';
|
note_type = 'Note';
|
||||||
|
@ -516,6 +520,19 @@ function fetchMoreNotes(clicked, noteType, uuid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<?php if(!empty($injectInPage)): ?>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var notes = <?= json_encode($notesOpinions) ?>;
|
||||||
|
var relationships = <?= json_encode($relationshipsOutbound) ?>;
|
||||||
|
var relationships_inbound = <?= json_encode($relationshipsInbound) ?>;
|
||||||
|
var relationship_related_object = <?= json_encode($related_objects) ?>;
|
||||||
|
var renderedNotes = renderAllNotesWithForm<?= $seed ?>(notes, relationships, relationships_inbound, relationship_related_object)
|
||||||
|
if (container_id) {
|
||||||
|
$('#' + container_id).html(renderedNotes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -25,5 +25,29 @@
|
||||||
'hide_global_scope' => isset($field['hide_global_scope']) ? $field['hide_global_scope'] : false
|
'hide_global_scope' => isset($field['hide_global_scope']) ? $field['hide_global_scope'] : false
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
} else if (!empty($field['includeTagCollection']) && empty($tags)) {
|
||||||
|
if (!empty($row['TagCollection'])) {
|
||||||
|
echo sprintf('<a class="badge" style="background-color: #fff; color: #000; border: 1px solid #000;" title="%s" href="%s">%s :: %s</a>',
|
||||||
|
__('Tag Collection'),
|
||||||
|
'/tag_collections/view/' . h($row['TagCollection'][0]['TagCollection']['id']),
|
||||||
|
__('Tag Collection'),
|
||||||
|
h($row['TagCollection'][0]['TagCollection']['name'])
|
||||||
|
);
|
||||||
|
echo '<div>';
|
||||||
|
echo $this->element(
|
||||||
|
'ajaxTags',
|
||||||
|
[
|
||||||
|
'scope' => '',
|
||||||
|
'attributeId' => 0,
|
||||||
|
'tags' => Hash::extract($row['TagCollection'][0]['TagCollectionTag'], '{n}.Tag'),
|
||||||
|
'tagAccess' => false,
|
||||||
|
'localTagAccess' => false,
|
||||||
|
'static_tags_only' => 1,
|
||||||
|
'scope' => isset($field['scope']) ? $field['scope'] : 'event',
|
||||||
|
'hide_global_scope' => isset($field['hide_global_scope']) ? $field['hide_global_scope'] : false
|
||||||
|
]
|
||||||
|
);
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
if (!empty($data['persistUrlParams'])) {
|
if (!empty($data['persistUrlParams'])) {
|
||||||
foreach ($data['persistUrlParams'] as $persistedParam) {
|
foreach ($data['persistUrlParams'] as $persistedParam) {
|
||||||
if (!empty($passedArgsArray[$persistedParam])) {
|
if (!empty($passedArgsArray[$persistedParam])) {
|
||||||
$data['paginatorOptions']['url'][] = $passedArgsArray[$persistedParam];
|
$data['paginatorOptions']['url'][$persistedParam] = $passedArgsArray[$persistedParam];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1100,6 +1100,13 @@ $divider = '<li class="divider"></li>';
|
||||||
'url' => $baseurl . '/servers/updateProgress',
|
'url' => $baseurl . '/servers/updateProgress',
|
||||||
'text' => __('Update Progress')
|
'text' => __('Update Progress')
|
||||||
));
|
));
|
||||||
|
if (Configure::read('Plugin.Benchmarking_enable')) {
|
||||||
|
echo $divider;
|
||||||
|
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||||
|
'url' => $baseurl . '/benchmarks/index',
|
||||||
|
'text' => __('Benchmarks')
|
||||||
|
));
|
||||||
|
}
|
||||||
echo $divider;
|
echo $divider;
|
||||||
if (Configure::read('MISP.background_jobs')) {
|
if (Configure::read('MISP.background_jobs')) {
|
||||||
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
|
||||||
|
|
|
@ -408,6 +408,11 @@ if (!empty($me)) {
|
||||||
'url' => $baseurl . '/servers/serverSettings',
|
'url' => $baseurl . '/servers/serverSettings',
|
||||||
'requirement' => $isSiteAdmin
|
'requirement' => $isSiteAdmin
|
||||||
),
|
),
|
||||||
|
[
|
||||||
|
'text' => __('Benchmarking'),
|
||||||
|
'url' => $baseurl . '/benchmarks/index',
|
||||||
|
'requirement' => $isSiteAdmin && Configure::read('Plugin.Benchmarking_enable')
|
||||||
|
],
|
||||||
array(
|
array(
|
||||||
'type' => 'separator',
|
'type' => 'separator',
|
||||||
'requirement' => $isSiteAdmin
|
'requirement' => $isSiteAdmin
|
||||||
|
|
|
@ -26,6 +26,11 @@ echo $this->element('genericElements/Form/genericForm', [
|
||||||
'label' => __('Disable correlation'),
|
'label' => __('Disable correlation'),
|
||||||
'type' => 'checkbox'
|
'type' => 'checkbox'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'field' => 'Feed.settings.unpublish_event',
|
||||||
|
'label' => __('Unpublish events'),
|
||||||
|
'type' => 'checkbox'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'field' => 'name',
|
'field' => 'name',
|
||||||
'label' => __('Name'),
|
'label' => __('Name'),
|
||||||
|
@ -161,6 +166,14 @@ echo $this->element('genericElements/Form/genericForm', [
|
||||||
'type' => 'dropdown',
|
'type' => 'dropdown',
|
||||||
'searchable' => 1
|
'searchable' => 1
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'field' => 'tag_collection_id',
|
||||||
|
'label' => __('Default Tag Collection'),
|
||||||
|
'options' => $dropdownData['tag_collections'],
|
||||||
|
'selected' => isset($entity['Feed']['tag_collection_id']) ? $entity['Feed']['tag_collection_id'] : '0',
|
||||||
|
'type' => 'dropdown',
|
||||||
|
'searchable' => 1
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'field' => 'rules',
|
'field' => 'rules',
|
||||||
'label' => __('Filter rules'),
|
'label' => __('Filter rules'),
|
||||||
|
|
|
@ -193,7 +193,8 @@
|
||||||
'class' => 'short',
|
'class' => 'short',
|
||||||
'data_path' => 'Tag',
|
'data_path' => 'Tag',
|
||||||
'element' => 'tags',
|
'element' => 'tags',
|
||||||
'scope' => 'feeds'
|
'scope' => 'feeds',
|
||||||
|
'includeTagCollection' => true,
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'name' => __('Visible'),
|
'name' => __('Visible'),
|
||||||
|
|
|
@ -170,15 +170,57 @@ $md.html(md.render($md.text()));
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
$object_uuid = $cluster['GalaxyCluster']['uuid'];
|
$object_uuid = $cluster['GalaxyCluster']['uuid'];
|
||||||
|
|
||||||
|
$notes = $cluster['GalaxyCluster']['Note'] ?? [];
|
||||||
|
$opinions = $cluster['GalaxyCluster']['Opinion'] ?? [];
|
||||||
|
$relationships_outbound = $cluster['GalaxyCluster']['Relationship'] ?? [];
|
||||||
|
$relationships_inbound = $cluster['GalaxyCluster']['RelationshipInbound'] ?? [];
|
||||||
|
$notesOpinions = array_merge($notes, $opinions);
|
||||||
|
if(!function_exists("countNotes")) {
|
||||||
|
function countNotes($notesOpinions) {
|
||||||
|
$notesTotalCount = count($notesOpinions);
|
||||||
|
$notesCount = 0;
|
||||||
|
$relationsCount = 0;
|
||||||
|
foreach ($notesOpinions as $notesOpinion) {
|
||||||
|
if ($notesOpinion['note_type'] == 2) { // relationship
|
||||||
|
$relationsCount += 1;
|
||||||
|
} else {
|
||||||
|
$notesCount += 1;
|
||||||
|
}
|
||||||
|
if (!empty($notesOpinion['Note'])) {
|
||||||
|
$nestedCounts = countNotes($notesOpinion['Note']);
|
||||||
|
$notesTotalCount += $nestedCounts['total'];
|
||||||
|
$notesCount += $nestedCounts['notesOpinions'];
|
||||||
|
$relationsCount += $nestedCounts['relations'];
|
||||||
|
}
|
||||||
|
if (!empty($notesOpinion['Opinion'])) {
|
||||||
|
$nestedCounts = countNotes($notesOpinion['Opinion']);
|
||||||
|
$notesTotalCount += $nestedCounts['total'];
|
||||||
|
$notesCount += $nestedCounts['notesOpinions'];
|
||||||
|
$relationsCount += $nestedCounts['relations'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['total' => $notesTotalCount, 'notesOpinions' => $notesCount, 'relations' => $relationsCount];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$counts = countNotes($notesOpinions);
|
||||||
|
$notesOpinionCount = $counts['notesOpinions'];
|
||||||
|
$allCounts = [
|
||||||
|
'notesOpinions' => $counts['notesOpinions'],
|
||||||
|
'relationships_outbound' => count($relationships_outbound),
|
||||||
|
'relationships_inbound' => count($relationships_inbound),
|
||||||
|
];
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
'container_id' => 'analyst_data_thread',
|
'container_id' => 'analyst_data_thread',
|
||||||
'object_type' => 'GalaxyCluster',
|
'object_type' => 'GalaxyCluster',
|
||||||
'object_uuid' => $object_uuid,
|
'object_uuid' => $object_uuid,
|
||||||
'shortDist' => $shortDist,
|
'shortDist' => $shortDist,
|
||||||
'notes' => $cluster['GalaxyCluster']['Note'] ?? [],
|
'notes' => $notes,
|
||||||
'opinions' => $cluster['GalaxyCluster']['Opinion'] ?? [],
|
'opinions' => $opinions,
|
||||||
'relationships_outbound' => $cluster['GalaxyCluster']['Relationship'] ?? [],
|
'relationships_outbound' => $relationships_outbound,
|
||||||
'relationships_inbound' => $cluster['GalaxyCluster']['RelationshipInbound'] ?? [],
|
'relationships_inbound' => $relationships_inbound,
|
||||||
|
'allCounts' => $allCounts,
|
||||||
];
|
];
|
||||||
|
|
||||||
echo $this->element('genericElements/Analyst_data/thread', $options);
|
echo $this->element('genericElements/Analyst_data/thread', $options);
|
||||||
|
|
|
@ -177,7 +177,7 @@ if ($isAdmin && $isTotp) {
|
||||||
'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph')
|
'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph')
|
||||||
));
|
));
|
||||||
echo sprintf(
|
echo sprintf(
|
||||||
'<div class="users view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s</div></div>%s%s%s<div style="margin-top:20px;">%s%s</div></div>',
|
'<div class="users view"><div class="row-fluid"><div class="span8" style="margin:0px;">%s</div></div>%s%s%s<div style="margin-top:20px;">%s%s%s</div></div>',
|
||||||
sprintf(
|
sprintf(
|
||||||
'<h2>%s</h2>%s',
|
'<h2>%s</h2>%s',
|
||||||
__('User %s', h($user['User']['email'])),
|
__('User %s', h($user['User']['email'])),
|
||||||
|
@ -210,6 +210,15 @@ if ($isAdmin && $isTotp) {
|
||||||
__('Review user logins')
|
__('Review user logins')
|
||||||
),
|
),
|
||||||
$me['Role']['perm_auth'] ? $this->element('/genericElements/accordion', array('title' => __('Auth keys'), 'url' => '/auth_keys/index/' . h($user['User']['id']))) : '',
|
$me['Role']['perm_auth'] ? $this->element('/genericElements/accordion', array('title' => __('Auth keys'), 'url' => '/auth_keys/index/' . h($user['User']['id']))) : '',
|
||||||
|
$me['Role']['perm_site_admin'] ?
|
||||||
|
$this->element(
|
||||||
|
'/genericElements/accordion',
|
||||||
|
[
|
||||||
|
'title' => __('Benchmarks'),
|
||||||
|
'url' => '/benchmarks/index/scope:user/average:1/aggregate:1/key:' . h($user['User']['id'])
|
||||||
|
]
|
||||||
|
) :
|
||||||
|
'',
|
||||||
$this->element('/genericElements/accordion', array('title' => 'Events', 'url' => '/events/index/searchemail:' . urlencode(h($user['User']['email']))))
|
$this->element('/genericElements/accordion', array('title' => 'Events', 'url' => '/events/index/searchemail:' . urlencode(h($user['User']['email']))))
|
||||||
);
|
);
|
||||||
$current_menu = [
|
$current_menu = [
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c953d8ee5d1ec43bf8a418f957ad6f920fe1c0e8
|
Subproject commit 8e8c3fa93de383c86b27c00830bfb6314d2c8087
|
|
@ -1 +1 @@
|
||||||
Subproject commit dc52c10844cbed9e2f39f0429665b4f9b1caef3e
|
Subproject commit 96492b9c932a4b307216550abeadddc727e17cec
|
|
@ -1 +1 @@
|
||||||
Subproject commit 55e0f57d5d2953417e4084fe311a80bae99270f5
|
Subproject commit 272192fe3223a2a5f382b1f24540e49cf6a945ff
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7c69f4f987ee753901e952071556d5ad0fafd115
|
Subproject commit c282baa102adf07c10dec8ca94488a1ffd933e32
|
|
@ -17,6 +17,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
adapt_position_from_viewport();
|
adapt_position_from_viewport();
|
||||||
|
var firstTabId = $('#attack-matrix-tabscontroller span[data-toggle="tab"]:first').attr('href');
|
||||||
|
resizeHeader(firstTabId);
|
||||||
|
|
||||||
$('.ajax_popover_form .btn-matrix-submit').click(function() {
|
$('.ajax_popover_form .btn-matrix-submit').click(function() {
|
||||||
makeTagging(pickedGalaxies);
|
makeTagging(pickedGalaxies);
|
||||||
|
|
|
@ -5385,6 +5385,18 @@ function submitDashboardAddWidget() {
|
||||||
var height = $('#DashboardHeight').val();
|
var height = $('#DashboardHeight').val();
|
||||||
var el = null;
|
var el = null;
|
||||||
var k = $('#last-element-counter').data('element-counter');
|
var k = $('#last-element-counter').data('element-counter');
|
||||||
|
|
||||||
|
if (config === '') {
|
||||||
|
config = '[]'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
config = JSON.parse(config);
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('fail', error.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config = JSON.stringify(config);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: baseurl + '/dashboards/getEmptyWidget/' + widget + '/' + (k+1),
|
url: baseurl + '/dashboards/getEmptyWidget/' + widget + '/' + (k+1),
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
|
@ -5398,14 +5410,7 @@ function submitDashboardAddWidget() {
|
||||||
"autoposition": 1
|
"autoposition": 1
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (config !== '') {
|
|
||||||
config = JSON.parse(config);
|
|
||||||
config = JSON.stringify(config);
|
|
||||||
} else {
|
|
||||||
config = '[]';
|
|
||||||
}
|
|
||||||
$('#widget_' + (k+1)).attr('config', config);
|
$('#widget_' + (k+1)).attr('config', config);
|
||||||
saveDashboardState();
|
|
||||||
$('#last-element-counter').data('element-counter', (k+1));
|
$('#last-element-counter').data('element-counter', (k+1));
|
||||||
},
|
},
|
||||||
complete: function(data) {
|
complete: function(data) {
|
||||||
|
|
|
@ -1836,10 +1836,9 @@ function genPicker(options, forNode = true) {
|
||||||
var $container = genSelect(options)
|
var $container = genSelect(options)
|
||||||
var $select = $container.find('select')
|
var $select = $container.find('select')
|
||||||
$select.addClass('start-chosen')
|
$select.addClass('start-chosen')
|
||||||
if (options.picker_options) {
|
var pickerOptions = options.picker_options ?? {}
|
||||||
// $select.data('chosen_options', options.picker_options)
|
pickerOptions['max_shown_results'] = 100
|
||||||
$select.attr('data-chosen_options', JSON.stringify(options.picker_options))
|
$select.attr('data-chosen_options', JSON.stringify(pickerOptions))
|
||||||
}
|
|
||||||
return $container
|
return $container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3206,6 +3206,17 @@
|
||||||
"column_type": "int(11)",
|
"column_type": "int(11)",
|
||||||
"column_default": "0",
|
"column_default": "0",
|
||||||
"extra": ""
|
"extra": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "tag_collection_id",
|
||||||
|
"is_nullable": "NO",
|
||||||
|
"data_type": "int",
|
||||||
|
"character_maximum_length": null,
|
||||||
|
"numeric_precision": "10",
|
||||||
|
"collation_name": null,
|
||||||
|
"column_type": "int(11)",
|
||||||
|
"column_default": "0",
|
||||||
|
"extra": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fuzzy_correlate_ssdeep": [
|
"fuzzy_correlate_ssdeep": [
|
||||||
|
@ -10603,5 +10614,5 @@
|
||||||
"uuid": false
|
"uuid": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"db_version": "124"
|
"db_version": "125"
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ misp-lib-stix2>=3.0.1.1
|
||||||
mixbox>=1.0.5
|
mixbox>=1.0.5
|
||||||
plyara>=2.1.1
|
plyara>=2.1.1
|
||||||
pydeep2>=0.5.1
|
pydeep2>=0.5.1
|
||||||
pymisp==2.4.188
|
pymisp==2.4.190
|
||||||
python-magic>=0.4.27
|
python-magic>=0.4.27
|
||||||
pyzmq>=25.1.1
|
pyzmq>=25.1.1
|
||||||
redis>=5.0.1
|
redis>=5.0.1
|
||||||
|
|
Loading…
Reference in New Issue