new: [benchmarking suite] added

- collect metrics about the usage of MISP
  - stored in redis
  - per endpoint / user / user-agent collection
  - collection of execution time, php memory use, sql execution time, sql query count
  - the collection happens on a daily basis
- Searchable / filterable interface for the collected data
- Dashboard widget for the collected data
feature/better-logic-for-merge-attribute-into-object
iglocska 2024-04-17 15:08:38 +02:00
parent 4dd5d369b4
commit 4544ef2516
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
15 changed files with 621 additions and 12 deletions

View File

@ -18,6 +18,7 @@
App::uses('AppModel', 'Model');
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
@ -38,7 +39,18 @@ abstract class AppShell extends Shell
{
$configLoad = $this->Tasks->load('ConfigLoad');
$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();
}

View File

@ -125,7 +125,6 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Pull'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$serverId = $this->args[1];
@ -166,7 +165,7 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Push'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$serverId = $this->args[1];
@ -370,7 +369,7 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Fetch feeds as local data'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$feedId = $this->args[1];
@ -426,7 +425,7 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Cache server'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$scope = $this->args[1];
@ -489,7 +488,7 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Cache feeds for quick lookups'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$scope = $this->args[1];
@ -735,6 +734,7 @@ class ServerShell extends AppShell
public function sendPeriodicSummaryToUsers()
{
$periods = $this->__getPeriodsForToday();
$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;
@ -800,7 +800,7 @@ class ServerShell extends AppShell
if (empty($this->args[0]) || empty($this->args[1])) {
die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Push Taxii'] . PHP_EOL);
}
$userId = $this->args[0];
$user = $this->getUser($userId);
$serverId = $this->args[1];

View File

@ -40,6 +40,12 @@ class AppController extends Controller
public $phptoonew = '8.0';
private $isApiAuthed = false;
/** @var redis */
private $redis = null;
/** @var benchmark_results */
private $benchmark_results = null;
public $baseurl = '';
public $restResponsePayload = null;
@ -57,9 +63,14 @@ class AppController extends Controller
/** @var ACLComponent */
public $ACL;
/** @var BenchmarkComponent */
public $Benchmark;
/** @var RestResponseComponent */
public $RestResponse;
public $start_time;
public function __construct($request = null, $response = null)
{
parent::__construct($request, $response);
@ -97,6 +108,12 @@ class AppController extends Controller
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'];
$action = $this->request->params['action'];
@ -147,8 +164,6 @@ class AppController extends Controller
Configure::write('Config.language', 'eng');
}
$this->User = ClassRegistry::init('User');
if (!empty($this->request->params['named']['disable_background_processing'])) {
Configure::write('MISP.background_jobs', 0);
}
@ -863,6 +878,21 @@ class AppController extends Controller
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')) {
$this->Session->destroy();
}

View File

@ -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);
}
}

View File

@ -95,6 +95,9 @@ class ACLComponent extends Component
'index' => ['perm_auth'],
'view' => ['perm_auth'],
],
'benchmarks' => [
'index' => []
],
'cerebrates' => [
'add' => [],
'delete' => [],

View File

@ -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;
}
}

View File

@ -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
app/Model/Benchmark.php Normal file
View File

@ -0,0 +1,6 @@
<?php
App::uses('AppModel', 'Model');
class Benchmark extends AppModel
{
}

View File

@ -7536,6 +7536,13 @@ class Server extends AppModel
'test' => 'testBool',
'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(
'level' => 0,
'description' => __('Enable/disable the enrichment services'),

View File

@ -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,
]
]
]);
?>

View File

@ -6,6 +6,7 @@
h($data['formula'])
);
}
$y_axis = $data['y-axis'] ?? 'Count';
?>
<div id="chartContainer-<?= $seed ?>" style="flex-grow: 1; position:relative;"></div>
<script>
@ -50,7 +51,7 @@ function init<?= $seed ?>() { // variables and functions have their own scope (n
show_legend: true,
style: {
xlabel: "Date",
ylabel: "Count",
ylabel: "<?= h($y_axis) ?>",
hideXAxis: false,
hideYAxis: false,
},

View File

@ -35,7 +35,7 @@
if (!empty($data['persistUrlParams'])) {
foreach ($data['persistUrlParams'] as $persistedParam) {
if (!empty($passedArgsArray[$persistedParam])) {
$data['paginatorOptions']['url'][] = $passedArgsArray[$persistedParam];
$data['paginatorOptions']['url'][$persistedParam] = $passedArgsArray[$persistedParam];
}
}
}

View File

@ -1100,6 +1100,13 @@ $divider = '<li class="divider"></li>';
'url' => $baseurl . '/servers/updateProgress',
'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;
if (Configure::read('MISP.background_jobs')) {
echo $this->element('/genericElements/SideMenu/side_menu_link', array(

View File

@ -408,6 +408,11 @@ if (!empty($me)) {
'url' => $baseurl . '/servers/serverSettings',
'requirement' => $isSiteAdmin
),
[
'text' => __('Benchmarking'),
'url' => $baseurl . '/benchmarks/index',
'requirement' => $isSiteAdmin && Configure::read('Plugin.Benchmarking_enable')
],
array(
'type' => 'separator',
'requirement' => $isSiteAdmin

View File

@ -177,7 +177,7 @@ if ($isAdmin && $isTotp) {
'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph')
));
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(
'<h2>%s</h2>%s',
__('User %s', h($user['User']['email'])),
@ -210,6 +210,15 @@ if ($isAdmin && $isTotp) {
__('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_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']))))
);
$current_menu = [