Merge branch 'feature-periodic-notification' into feature-workflow-improvement1

pull/8580/head
Sami Mokaddem 2022-09-09 14:06:41 +02:00
commit f1ab26a75a
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
35 changed files with 2033 additions and 121 deletions

View File

@ -578,6 +578,37 @@ class ServerShell extends AppShell
$this->Task->saveField('message', count($servers) . ' job(s) completed at ' . date('d/m/Y - H:i:s') . '.');
}
public function sendPeriodicSummaryToUsers()
{
$this->ConfigLoad->execute();
$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;
foreach ($periods as $period) {
$users = $this->User->getSubscribedUsersForPeriod($period);
echo __n('%s user has subscribed for the `%s` period', '%s users has subscribed for the `%s` period', count($users), count($users), $period) . PHP_EOL;
foreach ($users as $user) {
echo __('Sending `%s` report to `%s`', $period, $user['User']['email']) . PHP_EOL;
$emailTemplate = $this->User->generatePeriodicSummary($user['User']['id'], $period, false);
$this->User->sendEmail($user, $emailTemplate, false, null);
}
}
echo __('All reports sent. Task took %s secondes', time() - $start_time) . PHP_EOL;
}
private function __getPeriodsForToday(): array
{
$today = new DateTime();
$periods = ['daily'];
if ($today->format('j') == 1) {
$periods[] = 'weekly';
}
if ($today->format('N') == 1) {
$periods[] = 'monthly';
}
return $periods;
}
/**
* @param int $userId
* @return array

View File

@ -729,6 +729,7 @@ class ACLComponent extends Component
'initiatePasswordReset' => ['AND' => ['perm_admin', 'password_change_enabled']],
'login' => array('*'),
'logout' => array('*'),
'notificationSettings' => ['*'],
'register' => array('*'),
'registrations' => array(),
'resetAllSyncAuthKeys' => array(),
@ -743,6 +744,7 @@ class ACLComponent extends Component
'verifyCertificate' => array(),
'verifyGPG' => array(),
'view' => array('*'),
'viewPeriodicSummary' => ['*'],
'getGpgPublicKey' => array('*'),
'unsubscribe' => ['*'],
),

View File

@ -88,7 +88,7 @@ class RestResponseComponent extends Component
'restSearch' => array(
'description' => "Search MISP using a list of filter parameters and return the data in the selected format. The search is available on an event and an attribute level, just select the scope via the URL (/events/restSearch vs /attributes/restSearch). Besides the parameters listed, other, format specific ones can be passed along (for example: requested_attributes and includeContext for the CSV export). This API allows pagination via the page and limit parameters.",
'mandatory' => array('returnFormat'),
'optional' => array('page', 'limit', 'value', 'type', 'category', 'org', 'tag', 'tags', 'searchall', 'date', 'last', 'eventid', 'withAttachments', 'metadata', 'uuid', 'published', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'sgReferenceOnly', 'eventinfo', 'sharinggroup', 'excludeLocalTags', 'threat_level_id'),
'optional' => array('page', 'limit', 'value', 'type', 'category', 'org', 'tag', 'tags', 'event_tags', 'searchall', 'date', 'last', 'eventid', 'withAttachments', 'metadata', 'uuid', 'published', 'publish_timestamp', 'timestamp', 'enforceWarninglist', 'sgReferenceOnly', 'eventinfo', 'sharinggroup', 'excludeLocalTags', 'threat_level_id'),
'params' => array()
),
'addTag' => array(

View File

@ -74,6 +74,7 @@ class UsersController extends AppController
} else {
$this->set('user', $user);
$this->set('admin_view', false);
$this->set('periodic_notifications', $this->User::PERIODIC_NOTIFICATIONS);
}
}
@ -469,6 +470,7 @@ class UsersController extends AppController
} else {
$this->set('urlparams', $urlParams);
$this->set('passedArgsArray', $passedArgsArray);
$this->set('periodic_notifications', $this->User::PERIODIC_NOTIFICATIONS);
$conditions = array();
if ($this->_isSiteAdmin()) {
$users = $this->paginate();
@ -609,6 +611,7 @@ class UsersController extends AppController
$user2 = $this->User->find('first', array('conditions' => array('User.id' => $user['User']['invited_by']), 'recursive' => -1));
$this->set('id', $id);
$this->set('user2', $user2);
$this->set('periodic_notifications', $this->User::PERIODIC_NOTIFICATIONS);
$this->set('admin_view', true);
$this->render('view');
}
@ -2761,6 +2764,59 @@ class UsersController extends AppController
}
}
public function notificationSettings()
{
$user_id = $this->Auth->user('id');
$user = $this->User->find('first', [
'recursive' => -1,
'conditions' => ['User.id' => $user_id],
'contain' => [
'UserSetting',
]
]);
if (empty($user)) {
throw new NotFoundException(__('Invalid user'));
}
if ($this->request->is('post') || $this->request->is('put')) {
$success = $this->User->saveNotificationSettings($user_id, $this->request->data);
if (!empty($success)) {
$message = __('Notification settings saved');
$this->Flash->success($message);
$this->redirect(['action' => 'view', 'me']);
} else {
$message = __('Notification settings could not be saved');
$this->Flash->error($message);
}
}
$user['periodic_settings'] = $this->User->extractPeriodicSettingForUser($user);
$this->request->data = $user;
$this->set('user', $user);
$this->loadModel('Attribute');
$distributionData = $this->Attribute->fetchDistributionData($this->Auth->user());
unset($distributionData['levels'][5]);
$this->set('sharingGroups', $distributionData['sgs']);
$this->set('distributionLevels', $distributionData['levels']);
$this->loadModel('Organisation');
$orgs = $this->Organisation->find('list', [
'conditions' => ['local' => 1],
'fields' => ['id', 'name'],
'order' => 'name',
]);
$this->set('orgs', $orgs);
$this->set('user', $user);
}
public function viewPeriodicSummary(string $period)
{
$summary = $this->User->generatePeriodicSummary($this->Auth->user('id'), $period);
$periodic_settings = $this->User->extractPeriodicSettingForUser($this->Auth->user('id'));
$notification_settings = $this->User->getUsablePeriodicSettingForUser($periodic_settings, $period);
$this->set('periodic_settings', $periodic_settings);
$this->set('summary', $summary);
$this->set('period', $period);
}
private function __canChangePassword()
{
return $this->ACL->canUserAccess($this->Auth->user(), 'users', 'change_pw');

View File

@ -22,6 +22,8 @@ class ContextExport
private $__taxonomyFetched = [];
private $__galaxyFetched = [];
private $__passedOptions = [];
public $non_restrictive_export = true;
public $renderView = 'context_view';
@ -33,7 +35,6 @@ class ContextExport
$this->__aggregate($attribute, Hash::extract($attribute, 'AttributeTag.{n}.Tag'));
}
}
$this->__attack_export_tool->handler($data, $options);
return '';
}
@ -45,6 +46,7 @@ class ContextExport
App::uses('AttackExport', 'Export');
$this->__attack_export_tool = new AttackExport();
$this->__attack_export_tool->handler($options);
$this->__passedOptions = $options;
return '';
}
@ -55,6 +57,9 @@ class ContextExport
$this->__aggregateTagsPerTaxonomy();
$this->__aggregateClustersPerGalaxy();
$attackData = json_decode($attackFinal, true);
if (!empty($this->__passedOptions['filters']['staticHtml'])) {
$attackData['static'] = true;
}
return json_encode([
'attackData' => $attackData,
'tags' => $this->__aggregatedTags,

View File

@ -0,0 +1,133 @@
<?php
class TrendingTool
{
private $eventModel;
public function __construct($eventModel)
{
$this->eventModel = $eventModel;
}
public function getTrendsForTags(array $events, int $baseDayRange, int $rollingWindows=3, $tagFilterPrefixes=null): array
{
$clusteredTags = $this->__clusterTagsForRollingWindow($events, $baseDayRange, $rollingWindows, $tagFilterPrefixes);
$trendAnalysis = $this->__computeTrendAnalysis($clusteredTags);
return [
'clustered_tags' => $clusteredTags,
'trend_analysis' => $trendAnalysis,
];
}
private function __computeTrendAnalysis(array $clusteredTags): array
{
$tagsPerRollingWindow = $clusteredTags['tagsPerRollingWindow'];
$eventNumberPerRollingWindow = $clusteredTags['eventNumberPerRollingWindow'];
$trendAnalysis = [];
$allTimestamps = array_keys($tagsPerRollingWindow);
$allTags = [];
foreach ($allTimestamps as $i => $timestamp) {
$trendAnalysis[$timestamp] = [];
$tags = $tagsPerRollingWindow[$timestamp];
$nextTimestamp = isset($allTimestamps[$i + 1]) ? $allTimestamps[$i + 1] : false;
$previousTimestamp = isset($allTimestamps[$i - 1]) ? $allTimestamps[$i - 1] : false;
foreach ($tags as $tag => $amount) {
$rawChange = 0;
$percentChange = 0;
if (!empty($nextTimestamp)) {
$nextAmount = !empty($tagsPerRollingWindow[$nextTimestamp][$tag]) ? $tagsPerRollingWindow[$nextTimestamp][$tag] : 0;
$rawChange = $amount - $nextAmount;
$percentChange = 100 * $rawChange / $amount;
}
$allTags[$tag] = true;
$trendAnalysis[$timestamp][$tag] = [
'occurence' => round($amount / $eventNumberPerRollingWindow[$timestamp], 2),
'raw_change' => $rawChange,
'percent_change' => $percentChange,
'change_sign' => $rawChange > 0 ? 1 : ($rawChange < 0 ? -1 : 0),
];
}
if (!empty($previousTimestamp)) {
foreach (array_keys($trendAnalysis[$timestamp]) as $tag) {
if (empty($trendAnalysis[$previousTimestamp][$tag])) {
$trendAnalysis[$previousTimestamp][$tag] = [
'occurence' => 0,
'raw_change' => -$amount,
'percent_change' => 100 * (-$amount / $amount),
'change_sign' => -$amount > 0 ? 1 : (-$amount < 0 ? -1 : 0),
];
}
}
}
}
return $trendAnalysis;
}
private function __clusterTagsForRollingWindow(array $events, int $baseDayRange, int $rollingWindows = 3, $tagFilterPrefixes = null): array
{
$fullDayNumber = $baseDayRange + $baseDayRange * $rollingWindows;
$tagsPerRollingWindow = [];
$eventNumberPerRollingWindow = [];
$timestampRollingWindow = [];
for ($i = 0; $i <= $fullDayNumber; $i += $baseDayRange) {
$timestamp = $this->eventModel->resolveTimeDelta($i . 'd');
$timestampRollingWindow[] = $timestamp;
$tagsPerRollingWindow[$timestamp] = [];
}
$tagsPerRollingWindow = array_map(function () {
return [];
}, array_flip(array_slice($timestampRollingWindow, 1)));
$eventNumberPerRollingWindow = array_map(function () {
return 0;
}, array_flip(array_slice($timestampRollingWindow, 1)));
$allTagsPerPrefix = [];
foreach ($events as $event) {
$allTags = $this->eventModel->extractAllTagNames($event);
$rollingTimestamps = $this->__getTimestampFromRollingWindow($event['Event']['timestamp'], $timestampRollingWindow);
$filteredTags = array_filter($allTags, function ($tag) use ($tagFilterPrefixes, &$allTagsPerPrefix) {
if (is_null($tagFilterPrefixes)) {
return true;
} else {
foreach ($tagFilterPrefixes as $tagPrefix) {
if (substr($tag, 0, strlen($tagPrefix)) === $tagPrefix) {
$allTagsPerPrefix[$tagPrefix][$tag] = true;
return true;
}
}
return false;
}
});
foreach ($filteredTags as $tag) {
if (empty($tagsPerRollingWindow[$rollingTimestamps['current']][$tag])) {
$tagsPerRollingWindow[$rollingTimestamps['current']][$tag] = 0;
}
$tagsPerRollingWindow[$rollingTimestamps['current']][$tag] += 1;
}
$eventNumberPerRollingWindow[$rollingTimestamps['current']] += 1;
}
return [
'tagsPerRollingWindow' => $tagsPerRollingWindow,
'eventNumberPerRollingWindow' => $eventNumberPerRollingWindow,
'allTagsPerPrefix' => array_map(function ($clusteredTags) {
return array_keys($clusteredTags);
}, $allTagsPerPrefix),
];
}
private function __getTimestampFromRollingWindow(int $eventTimestamp, array $rollingWindow): array
{
$i = 0;
if (count($rollingWindow) > 2) {
for ($i = 0; $i < count($rollingWindow) - 1; $i++) {
if ($eventTimestamp >= $rollingWindow[$i]) {
break;
}
}
}
return [
'previous' => isset($rollingWindow[$i - 1]) ? $rollingWindow[$i - 1] : null,
'current' => $rollingWindow[$i],
'next' => isset($rollingWindow[$i + 1]) ? $rollingWindow[$i + 1] : null,
];
}
}

View File

@ -83,7 +83,7 @@ class AppModel extends Model
75 => false, 76 => true, 77 => false, 78 => false, 79 => false, 80 => false,
81 => false, 82 => false, 83 => false, 84 => false, 85 => false, 86 => false,
87 => false, 88 => false, 89 => false, 90 => false, 91 => false, 92 => false,
93 => false, 94 => false, 95 => false,
93 => false, 94 => false, 95 => true, 96 => false, 97 => true,
);
const ADVANCED_UPDATES_DESCRIPTION = array(
@ -265,6 +265,10 @@ class AppModel extends Model
}
$dbUpdateSuccess = true;
break;
case 96:
$this->removeDuplicatedUUIDs();
$dbUpdateSuccess = $this->updateDatabase('createUUIDsConstraints');
break;
default:
$dbUpdateSuccess = $this->updateDatabase($command);
break;
@ -1862,6 +1866,13 @@ class AppModel extends Model
case 95:
$sqlArray[] = "ALTER TABLE `servers` ADD `remove_missing_tags` tinyint(1) NOT NULL DEFAULT 0 AFTER `skip_proxy`;";
break;
case 97:
$sqlArray[] = "ALTER TABLE `users`
ADD COLUMN `notification_daily` tinyint(1) NOT NULL DEFAULT 0,
ADD COLUMN `notification_weekly` tinyint(1) NOT NULL DEFAULT 0,
ADD COLUMN `notification_monthly` tinyint(1) NOT NULL DEFAULT 0
;";
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
@ -1930,6 +1941,15 @@ class AppModel extends Model
$indexArray[] = array('shadow_attributes', 'first_seen');
$indexArray[] = array('shadow_attributes', 'last_seen');
break;
case 'createUUIDsConstraints':
$tables_to_check = ['events', 'attributes', 'objects', 'sightings', 'dashboards', 'inbox', 'organisations', 'tag_collections'];
foreach ($tables_to_check as $table) {
if (!$this->__checkIndexExists($table, 'uuid', true)) {
$this->__dropIndex($table, 'uuid');
$this->__addIndex($table, 'uuid', null, true);
}
}
break;
default:
return false;
}
@ -2180,6 +2200,18 @@ class AppModel extends Model
return $additionResult;
}
private function __checkIndexExists($table, $column_name, $is_unique = false): bool
{
$query = sprintf(
'SHOW INDEX FROM %s WHERE Column_name = \'%s\' and Non_unique = %s;',
$table,
$column_name,
!empty($is_unique) ? '0' : '1'
);
$existing_index = $this->query($query);
return !empty($existing_index);
}
public function cleanCacheFiles()
{
Cache::clear();
@ -2704,6 +2736,143 @@ class AppModel extends Model
return true;
}
public function removeDuplicatedUUIDs()
{
$removedResults = array(
'Event' => $this->removeDuplicateEventUUIDs(),
'Attribute' => $this->removeDuplicateAttributeUUIDs(),
'Object' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('MispObject'), 'timestamp'),
'Sighting' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('Sighting'), 'date_sighting'),
'Dashboard' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('Dashboard'), 'timestamp'),
'Inbox' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('Inbox'), 'timestamp'),
'TagCollection' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('TagCollection')),
// 'GalaxyCluster' => $this->__removeDuplicateUUIDsGeneric(ClassRegistry::init('GalaxyCluster')),
);
$this->Log->createLogEntry('SYSTEM', 'update_database', 'Server', 0, __('Removed duplicated UUIDs'), __('Event: %s, Attribute: %s, Object: %s, Sighting: %s, Dashboard: %s, Inbox: %s, TagCollection: %s', h($removedResults['Event']), h($removedResults['Attribute']), h($removedResults['Object']), h($removedResults['Sighting']), h($removedResults['Dashboard']), h($removedResults['Inbox']), h($removedResults['TagCollection'])));
}
private function __removeDuplicateUUIDsGeneric($model, $sort_by=false): int
{
$className = get_class($model);
$alias = $model->alias;
$this->Log = ClassRegistry::init('Log');
$duplicates = $model->find('all', array(
'fields' => array('uuid', 'count(uuid) as occurrence'),
'recursive' => -1,
'group' => array('uuid HAVING occurrence > 1'),
));
$counter = 0;
foreach ($duplicates as $duplicate) {
$options = [
'recursive' => -1,
'conditions' => array('uuid' => $duplicate[$alias]['uuid']),
];
if (!empty($sort_by)) {
$options['order'] = "$sort_by DESC";
}
$fetched_duplicates = $model->find('all', $options);
unset($fetched_duplicates[0]);
foreach ($fetched_duplicates as $fetched_duplicate) {
$model->delete($fetched_duplicate[$alias]['id']);
$this->Log->createLogEntry('SYSTEM', 'delete', $className, $fetched_duplicate[$alias]['id'], __('Removed %s (%s)', $className, $fetched_duplicate[$alias]['id']), __('%s\'s UUID duplicated (%s)', $className, $fetched_duplicate[$alias]['uuid']));
$counter++;
}
}
return $counter;
}
public function removeDuplicateAttributeUUIDs()
{
$this->Attribute = ClassRegistry::init('Attribute');
$this->Log = ClassRegistry::init('Log');
$duplicates = $this->Attribute->find('all', array(
'fields' => array('Attribute.uuid', 'count(Attribute.uuid) as occurrence'),
'recursive' => -1,
'group' => array('Attribute.uuid HAVING occurrence > 1'),
'order' => false,
));
$counter = 0;
foreach ($duplicates as $duplicate) {
$attributes = $this->Attribute->find('all', array(
'recursive' => -1,
'conditions' => array('uuid' => $duplicate['Attribute']['uuid']),
'contain' => array(
'AttributeTag' => array(
'fields' => array('tag_id')
)
),
'order' => 'timestamp DESC',
));
$tagIDsOfFirstAttribute = Hash::extract($attributes[0]['AttributeTag'], '{n}.tag_id');
$eventIDOfFirstAttribute = $attributes[0]['Attribute']['event_id'];
unset($attributes[0]);
foreach ($attributes as $attribute) {
$tagIDs = Hash::extract($attribute['AttributeTag'], '{n}.tag_id');
$logTag = false;
$logEventID = false;
if (empty(array_diff($tagIDs, $tagIDsOfFirstAttribute))) {
$logTag = true;
}
if ($eventIDOfFirstAttribute != $attribute['Attribute']['event_id']) {
$logEventID = true;
}
$success = $this->Attribute->delete($attribute['Attribute']['id']);
if (empty($success)) {
$this->Log->createLogEntry('SYSTEM', 'delete', 'Attribute', $attribute['Attribute']['id'], __('Could not remove attribute (%s)', $attribute['Attribute']['id']), __('Deletion was rejected.'));
continue;
}
$logMessage = __('Attribute\'s UUID duplicated (%s).', $attribute['Attribute']['uuid']);
if ($logEventID) {
$logMessage .= __(' Was part of another event_id (%s) than the one that was kept (%s).', $attribute['Attribute']['event_id'], $eventIDOfFirstAttribute);
} else if ($logTag) {
$logMessage .= __(' Tag IDs attached [%s]', implode($tagIDs));
} else {
}
$this->Log->createLogEntry('SYSTEM', 'delete', 'Attribute', $attribute['Attribute']['id'], __('Removed attribute (%s)', $attribute['Attribute']['id']), $logMessage);
$counter++;
}
}
return $counter;
}
public function removeDuplicateEventUUIDs()
{
$this->Event = ClassRegistry::init('Event');
$this->Log = ClassRegistry::init('Log');
$duplicates = $this->Event->find('all', array(
'fields' => array('Event.uuid', 'count(Event.uuid) as occurrence'),
'recursive' => -1,
'group' => array('Event.uuid HAVING occurrence > 1'),
));
$counter = 0;
// load this so we can remove the blocklist item that will be created, this is the one case when we do not want it.
if (Configure::read('MISP.enableEventBlocklisting') !== false) {
$this->EventBlocklist = ClassRegistry::init('EventBlocklist');
}
foreach ($duplicates as $duplicate) {
$events = $this->Event->find('all', array(
'recursive' => -1,
'conditions' => array('uuid' => $duplicate['Event']['uuid']),
'order' => 'timestamp DESC',
));
unset($events[0]);
foreach ($events as $event) {
$uuid = $event['Event']['uuid'];
$this->Event->delete($event['Event']['id']);
$this->Log->createLogEntry('SYSTEM', 'delete', 'Event', $event['Event']['id'], __('Removed event (%s)', $event['Event']['id']), __('Event\'s UUID duplicated (%s)', $event['Event']['uuid']));
$counter++;
// remove the blocklist entry that we just created with the event deletion, if the feature is enabled
// We do not want to block the UUID, since we just deleted a copy
if (Configure::read('MISP.enableEventBlocklisting') !== false) {
$this->EventBlocklist->deleteAll(array('EventBlocklist.event_uuid' => $uuid));
}
}
}
return $counter;
}
public function checkFilename($filename)
{
return preg_match('@^([a-z0-9_.]+[a-z0-9_.\- ]*[a-z0-9_.\-]|[a-z0-9_.])+$@i', $filename);

View File

@ -1177,14 +1177,15 @@ class Attribute extends AppModel
public function set_filter_tags(&$params, $conditions, $options)
{
if (empty($params['tags'])) {
if (empty($params['tags']) && empty($params['event_tags'])) {
return $conditions;
}
/** @var Tag $tag */
$tag = ClassRegistry::init('Tag');
$params['tags'] = $this->dissectArgs($params['tags']);
$tag_key = !empty($params['tags']) ? 'tags' : 'event_tags';
$params[$tag_key] = $this->dissectArgs($params[$tag_key]);
foreach (array(0, 1, 2) as $tag_operator) {
$tagArray[$tag_operator] = $tag->fetchTagIdsSimple($params['tags'][$tag_operator]);
$tagArray[$tag_operator] = $tag->fetchTagIdsSimple($params[$tag_key][$tag_operator]);
}
$temp = array();
if (!empty($tagArray[0])) {
@ -1204,19 +1205,21 @@ class Attribute extends AppModel
$temp,
$this->subQueryGenerator($tag->EventTag, $subquery_options, $lookup_field)
);
$subquery_options = array(
'conditions' => array(
'tag_id' => $tagArray[0]
),
'fields' => array(
$options['scope'] === 'Event' ? 'Event.id' : 'attribute_id'
)
);
$lookup_field = $options['scope'] === 'Event' ? 'Event.id' : 'Attribute.id';
$temp = array_merge(
$temp,
$this->subQueryGenerator($tag->AttributeTag, $subquery_options, $lookup_field)
);
if ($tag_key != 'event_tags') {
$subquery_options = array(
'conditions' => array(
'tag_id' => $tagArray[0]
),
'fields' => array(
$options['scope'] === 'Event' ? 'Event.id' : 'attribute_id'
)
);
$lookup_field = $options['scope'] === 'Event' ? 'Event.id' : 'Attribute.id';
$temp = array_merge(
$temp,
$this->subQueryGenerator($tag->AttributeTag, $subquery_options, $lookup_field)
);
}
}
}
if (!empty($temp)) {
@ -1269,37 +1272,39 @@ class Attribute extends AppModel
$temp[$k]['OR'],
$this->subQueryGenerator($tag->EventTag, $subquery_options, $lookup_field)
);
$subquery_options = array(
'conditions' => array(
'tag_id' => $anded_tag
),
'fields' => array(
$options['scope'] === 'Event' ? 'Event.id' : 'attribute_id'
)
);
$lookup_field = $options['scope'] === 'Event' ? 'Event.id' : 'Attribute.id';
$temp[$k]['OR'] = array_merge(
$temp[$k]['OR'],
$this->subQueryGenerator($tag->AttributeTag, $subquery_options, $lookup_field)
);
if ($tag_key != 'event_tags') {
$subquery_options = array(
'conditions' => array(
'tag_id' => $anded_tag
),
'fields' => array(
$options['scope'] === 'Event' ? 'Event.id' : 'attribute_id'
)
);
$lookup_field = $options['scope'] === 'Event' ? 'Event.id' : 'Attribute.id';
$temp[$k]['OR'] = array_merge(
$temp[$k]['OR'],
$this->subQueryGenerator($tag->AttributeTag, $subquery_options, $lookup_field)
);
}
}
}
}
if (!empty($temp)) {
$conditions['AND'][] = array('AND' => $temp);
}
$params['tags'] = array();
$params[$tag_key] = array();
if (!empty($tagArray[0]) && empty($options['pop'])) {
$params['tags']['OR'] = $tagArray[0];
$params[$tag_key]['OR'] = $tagArray[0];
}
if (!empty($tagArray[1])) {
$params['tags']['NOT'] = $tagArray[1];
$params[$tag_key]['NOT'] = $tagArray[1];
}
if (!empty($tagArray[2]) && empty($options['pop'])) {
$params['tags']['AND'] = $tagArray[2];
$params[$tag_key]['AND'] = $tagArray[2];
}
if (empty($params['tags'])) {
unset($params['tags']);
if (empty($params[$tag_key])) {
unset($params[$tag_key]);
}
return $conditions;
}

View File

@ -638,6 +638,36 @@ class DecayingModel extends AppModel
return $attribute;
}
public function attachBaseScoresToEvent($user, $event, $model_id=false, $model_overrides=array(), $include_full_model=0)
{
$models = [];
if ($model_id === false) { // fetch all allowed and associated models
$models = $this->fetchAllAllowedModels($user, false, [], ['DecayingModel.enabled' => true]);
} else {
$models = $this->fetchModels($user, $model_id, false, array());
}
foreach ($models as $model) {
if (!empty($model_overrides)) {
$model = $this->overrideModelParameters($model, $model_overrides);
}
$basescore = $this->getBaseScoreForEvent($event, $model, $user);
$decayed = $this->isBaseScoreDecayed($model, $basescore);
$to_attach = [
'base_score' => $basescore,
'decayed' => $decayed,
'DecayingModel' => [
'id' => $model['DecayingModel']['id'],
'name' => $model['DecayingModel']['name']
]
];
if ($include_full_model) {
$to_attach['DecayingModel'] = $model['DecayingModel'];
}
$event['event_base_score'][] = $to_attach;
}
return $event;
}
public function getScore($attribute, $model, $user=false)
{
if (is_numeric($attribute) && $user !== false) {
@ -657,6 +687,18 @@ class DecayingModel extends AppModel
return $this->Computation->computeCurrentScore($user, $model, $attribute);
}
public function getBaseScoreForEvent(array $event, array $model): float
{
$this->Computation = $this->getModelClass($model);
return $this->Computation->computeBasescore($model, $event)['base_score'];
}
public function isBaseScoreDecayed(array $model, float $basescore): bool
{
$threshold = $model['DecayingModel']['parameters']['threshold'];
return $threshold > $basescore;
}
public function isDecayed($attribute, $model, $score=false, $user=false)
{
if ($score === false) {

View File

@ -1370,6 +1370,7 @@ class Event extends AppModel
'eventinfo' => array('function' => 'set_filter_eventinfo'),
'ignore' => array('function' => 'set_filter_ignore'),
'tags' => array('function' => 'set_filter_tags'),
'event_tags' => array('function' => 'set_filter_tags', 'pop' => true),
'from' => array('function' => 'set_filter_timestamp', 'pop' => true),
'to' => array('function' => 'set_filter_timestamp', 'pop' => true),
'date' => array('function' => 'set_filter_date', 'pop' => true),
@ -1620,6 +1621,7 @@ class Event extends AppModel
'includeRelatedTags',
'excludeLocalTags',
'includeDecayScore',
'includeBaseScoresOnEvent',
'includeSightingdb',
'includeFeedCorrelations',
'includeServerCorrelations',
@ -1629,7 +1631,8 @@ class Event extends AppModel
'limit',
'page',
'order',
'protected'
'protected',
'published',
);
if (!isset($options['excludeLocalTags']) && !empty($user['Role']['perm_sync']) && empty($user['Role']['perm_site_admin'])) {
$options['excludeLocalTags'] = 1;
@ -1766,6 +1769,9 @@ class Event extends AppModel
if ($options['protected']) {
$conditions['AND'][] = array('Event.protected' => $options['protected']);
}
if ($options['published']) {
$conditions['AND'][] = array('Event.published' => $options['published']);
}
if (!empty($options['includeRelatedTags'])) {
$options['includeGranularCorrelations'] = 1;
}
@ -1924,7 +1930,7 @@ class Event extends AppModel
$sharingGroupData = $sharingGroupReferenceOnly ? [] : $this->__cacheSharingGroupData($user, $useCache);
// Initialize classes that will be necessary during event fetching
if (!empty($options['includeDecayScore']) && !isset($this->DecayingModel)) {
if ((!empty($options['includeDecayScore']) || !empty($options['includeBaseScoresOnEvent'])) && !isset($this->DecayingModel)) {
$this->DecayingModel = ClassRegistry::init('DecayingModel');
}
if ($options['includeServerCorrelations'] && !$isSiteAdmin && $user['org_id'] != Configure::read('MISP.host_org_id')) {
@ -2021,6 +2027,9 @@ class Event extends AppModel
}
//$event['RelatedShadowAttribute'] = $this->getRelatedAttributes($user, $event['Event']['id'], true);
}
if (!empty($options['includeBaseScoresOnEvent'])) {
$event = $this->DecayingModel->attachBaseScoresToEvent($user, $event);
}
$shadowAttributeByOldId = [];
if (!empty($event['ShadowAttribute'])) {
if ($isSiteAdmin && $options['includeFeedCorrelations']) {
@ -2689,7 +2698,7 @@ class Event extends AppModel
public function set_filter_tags(&$params, $conditions, $options)
{
if (!empty($params['tags'])) {
if (!empty($params['tags']) || !empty($params['event_tags'])) {
$conditions = $this->Attribute->set_filter_tags($params, $conditions, $options);
}
return $conditions;
@ -7623,4 +7632,39 @@ class Event extends AppModel
$kafkaPubTool->publishJson($kafkaTopic, $fullEvent[0], 'publish');
}
}
public function getTrendsForTags(array $user, array $eventFilters=[], int $baseDayRange, int $rollingWindows=3, $tagFilterPrefixes=null): array
{
$fullDayNumber = $baseDayRange + $baseDayRange * $rollingWindows;
$fullRange = $this->resolveTimeDelta($fullDayNumber . 'd');
$eventFilters['last'] = $fullRange . 'd';
$eventFilters['order'] = 'timestamp DESC';
$events = $this->fetchEvent($user, $eventFilters);
App::uses('TrendingTool', 'Tools');
$trendingTool = new TrendingTool($this);
$trendAnalysis = $trendingTool->getTrendsForTags($events, $baseDayRange, $rollingWindows, $tagFilterPrefixes);
$clusteredTags = $trendAnalysis['clustered_tags'];
$trendAnalysis = $trendAnalysis['trend_analysis'];
return [
'clustered_tags' => $trendAnalysis,
'clustered_events' => $clusteredTags['eventNumberPerRollingWindow'],
'all_tags' => $clusteredTags['allTagsPerPrefix'],
'all_timestamps' => array_keys($clusteredTags['eventNumberPerRollingWindow']),
];
}
public function getTrendsForTagsFromEvents(array $events, int $baseDayRange, int $rollingWindows=3, $tagFilterPrefixes=null): array
{
App::uses('TrendingTool', 'Tools');
$trendingTool = new TrendingTool($this);
$trendAnalysis = $trendingTool->getTrendsForTags($events, $baseDayRange, $rollingWindows, $tagFilterPrefixes);
$clusteredTags = $trendAnalysis['clustered_tags'];
$trendAnalysis = $trendAnalysis['trend_analysis'];
return [
'clustered_tags' => $trendAnalysis,
'clustered_events' => $clusteredTags['eventNumberPerRollingWindow'],
'all_tags' => $clusteredTags['allTagsPerPrefix'],
'all_timestamps' => array_keys($clusteredTags['eventNumberPerRollingWindow']),
];
}
}

View File

@ -18,6 +18,9 @@ App::uses('BlowfishConstantPasswordHasher', 'Controller/Component/Auth');
*/
class User extends AppModel
{
private const PERIODIC_USER_SETTING_KEY = 'periodic_notification_filters';
public const PERIODIC_NOTIFICATIONS = ['notification_daily', 'notification_weekly', 'notification_monthly'];
public $displayField = 'email';
public $validate = array(
@ -1645,4 +1648,244 @@ class User extends AppModel
$salt = Configure::read('Security.salt');
return substr(hash('sha256', "{$user['id']}|$salt"), 0, 8);
}
public function extractPeriodicSettingForUser($user, $decode=false): array
{
$filter_names = ['orgc_id', 'distribution', 'sharing_group_id', 'event_info', 'tags', 'trending_for_tags'];
$filter_to_decode = ['tags', 'trending_for_tags', ];
if (is_numeric($user)) {
$user = $this->find('first', [
'recursive' => -1,
'conditions' => ['User.id' => $user],
'contain' => [
'UserSetting',
]
]);
}
$periodic_settings = array_values(array_filter($user['UserSetting'], function ($userSetting) {
return $userSetting['setting'] == self::PERIODIC_USER_SETTING_KEY;
}));
$periodic_settings_indexed = [];
if (!empty($periodic_settings)) {
foreach ($filter_names as $filter_name) {
$periodic_settings_indexed[$filter_name] = $periodic_settings[0]['value'][$filter_name];
}
}
foreach ($filter_to_decode as $filter) {
if (!empty($decode) && !empty($periodic_settings_indexed[$filter])) {
$periodic_settings_indexed[$filter] = JsonTool::decode($periodic_settings_indexed[$filter]);
}
}
return $periodic_settings_indexed;
}
public function getUsablePeriodicSettingForUser(array $periodicSettings, $period='daily'): array
{
return $this->__getUsableFilters($periodicSettings, $period);
}
public function saveNotificationSettings(int $user_id, array $data): bool
{
$existingUser = $this->find('first', [
'recursive' => -1,
'conditions' => ['User.id' => $user_id],
]);
if (empty($existingUser)) {
return false;
}
foreach (self::PERIODIC_NOTIFICATIONS as $notification_period) {
$existingUser['User'][$notification_period] = $data['User'][$notification_period];
}
$success = $this->save($existingUser, [
'fieldList' => self::PERIODIC_NOTIFICATIONS
]);
if ($success) {
$periodic_settings = $data['periodic_settings'];
$param_to_decode = ['tags', 'trending_for_tags', ];
foreach ($param_to_decode as $param) {
if (empty($periodic_settings[$param])) {
$periodic_settings[$param] = '[]';
} else {
$decodedTags = json_decode($periodic_settings[$param], true);
if ($decodedTags === null) {
return false;
}
}
}
$notification_filters = [
'orgc_id' => $periodic_settings['orgc_id'] ?? [],
'distribution' => $periodic_settings['distribution'] ?? '',
'sharing_group_id' => $periodic_settings['distribution'] != 4 ? '' : ($periodic_settings['sharing_group_id'] ?? []),
'event_info' => $periodic_settings['event_info'] ?? '',
'tags' => $periodic_settings['tags'] ?? '[]',
'trending_for_tags' => $periodic_settings['trending_for_tags'] ?? '[]',
];
$new_user_setting = [
'UserSetting' => [
'user_id' => $existingUser['User']['id'],
'setting' => self::PERIODIC_USER_SETTING_KEY,
'value' => $notification_filters
]
];
$success = $this->UserSetting->setSetting($existingUser['User'], $new_user_setting);
}
return !empty($success);
}
public function getSubscribedUsersForPeriod(string $period): array
{
return $this->find('all', [
'recursive' => -1,
'conditions' => ["notification_$period" => true],
]);
}
/**
* generatePeriodicSummary
*
* @param int $user_id
* @param string $period
* @return string|SendEmailTemplate
* @throws NotFoundException
* @throws InvalidArgumentException
*/
public function generatePeriodicSummary(int $user_id, string $period, $rendered=true)
{
$existingUser = $this->getUserById($user_id);
$user = $this->rearrangeToAuthForm($existingUser);
$allowed_periods = array_map(function($period) {
return substr($period, strlen('notification_'));
}, self::PERIODIC_NOTIFICATIONS);
if (!in_array($period, $allowed_periods)) {
throw new InvalidArgumentException(__('Invalid period. Must be one of %s', JsonTool::encode(self::PERIODIC_NOTIFICATIONS)));
}
App::import('Tools', 'SendEmail');
$emailTemplate = $this->prepareEmailTemplate($period);
$periodicSettings = $this->extractPeriodicSettingForUser($user, true);
$filters = $this->getUsablePeriodicSettingForUser($periodicSettings, $period);
$filtersForRestSearch = $filters; // filters for restSearch are slightly different than fetchEvent
$filters['last'] = $this->resolveTimeDelta($filters['last']);
$filters['sgReferenceOnly'] = true;
$filters['includeEventCorrelations'] = false;
$filters['noSightings'] = true;
$filters['includeGalaxy'] = false;
$events = $this->__getEventsForFilters($user, $filters);
$elementCounter = 0;
$renderView = false;
$filtersForRestSearch['publish_timestamp'] = $filtersForRestSearch['last'];
$filtersForRestSearch['returnFormat'] = 'context';
$filtersForRestSearch['staticHtml'] = true;
unset($filtersForRestSearch['last']);
if (!empty($filtersForRestSearch['tags'])) {
$filtersForRestSearch['event_tags'] = $filtersForRestSearch['tags'];
unset($filtersForRestSearch['tags']);
}
$finalContext = $this->Event->restSearch($user, 'context', $filtersForRestSearch, false, false, $elementCounter, $renderView);
$finalContext = json_decode($finalContext->intoString(), true);
$aggregated_context = $this->__renderAggregatedContext($finalContext);
$rollingWindows = 2;
$trendAnalysis = $this->Event->getTrendsForTagsFromEvents($events, $this->__periodToDays($period), $rollingWindows, $periodicSettings['trending_for_tags']);
$trendData = [
'trendAnalysis' => $trendAnalysis,
'tagFilterPrefixes' => $periodicSettings['trending_for_tags'],
];
$trending_summary = $this->__renderTrendingSummary($trendData);
$emailTemplate->set('baseurl', $this->Event->__getAnnounceBaseurl());
$emailTemplate->set('events', $events);
$emailTemplate->set('filters', $filters);
$emailTemplate->set('period', $period);
$emailTemplate->set('aggregated_context', $aggregated_context);
$emailTemplate->set('trending_summary', $trending_summary);
$emailTemplate->set('analysisLevels', $this->Event->analysisLevels);
$emailTemplate->set('distributionLevels', $this->Event->distributionLevels);
if (!empty($rendered)) {
$summary = $emailTemplate->render();
return $summary->format() == 'text' ? $summary->text : $summary->html;
}
return $emailTemplate;
}
private function __renderAggregatedContext(array $restSearchOutput): string
{
return $this->__renderGeneric('Events' . DS . 'module_views', 'context_view', $restSearchOutput);
}
private function __renderTrendingSummary(array $trendData): string
{
return $this->__renderGeneric('Elements' . DS . 'Events', 'trendingSummary', $trendData);
}
private function __renderGeneric(string $viewPath, string $viewFile, array $viewVars): string
{
$view = new View();
$view->autoLayout = false;
$view->helpers = ['TextColour'];
$view->loadHelpers();
$view->set($viewVars);
$view->set('baseurl', $this->Event->__getAnnounceBaseurl());
$view->viewPath = $viewPath;
return $view->render($viewFile, false);
}
private function __getUsableFilters(array $period_filters, string $period='daily'): array
{
$filters = [
'last' => $this->__genTimerangeFilter($period),
'published' => true,
'includeBaseScoresOnEvent' => true,
];
if (!empty($period_filters['orgc_id'])) {
$filters['orgc_id'] = $period_filters['orgc_id'];
}
if (isset($period_filters['distribution']) && $period_filters['distribution'] >= 0) {
$filters['distribution'] = intval($period_filters['distribution']);
}
if (!empty($period_filters['sharing_group_id'])) {
$filters['sharing_group_id'] = $period_filters['sharing_group_id'];
}
if (!empty($period_filters['event_info'])) {
$filters['event_info'] = $period_filters['event_info'];
}
if (!empty($period_filters['tags'])) {
$filters['tags'] = $period_filters['tags'];
}
return $filters;
}
private function __genTimerangeFilter(string $period='daily'): string
{
$timerange = '1d';
if ($period == 'weekly') {
$timerange = '7d';
} else if ($period == 'monthly'){
$timerange = '31d';
}
return $timerange;
}
private function __periodToDays(string $period='daily'): int
{
return ($period == 'daily' ? 1 : (
$period == 'weekly' ? 7 : 31)
);
}
private function __getEventsForFilters(array $user, array $filters): array
{
$this->Event = ClassRegistry::init('Event');
$events = $this->Event->fetchEvent($user, $filters);
return $events;
}
public function prepareEmailTemplate(string $period='daily'): SendEmailTemplate
{
$subject = sprintf('[%s MISP] %s %s', Configure::read('MISP.org'), Inflector::humanize($period), __('Notification - %s', (new DateTime())->format('Y-m-d')));
$template = new SendEmailTemplate("notification_$period");
$template->subject($subject);
return $template;
}
}

View File

@ -100,6 +100,15 @@ class UserSetting extends AppModel
'oidc' => [ // Data saved by OIDC plugin
'internal' => true,
],
'periodic_notification_filters' => [
'placeholder' => [
'orgc_id' => '1',
'distribution' => '1',
'sharing_group_id' => '1',
'event_info' => 'phishing',
'tags' => '["tlp:red"]',
],
],
);
// massage the data before we send it off for validation before saving anything

View File

@ -0,0 +1,345 @@
<?php
$clusteredTags = $trendAnalysis['clustered_tags'];
$clusteredEvents = $trendAnalysis['clustered_events'];
$allTags = $trendAnalysis['all_tags'];
$allTimestamps = $trendAnalysis['all_timestamps'];
$currentPeriod = $allTimestamps[0];
$previousPeriod = $allTimestamps[1];
$previousPeriod2 = $allTimestamps[2];
$allUniqueTagsPerPeriod = array_map(function ($tags) {
return array_keys($tags);
}, $clusteredTags);
$allUniqueTags = array_unique(array_merge($allUniqueTagsPerPeriod[$currentPeriod], $allUniqueTagsPerPeriod[$previousPeriod], $allUniqueTagsPerPeriod[$previousPeriod2]));
App::uses('ColourPaletteTool', 'Tools');
$paletteTool = new ColourPaletteTool();
$COLOR_PALETTE = $paletteTool->createColourPalette(max(count($allUniqueTags), 1));
$trendIconMapping = [
1 => '▲',
-1 => '▼',
0 => '⮞',
'?' => '',
];
$trendColorMapping = [
1 => '#b94a48',
-1 => '#468847',
0 => '#3a87ad',
'?' => '#999999',
];
$now = new DateTime();
$currentPeriodDate = DateTime::createFromFormat('U', $currentPeriod);
$previousPeriodDate = DateTime::createFromFormat('U', $previousPeriod);
$previousPeriod2Date = DateTime::createFromFormat('U', $previousPeriod2);
$colorForTags = [];
$chartData = [];
$maxValue = 0;
foreach ($allUniqueTags as $i => $tag) {
$colorForTags[$tag] = $COLOR_PALETTE[$i];
$chartData[$tag] = [
$clusteredTags[$previousPeriod2][$tag]['occurence'] ?? 0,
$clusteredTags[$previousPeriod][$tag]['occurence'] ?? 0,
$clusteredTags[$currentPeriod][$tag]['occurence'] ?? 0,
];
$maxValue = max($maxValue, max($chartData[$tag]));
}
$canvasWidth = 600;
$canvasHeight = 150;
foreach (array_keys($chartData) as $tag) {
$chartData[$tag][0] = [0, $canvasHeight - ($chartData[$tag][0] / $maxValue) * $canvasHeight];
$chartData[$tag][1] = [$canvasWidth / 2, $canvasHeight - ($chartData[$tag][1] / $maxValue) * $canvasHeight];
$chartData[$tag][2] = [$canvasWidth, $canvasHeight - ($chartData[$tag][2] / $maxValue) * $canvasHeight];
}
if (!function_exists('reduceTag')) {
function reduceTag(string $tagname, int $reductionLength = 1): string
{
$re = '/^(?<namespace>[a-z0-9_-]+)(:(?<predicate>[a-z0-9_-]+)="(?<value>[^"]+)"$)?(:(?<predicate2>[a-z0-9_-]+))?/';
$matches = [];
preg_match($re, $tagname, $matches);
if (!empty($matches['predicate2'])) {
return $reductionLength == 0 ? $tagname : $matches['predicate2'];
} else if (!empty($matches['value'])) {
return $reductionLength == 0 ? $tagname : ($reductionLength == 1 ? sprintf('%s="%s"', $matches['predicate'], $matches['value']) : $matches['value']
);
} else if (!empty($matches['namespace'])) {
return $matches['namespace'];
} else {
return $tagname;
}
}
}
if (!function_exists('computeLinePositions')) {
function computeLinePositions(float $x1, float $y1, float $x2, float $y2): array
{
$x_offset = 3.5;
$y_offset = 1;
$conf = [
'left' => $x1 + $x_offset,
'top' => $y1 + $y_offset,
'width' => sqrt(pow($y2 - $y1, 2) + pow($x2 - $x1, 2)),
'angle' => atan(($y2 - $y1) / ($x2 - $x1)),
];
return $conf;
}
}
?>
<div style="display: flex; column-gap: 20px; justify-content: space-around; margin-bottom: 40px;">
<div style="display: flex; align-items: center;">
<table class="table table-condensed" style="min-width: 300px; max-width: 400px; margin: 0;">
<tbody>
<tr>
<td><?= __('Period duration') ?></td>
<td><?= __('%s days', $currentPeriodDate->diff($now)->format('%a')); ?></td>
</tr>
<tr>
<td><?= __('Starting period') ?></td>
<td><?= sprintf('%s', $currentPeriodDate->format('M d, o. (\W\e\e\k W)')); ?></td>
</tr>
<tr>
<td><?= __('Previous period') ?></td>
<td><?= sprintf('%s', $previousPeriodDate->format('M d, o. (\W\e\e\k W)')); ?></td>
</tr>
<tr>
<td><?= __('Period-2') ?></td>
<td><?= sprintf('%s', $previousPeriod2Date->format('M d, o. (\W\e\e\k W)')); ?></td>
</tr>
</tbody>
</table>
</div>
<?php if (!empty($allUniqueTags)) : ?>
<div style="padding: 0 40px;">
<div class="chart-container">
<div class="y-axis-container">
<div>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, 0, -25) ?>"><?= h($maxValue) ?></span>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, ($canvasHeight - 20)/2, 0) ?>"><?= h(round($maxValue / 2, 2)) ?></span>
<span class="y-axis-label" style="<?= sprintf('left: %spx; top: %spx; transform: translate(-100%%, %s%%)', 0, ($canvasHeight - 20), 25) ?>"><?= 0 ?></span>
</div>
</div>
<div class="canvas">
<?php foreach ($chartData as $tag => $coords) : ?>
<?php for ($i = 0; $i < 3; $i++) : ?>
<?php
$coord = $coords[$i];
$previousCoord = isset($coords[$i - 1]) ? $coords[$i - 1] : false;
?>
<span class="dot" style="<?= sprintf('left: %spx; top: %spx; background-color: %s;', $coord[0], $coord[1], $colorForTags[$tag]) ?>" title="<?= h($tag) ?>"></span>
<?php
if (!empty($previousCoord)) {
$linePosition = computeLinePositions($previousCoord[0], $previousCoord[1], $coord[0], $coord[1]);
echo sprintf(
'<span class="line" style="left: %spx; top: %spx; width: %spx; transform: rotate(%srad); background-color: %s;" title="%s"></span>',
$linePosition['left'],
$linePosition['top'],
$linePosition['width'],
$linePosition['angle'],
$colorForTags[$tag],
h($tag)
);
}
?>
<?php endfor ?>
<?php endforeach ?>
</div>
<div class="x-axis-container">
<span class="x-axis-label" style="<?= sprintf('left: %spx; top: %spx;', 0, 0) ?>"><?= __('Period-2') ?></span>
<span class="x-axis-label" style="<?= sprintf('left: %spx; top: %spx;', $canvasWidth / 2, 0) ?>"><?= __('Previous period') ?></span>
<span class="x-axis-label" style="<?= sprintf('left: %spx; top: %spx;', $canvasWidth, 0) ?>"><?= __('Starting period') ?></span>
</div>
</div>
</div>
<?php else : ?>
<p><?= __('- No tag for the selected tag namespace -') ?></p>
<?php endif; ?>
</div>
<?php if (!empty($allTags)) : ?>
<table class="table table-condensed no-border">
<thead>
<tr>
<th></th>
<th>
<span>
<div><?= __('Period-2') ?></div>
<div style="font-weight: normal;"><?= __('%s events', h($clusteredEvents[$previousPeriod2])) ?></div>
</span>
<table>
<thead style="font-size: small;">
<tr>
<td style="min-width: 20px;">#</td>
<td style="min-width: 15px;">⥮</td>
<td>%</td>
<td></td>
</tr>
</thead>
</table>
</th>
<th>
<span>
<div><?= __('Previous period') ?></div>
<div style="font-weight: normal;"><?= __('%s events', h($clusteredEvents[$previousPeriod])) ?></div>
</span>
<table>
<thead style="font-size: small;">
<tr>
<td style="min-width: 20px;">#</td>
<td style="min-width: 15px;">⥮</td>
<td>%</td>
<td></td>
</tr>
</thead>
</table>
</th>
<th>
<span>
<div><?= __('Starting period') ?></div>
<div style="font-weight: normal;"><?= __('%s events', h($clusteredEvents[$currentPeriod])) ?></div>
</span>
<table>
<thead style="font-size: small;">
<tr>
<td style="min-width: 20px;">#</td>
<td style="min-width: 15px;">⥮</td>
<td>%</td>
<td></td>
</tr>
</thead>
</table>
</th>
</tr>
</thead>
<?php foreach ($tagFilterPrefixes as $tagPrefix) : ?>
<?php
if (empty($allTags[$tagPrefix])) {
continue;
}
?>
<tbody>
<tr>
<td colspan="4">
<h4><?= __('Tag namespace: %s', sprintf('<code>%s</code>', h($tagPrefix))) ?></h4>
</td>
</tr>
<?php foreach ($allTags[$tagPrefix] as $tagName) : ?>
<tr>
<td style="padding-left: 15px;">
<span class="tag-legend" style="background-color: <?= $colorForTags[$tagName] ?>;"></span>
<code><?= h(reduceTag($tagName, count(explode(':', $tagPrefix)))) ?></code>
</td>
<td>
<table class="table-condensed no-border">
<tbody>
<tr>
<td><?= h($clusteredTags[$previousPeriod2][$tagName]['occurence'] ?? '-') ?></td>
<td><?= h($clusteredTags[$previousPeriod2][$tagName]['raw_change'] ?? '-') ?></td>
<td><?= h($clusteredTags[$previousPeriod2][$tagName]['percent_change'] ?? '-') ?>%</td>
<td style="font-size: large; color: <?= $trendColorMapping[$clusteredTags[$previousPeriod2][$tagName]['change_sign'] ?? '?'] ?>"><?= $trendIconMapping[$clusteredTags[$previousPeriod2][$tagName]['change_sign'] ?? '?'] ?></td>
</tr>
</tbody>
</table>
</td>
<td>
<table class="table-condensed no-border">
<tbody>
<tr>
<td><?= h($clusteredTags[$previousPeriod][$tagName]['occurence'] ?? '-') ?></td>
<td><?= h($clusteredTags[$previousPeriod][$tagName]['raw_change'] ?? '-') ?></td>
<td><?= h($clusteredTags[$previousPeriod][$tagName]['percent_change'] ?? '-') ?>%</td>
<td style="font-size: large; color: <?= $trendColorMapping[$clusteredTags[$previousPeriod][$tagName]['change_sign'] ?? '?'] ?>"><?= $trendIconMapping[$clusteredTags[$previousPeriod][$tagName]['change_sign'] ?? '?'] ?></td>
</tr>
</tbody>
</table>
</td>
<td>
<table class="table-condensed no-border">
<tbody>
<tr>
<td><?= h($clusteredTags[$currentPeriod][$tagName]['occurence'] ?? '-') ?></td>
<td><?= h($clusteredTags[$currentPeriod][$tagName]['raw_change'] ?? '-') ?></td>
<td><?= h($clusteredTags[$currentPeriod][$tagName]['percent_change'] ?? '-') ?>%</td>
<td style="font-size: large; color: <?= $trendColorMapping[$clusteredTags[$currentPeriod][$tagName]['change_sign'] ?? '?'] ?>"><?= $trendIconMapping[$clusteredTags[$currentPeriod][$tagName]['change_sign'] ?? '?'] ?></td>
</tr>
</tbody>
</table>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endforeach; ?>
</table>
<?php endif; ?>
<style>
.dot {
position: absolute;
height: 7px;
width: 7px;
border-radius: 50%;
}
.line {
position: absolute;
background: blue;
height: 3px;
transform-origin: left center;
box-shadow: 1px 1px 2px 0px #00000066;
}
.chart-container {
position: relative;
background-color: #dddddd33;
padding: 5px 35px 5px 45px;
border-radius: 5px;
border: 1px solid #ddd;
}
.canvas {
width: 610px;
height: 160px;
position: relative;
}
.x-axis-container {
position: relative;
height: 20px;
}
.x-axis-label {
position: absolute;
white-space: nowrap;
translate: -50%;
}
.y-axis-container {
height: 150px;
border-right: 1px solid #000;
position: absolute;
left: -5px;
top: 10px;
padding-left: inherit;
}
.y-axis-container > div {
position: relative;
height: 100%;
}
.y-axis-label {
position: absolute;
white-space: nowrap;
/* transform: translate(-100%, 0%); */
padding: 0 5px;
}
.tag-legend {
display: inline-block;
width: 10px;
height: 10px;
border: 1px solid #000;
}
</style>

View File

@ -1,8 +1,22 @@
<?php
$seed = 's-' . mt_rand();
$fieldData['type'] = 'select';
if (empty($fieldData['class'])) {
$fieldData['class'] = $seed;
} else {
$fieldData['class'] .= ' ' . $seed;
}
echo $this->Form->input($fieldData['field'], $fieldData);
if (!empty($params['description'])) {
echo sprintf('<small class="clear form-field-description apply_css_arrow">%s</small>', h($params['description']));
}
?>
<?php if (!empty($fieldData['picker'])) : ?>
<script>
var chosenOptions = <?= JsonTool::encode($fieldData['_chosenOptions'] ?? []) ?>;
$('select.<?= $seed ?>').chosen(chosenOptions)
</script>
<?php endif; ?>

View File

@ -0,0 +1,33 @@
<?php
$seed = mt_rand();
$input = $this->Form->input($fieldData['field'], [
'class' => 'tag-textarea',
'label' => $fieldData['label'] ?? __('Tag list'),
'type' => 'textarea',
'placeholder' => $fieldData['placeholder'] ?? 'tlp:red, PAP:GREEN',
'div' => 'input text input-append',
'after' => sprintf('<button type="button" class="btn" onclick="pickerTags.call(this);">%s</button>', __('Pick tags')),
]);
?>
<div class="seed-<?= $seed ?>">
<?= $input ?>
</div>
<script>
function pickerTags() {
$(this).data('popover-no-submit', true);
$(this).data('popover-callback-function', setTagsAfterSelect);
var target_id = 0;
var target_type = 'galaxyClusterRelation';
popoverPopup(this, target_id + '/' + target_type, 'tags', 'selectTaxonomy')
}
function setTagsAfterSelect(selected, additionalData) {
selectedTags = [];
selected.forEach(function(selection) {
selectedTags.push(additionalData.itemOptions[selection].tag_name);
});
$('div.seed-<?= $seed ?> textarea.tag-textarea').val(JSON.stringify(selectedTags));
}
</script>

View File

@ -377,6 +377,18 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
'text' => __('View delegation requests')
));
}
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'viewPeriodicSummary',
'url' => $baseurl . '/users/viewPeriodicSummary/daily',
'text' => __('View periodic summary')
));
if ($menuItem === 'viewPeriodicSummary' || $menuItem === 'notification_settings') {
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'notification_settings',
'url' => $baseurl . '/users/notificationSettings',
'text' => __('Periodic summary settings')
));
}
echo $divider;
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'url' => $baseurl . '/events/export',
@ -633,6 +645,11 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
'url' => $baseurl . '/user_settings/index/user_id:me',
'text' => __('My Settings')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'notification_settings',
'url' => $baseurl . '/users/notificationSettings',
'text' => __('Periodic summary settings')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'user_settings_set',
'url' => $baseurl . '/user_settings/setSetting',

View File

@ -32,15 +32,21 @@
- $scores: The score associate with either the value or the tag name (if provided)
- $removeTrailling: How much part of the name of the cell should be remove: e.g. $removeTrailling=2 => "abc def ghi", will be: "abc"
- $colours: The colour associated with the tag name (if provided)
- $static: Should the output be inert. Used for embedding in other webpages or mails
*
*
*
*/
echo $this->element('genericElements/assetLoader', [
'css' => ['attack_matrix'],
'js' => ['attack_matrix'],
]);
if (!empty($static)) {
$pickingMode = false;
}
if (empty($static)) {
echo $this->element('genericElements/assetLoader', [
'css' => ['attack_matrix'],
'js' => ['attack_matrix'],
]);
}
$clustersNamesMapping = array(); // used to map name with id for the chosen select
if (isset($interpolation) && !empty($interpolation)) {
foreach ($interpolation as $k => $colArr) {
@ -55,6 +61,8 @@ if (isset($interpolation) && !empty($interpolation)) {
$colorScale = 'black';
}
?>
<?php if (empty($static)): ?>
<div class="attack-matrix-options" style="right: initial; background: transparent;">
<ul id="attack-matrix-tabscontroller" class="nav nav-tabs" style="margin-bottom: 2px;">
<?php
@ -63,17 +71,23 @@ if (!isset($defaultTabName)) {
$defaultTabName = key($tabs); // get first key
}
if (empty($static)):
foreach($tabs as $tabName => $column):
?>
<li class="tactic <?php echo $tabName==$defaultTabName ? "active" : ""; ?>"><span href="#tabMatrix-<?php echo h($tabName); ?>" data-toggle="tab" style="padding-top: 3px; padding-bottom: 3px;"><?php echo h($tabName); ?></span></li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</div>
<?php endif; ?>
<?php if (empty($static)): ?>
<div class="attack-matrix-options matrix-div-submit submit-container">
<span class="btn btn-inverse btn-matrix-submit" role="button" tabindex="0" style="padding: 1px 5px !important;font-size: 12px !important;font-weight: bold;"><?php echo __('Submit'); ?></span>
</div>
<?php endif; ?>
<?php if (empty($static)): ?>
<div class="attack-matrix-options">
<?php if (isset($interpolation)): ?>
<span id="matrix-heatmap-legend-caret">
@ -88,8 +102,9 @@ foreach($tabs as $tabName => $column):
<?php endif; ?>
<label style="display: inline-block; margin-left: 30px;"><input type="checkbox" id="checkbox_attackMatrix_showAll" checked><i class="fa fa-filter"></i><?= __('Show all') ?></label>
</div>
<?php endif; ?>
<?php if (isset($eventId)): ?>
<?php if (isset($eventId) && empty($static)): ?>
<div class="hidden">
<?php
$url = sprintf(
@ -111,21 +126,26 @@ foreach($tabs as $tabName => $column):
<div id="matrix_container" class="fixed-table-container-inner" style="" data-picking-mode="<?php echo $pickingMode ? 'true' : 'false'; ?>">
<div class="tab-content">
<?php foreach($tabs as $tabName => $column): ?>
<?php
if (!empty($static) && $tabName != $defaultTabName) {
// We cannot hide other tabs without JS. Only releave the default one for now.
continue;
}
?>
<div class="tab-pane <?php echo $tabName==$defaultTabName ? "active" : ""; ?>" id="tabMatrix-<?php echo h($tabName); ?>">
<div class="header-background"></div>
<div class="fixed-table-container-inner" style="">
<div class="fixed-table-container-inner" style="overflow-y: auto; max-height: 670px;">
<table class="table table-condensed matrix-table">
<thead>
<thead style="background-color: #363636;">
<tr>
<?php
foreach($columnOrders[$tabName] as $co):
$name = str_replace("-", " ", $co);
?>
<th>
<?php echo h(ucfirst($name)); ?>
<div class="th-inner" style="flex-direction: column; align-items: flex-start; padding-top: 3px;">
<?php echo empty($static) ? h(ucfirst($name)) : ''; ?>
<div class="th-inner" style="flex-direction: column; align-items: flex-start; padding-top: 3px; color: white;">
<span><?php echo h(ucfirst($name)); ?></span>
<i style="font-size: smaller;"><?php echo __('(%s items)', isset($column[$co]) ? count($column[$co]) : 0); ?></i>
</div>
</th>
@ -160,7 +180,7 @@ foreach($tabs as $tabName => $column):
$externalId = isset($cell['external_id']) ? $cell['external_id'] : '';
$title = h($externalId);
if (!empty($cell['description'])) {
if (empty($static) && !empty($cell['description'])) {
$shortDescription = $this->Markdown->toText($cell['description']);
if (strlen($shortDescription) > 1000) {
$shortDescription = mb_substr($shortDescription, 0, 1000) . '[...]';

View File

@ -0,0 +1,2 @@
<?php
$this->extend('/Emails/notification_common');

View File

@ -0,0 +1,7 @@
<?php
$this->__vars = [
'event_table_include_basescore' => false,
];
$this->extend('/Emails/notification_common');
$this->assign('detailed-summary-type', ' ');
$this->assign('detailed-summary-tags', ' ');

View File

@ -0,0 +1,5 @@
<?php
$this->__vars = [
'event_table_include_basescore' => true,
];
$this->extend('/Emails/notification_common');

View File

@ -0,0 +1,518 @@
<?php
/**
* Available template block that can be overriden using $this->assign('table-overview', ' ');
* - `prepend-html`
* - `table-overview`
* - `detailed-summary-full`
* - `detailed-summary-mitre-attack`
* - `detailed-summary-type`
* - `detailed-summary-tags`
* - `detailed-summary-events`
* - `aggregated-context`
*
* Additional variables:
* - `event-table-include-basescore`: bool
*/
if (empty($this->__vars)) {
$this->__vars = [];
}
$default_vars = [
'event_table_include_basescore' => true,
'additional_taxonomy_event_list' => [
'PAP' => 'PAP:'
],
];
$vars = array_merge($default_vars, $this->__vars);
$now = new DateTime();
$start_date = new DateTime('7 days ago');
$event_number = count($events);
$attribute_number = 0;
$object_number = 0;
$event_report_number = 0;
$proposal_number = 0;
$attribute_types = [];
$object_types = [];
$all_event_report = [];
$all_tag_amount = [];
$unique_tag_number = 0;
$tag_color_mapping = [];
$mitre_attack_techniques = [];
$mitre_galaxy_tag_prefix = 'misp-galaxy:mitre-attack-pattern="';
foreach ($events as $event) {
$unique_tag_per_event = [];
$attribute_number += count($event['Attribute']);
$object_number += count($event['Object']);
$event_report_number += count($event['EventReport']);
$proposal_number += count($event['ShadowAttribute']);
foreach ($event['EventTag'] as $event_tag) {
$tag = $event_tag['Tag'];
if (!empty($unique_tag_per_event[$tag['name']])) {
continue; // Only one instance of tag per event
}
$unique_tag_per_event[$tag['name']] = true;
if (empty($all_tag_amount[$tag['name']])) {
$all_tag_amount[$tag['name']] = 0;
$tag_color_mapping[$tag['name']] = $tag['colour'];
}
$all_tag_amount[$tag['name']] += 1;
if (!empty($tag['is_galaxy']) && substr($tag['name'], 0, strlen($mitre_galaxy_tag_prefix)) === $mitre_galaxy_tag_prefix) {
$technique = substr($tag['name'], strlen($mitre_galaxy_tag_prefix), strlen($tag['name']) - strlen($mitre_galaxy_tag_prefix) - 1);
$mitre_attack_techniques[$technique] = $event_tag;
}
}
foreach ($event['Attribute'] as $attribute) {
if (empty($attribute_types[$attribute['type']])) {
$attribute_types[$attribute['type']] = 0;
}
$attribute_types[$attribute['type']] += 1;
foreach ($attribute['AttributeTag'] as $attribute_tag) {
$tag = $attribute_tag['Tag'];
if (!empty($unique_tag_per_event[$tag['name']])) {
continue; // Only one instance of tag per event
}
$unique_tag_per_event[$tag['name']] = true;
if (empty($all_tag_amount[$tag['name']])) {
$all_tag_amount[$tag['name']] = 0;
$tag_color_mapping[$tag['name']] = $tag['colour'];
}
$all_tag_amount[$tag['name']] += 1;
if (!empty($tag['is_galaxy']) && substr($tag['name'], 0, strlen($mitre_galaxy_tag_prefix)) === $mitre_galaxy_tag_prefix) {
$technique = substr($tag['name'], strlen($mitre_galaxy_tag_prefix), strlen($tag['name']) - strlen($mitre_galaxy_tag_prefix) - 1);
$mitre_attack_techniques[$technique] = $attribute_tag;
}
}
}
foreach ($event['Object'] as $object) {
if (empty($object_types[$object['name']])) {
$object_types[$object['name']] = 0;
}
$object_types[$object['name']] += 1;
$attribute_number += count($object['Attribute']);
foreach ($object['Attribute'] as $attribute) {
if (empty($attribute_types[$attribute['type']])) {
$attribute_types[$attribute['type']] = 0;
}
$attribute_types[$attribute['type']] += 1;
foreach ($attribute['AttributeTag'] as $attribute_tag) {
$tag = $attribute_tag['Tag'];
if (!empty($unique_tag_per_event[$tag['name']])) {
continue; // Only one instance of tag per event
}
$unique_tag_per_event[$tag['name']] = true;
if (empty($all_tag_amount[$tag['name']])) {
$all_tag_amount[$tag['name']] = 0;
$tag_color_mapping[$tag['name']] = $tag['colour'];
}
$all_tag_amount[$tag['name']] += 1;
if (!empty($tag['is_galaxy']) && substr($tag['name'], 0, strlen($mitre_galaxy_tag_prefix)) === $mitre_galaxy_tag_prefix) {
$technique = substr($tag['name'], strlen($mitre_galaxy_tag_prefix), strlen($tag['name']) - strlen($mitre_galaxy_tag_prefix) - 1);
$mitre_attack_techniques[$technique] = $attribute_tag;
}
}
}
}
foreach ($event['EventReport'] as $event_report) {
$all_event_report[] = [
'uuid' => $event_report['uuid'],
'name' => $event_report['name'],
'event_id' => $event_report['event_id'],
'event_info' => $event['Event']['info'],
];
}
}
if (!function_exists('findAndBuildTag')) {
function findAndBuildTag($tag_list, $tag_prefix, $that)
{
foreach ($tag_list as $tag) {
if (substr($tag['Tag']['name'], 0, strlen($tag_prefix)) == $tag_prefix) {
return $that->element('tag', ['tag' => $tag]);
}
}
return '';
}
}
$unique_tag_number = count(array_keys($all_tag_amount));
arsort($attribute_types);
arsort($object_types);
arsort($all_tag_amount);
array_splice($attribute_types, 10);
array_splice($object_types, 10);
array_splice($all_tag_amount, 10);
?>
<?php if ($this->fetch('prepend-html')) : ?>
<?= $this->fetch('prepend-html') ?>
<?php endif; ?>
<?php if ($this->fetch('table-overview')) : ?>
<?= $this->fetch('table-overview'); ?>
<?php else : ?>
<div class="panel">
<div class="panel-header">
<?= __('Data at a glance') ?>
</div>
<div class="panel-body">
<table class="table table-condensed mw-50">
<tbody>
<tr>
<td><?= __('Summary period') ?></td>
<td><?= h($period) ?></td>
</tr>
<tr>
<td><?= __('Summary for dates') ?></td>
<td>
<?=
sprintf('<strong>%s</strong> (Week %s) ➞ <strong>%s</strong> (Week %s)',
$start_date->format('M d, o'),
$start_date->format('W'),
$now->format('M d, o'),
$now->format('W'),
$start_date->format('M d, o'),
)
?>
</td>
</tr>
<tr>
<td><?= __('Generation date') ?></td>
<td><?= date("c"); ?></td>
</tr>
<tr>
<td><?= __('Events #') ?></td>
<td><?= $event_number ?></td>
</tr>
<tr>
<td><?= __('Attributes #') ?></td>
<td><?= $attribute_number ?></td>
</tr>
<tr>
<td><?= __('Objects #') ?></td>
<td><?= $object_number ?></td>
</tr>
<tr>
<td><?= __('Event Report #') ?></td>
<td><?= $event_report_number ?></td>
</tr>
<tr>
<td><?= __('Proposals #') ?></td>
<td><?= $proposal_number ?></td>
</tr>
<tr>
<td><?= __('Unique tags #') ?></td>
<td><?= $unique_tag_number ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php if ($this->fetch('detailed-summary-full')) : ?>
<?= $this->fetch('detailed-summary-full'); ?>
<?php else : ?>
<div class="panel">
<div class="panel-header">
<?= __('Detailed summary') ?>
</div>
<div class="panel-body">
<?php if ($this->fetch('detailed-summary-mitre-attack')) : ?>
<?= $this->fetch('detailed-summary-mitre-attack'); ?>
<?php else : ?>
<?php if (!empty($mitre_attack_techniques)) : ?>
<h4><?= __('Mitre Att&ck techniques') ?></h4>
<ul>
<?php foreach ($mitre_attack_techniques as $technique => $tag) : ?>
<li>
<?php
$tag['Tag']['name'] = $technique;
echo $this->element('tag', ['tag' => $tag])
?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endif; ?>
<?php if ($this->fetch('detailed-summary-type')) : ?>
<?= $this->fetch('detailed-summary-type'); ?>
<?php else : ?>
<?php if (!empty($attribute_types)) : ?>
<h4><?= __('Top 10 Attribute types') ?></h4>
<ul>
<?php foreach ($attribute_types as $type => $amount) : ?>
<li><strong><?= h($type) ?></strong>: <?= $amount ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($object_types)) : ?>
<h4><?= __('Top 10 MISP Object names') ?></h4>
<ul>
<?php foreach ($object_types as $name => $amount) : ?>
<li><strong><?= h($name) ?></strong>: <?= $amount ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($all_event_report)) : ?>
<h4><?= __('All Event Reports') ?></h4>
<ul>
<?php foreach ($all_event_report as $report) : ?>
<li>
<a href="<?= sprintf('%s/eventReports/view/%s', $baseurl, h($report['uuid'])) ?>">
<?= sprintf('%s :: %s', h($report['event_info']), h($report['name'])); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endif; ?>
<?php if ($this->fetch('detailed-summary-tags')) : ?>
<?= $this->fetch('detailed-summary-tags'); ?>
<?php else : ?>
<h4><?= __('Top 10 Tags') ?></h4>
<ul>
<?php foreach ($all_tag_amount as $tag_name => $amount) : ?>
<li>
<span style="padding: 2px 9px; margin-right: 5px; border-radius: 9px; font-weight: bold; background-color: #999; color: #fff;">
<?= $amount ?>
</span>
<?= $this->element('tag', ['tag' => ['Tag' => ['name' => $tag_name, 'colour' => $tag_color_mapping[$tag_name]]]]) ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($this->fetch('detailed-summary-events')) : ?>
<?= $this->fetch('detailed-summary-events'); ?>
<?php else : ?>
<?php if (!empty($events)) : ?>
<h4><?= __('Event list') ?></h4>
<table class="table table-condensed">
<thead>
<tr>
<th><?= __('Publish date') ?></th>
<th><?= __('Creator Org.') ?></th>
<th><?= __('Distribution') ?></th>
<th><?= __('State') ?></th>
<th><?= __('Threat Level') ?></th>
<?php foreach ($vars['additional_taxonomy_event_list'] as $taxonomy_name => $taxonomy_prefix) : ?>
<th><?= h($taxonomy_name) ?></th>
<?php endforeach; ?>
<?php if (!empty($vars['event_table_include_basescore'])) : ?>
<th><?= __('Decaying Base Score') ?></th>
<?php endif; ?>
<th><?= __('Event Info') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($events as $event) : ?>
<?php
$workflowTag = findAndBuildTag($event['EventTag'], 'workflow:', $this);
$analysisHtml = !empty($workflowTag) ? $workflowTag : '';
$tlpTag = findAndBuildTag($event['EventTag'], 'tlp:', $this);
$tlpHtml = !empty($tlpTag) ? $tlpTag : '';
?>
<tr>
<td><?= DateTime::createFromFormat('U', h($event['Event']['publish_timestamp']))->format('Y-m-d') ?></td>
<td><?= h($event['Orgc']['name']) ?></td>
<td>
<<?= !empty($tlpHtml) ? 'small' : 'span' ?>><?= h($distributionLevels[$event['Event']['distribution']]) ?></<?= !empty($tlpHtml) ? 'small' : 'span' ?>>
<span style="margin-left: 3px;"><?= $tlpHtml ?></span>
</td>
<td>
<<?= !empty($analysisHtml) ? 'small' : 'span' ?>><?= h($analysisLevels[$event['Event']['analysis']]) ?></<?= !empty($analysisHtml) ? 'small' : 'span' ?>>
<span style="margin-left: 3px;"><?= $analysisHtml ?></span>
</td>
<td><?= h($event['ThreatLevel']['name']); ?></td>
<?php foreach ($vars['additional_taxonomy_event_list'] as $taxonomy_name => $taxonomy_prefix) : ?>
<td><?= findAndBuildTag($event['EventTag'], $taxonomy_prefix, $this) ?></td>
<?php endforeach; ?>
<?php if (!empty($vars['event_table_include_basescore'])) : ?>
<td>
<?php if (isset($event['event_base_score'])) : ?>
<table class="table-xcondensed no-border">
<?php foreach ($event['event_base_score'] as $bs) : ?>
<tr>
<td style="line-height: 14px;"><i class="no-overflow" style="max-width: 12em;" title="<?= h($bs['DecayingModel']['name']); ?>"><?= h($bs['DecayingModel']['name']); ?>:</i></td>
<td style="line-height: 14px;"><b style="color: <?= !empty($bs['decayed']) ? '#b94a48' : '#468847' ?>;"><?= round($bs['base_score'], 2) ?></b></td>
</tr>
<?php endforeach; ?>
</table>
<?php else : ?>
&nbsp;
<?php endif; ?>
</td>
<?php endif; ?>
<td><a href="<?= sprintf('%s/events/view/%s', $baseurl, h($event['Event']['uuid'])) ?>"><?= h($event['Event']['info']) ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p><?= __('No events.') ?></p>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endif; // detailed-summary-full
?>
<?php if ($this->fetch('trending-summary')) : ?>
<?= $this->fetch('trending-summary'); ?>
<?php else : ?>
<div class="panel">
<div class="panel-header">
<?= __('Tag trendings') ?>
</div>
<div class="panel-body">
<?= $trending_summary; ?>
</div>
</div>
<?php endif; ?>
<?php if ($this->fetch('aggregated-context')) : ?>
<?= $this->fetch('aggregated-context'); ?>
<?php else : ?>
<div class="panel">
<div class="panel-header">
<?= __('Context summary') ?>
</div>
<div class="panel-body">
<?= $aggregated_context; ?>
</div>
</div>
<?php endif; ?>
<?= $this->fetch('content'); ?>
<style>
.mw-50 {
max-width: 50%;
}
.panel {
border: 1px solid #ccc;
border-radius: 3px;
margin-bottom: 20px;
box-shadow: 0px 5px 10px 0 #00000033;
}
.panel-header {
border-bottom: 1px solid #ccc;
padding: 4px 10px;
background-color: #cccccc22;
font-weight: bold;
font-size: 25px;
clear: both;
line-height: 40px;
}
.panel-body {
padding: 15px;
position: relative;
}
.panel h4 {
margin-top: 0.75em;
}
.panel h4::before {
content: '▲';
transform: rotate(90deg);
display: inline-block;
margin-right: 0.25em;
color: #ccc;
text-shadow: 0px 0px #999;
}
.tag {
display: inline-block;
padding: 2px 4px;
font-size: 12px;
font-weight: bold;
line-height: 14px;
margin-right: 2px;
border-radius: 3px;
}
.no-overflow {
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
.table {
width: 100%;
margin-bottom: 20px;
}
.table.table-condensed td,
.table.table-condensed th {
padding: 4px 5px;
}
.table-xcondensed td,
.table-xcondensed th {
padding: 0px 2px !important;
}
.table th,
.table td {
padding: 8px;
line-height: 20px;
text-align: left;
vertical-align: top;
border-top: 1px solid #dddddd;
}
.table thead th {
vertical-align: bottom;
}
.table caption+thead tr:first-child th,
.table caption+thead tr:first-child td,
.table colgroup+thead tr:first-child th,
.table colgroup+thead tr:first-child td,
.table thead:first-child tr:first-child th,
.table thead:first-child tr:first-child td {
border-top: 0;
}
table.no-border td {
border-top: 0;
}
.table.no-border tbody+tbody {
border-top: 0;
}
</style>

View File

@ -0,0 +1 @@
daily

View File

@ -0,0 +1 @@
monthly

View File

@ -0,0 +1 @@
weekly

View File

@ -392,6 +392,17 @@
echo sprintf('<h3>%s</h3>', $data['title']);
echo sprintf('<p>%s</p>', implode(" ", $data['description']));
echo sprintf("<pre>%s</pre>", implode("\n", $data['url']));
?>
<h3><?= __('Setting up the periodic notification scheduled task.') ?></h3>
<p><?= __('The current recommendation to schedule periodic tasks in MISP is to use CRON jobs.') ?></p>
<p><?= __('The %s functionality takes care of sending daily, weekly and monthly summaries. As this process is resource intensive, it\'s heavily recommended to run it once per day. But more importantly, spamming recipients\' mailbox will undermine their trust and willingness to participate in the community. As such, in order for site-admins to keep running a thriving community, they are advised to make sure their system configuration and behaviours stays inclusive, open, collaborative and enjoyable to all members.', sprintf('<code>%s</code>', 'sendPeriodicSummaryToUsers')) ?></p>
<p><?= __('The command below is a recommendation on how the CRON entry should look like. This entry executes the command each day at 06:00 AM. Daily mails will be sent. Weekly mails are sent on Mondays. Monthly mails are sent on the 1st of each month.') ?></p>
<pre>0 6 * * * /var/www/MISP/app/Console/cake Server sendPeriodicSummaryToUsers >/dev/null 2>&1 # Send daily, weekly and monthly summary when appropriate</pre>
<ul>
<li><?= __('Users can visualize the output that would be generated by accessing %s.', sprintf('<a href="%s">%s</a>', $baseurl . '/users/viewPeriodicSummary/daily', __('their periodic summary'))) ?></li>
<li><?= __('Users can edit their setting by accessing %s.', sprintf('<a href="%s">%s</a>', $baseurl . '/users/notificationSettings', __('their periodic notification settings'))) ?></li>
</ul>
<?php
$data = array(
'title' => __('Administering the background workers via the API.'),
'description' => array(

View File

@ -5,6 +5,9 @@
$md[] = sprintf('## %s', __('Tags and Taxonomies'));
$mdTags = [];
foreach ($tags as $namespace => $entries) {
if (empty($entries[0]['Taxonomy'])) {
continue;
}
$mdTags[] = sprintf('#### %s', h($namespace));
if (!empty($entries[0]['Taxonomy']['description'])) {
$mdTags[] = sprintf('*%s*', h($entries[0]['Taxonomy']['description']));

View File

@ -1,75 +1,84 @@
<div>
<h1><?= __('Aggregated context data') ?></h1>
<h2><?= __('Tags and Taxonomies') ?></h2>
<div>
<?php
$htmlTags = '';
foreach ($tags as $namespace => $entries) {
$htmlTags .= sprintf('<div><h4>%s</h4></div>', h($namespace));
if (!empty($entries[0]['Taxonomy']['description'])) {
$htmlTags .= sprintf('<div><i>%s</i></div>', h($entries[0]['Taxonomy']['description']));
}
$htmlTags .= '<ul>';
foreach ($entries as $entry) {
$taxonomyInfo = '<ul>';
if (!empty($entry['TaxonomyPredicate'])) {
$taxonomyInfo .= sprintf(
'<li><strong>%s</strong>: %s</li>',
h($entry['TaxonomyPredicate']['value']),
h($entry['TaxonomyPredicate']['expanded'])
<?php if (!empty($tags)): ?>
<h2><?= __('Tags and Taxonomies') ?></h2>
<div>
<?php
$htmlTags = '';
$customTagHtml = '';
foreach ($tags as $namespace => $entries) {
if (empty($entries[0]['Taxonomy'])) {
continue;
}
$htmlTags .= sprintf('<div><h4><code>%s</code></h4></div>', h($namespace));
if (!empty($entries[0]['Taxonomy']['description'])) {
$htmlTags .= sprintf('<div><i>%s</i></div>', h($entries[0]['Taxonomy']['description']));
}
$htmlTags .= '<ul>';
foreach ($entries as $entry) {
$taxonomyInfo = '<ul>';
if (!empty($entry['TaxonomyPredicate'])) {
$taxonomyInfo .= sprintf(
'<li><strong>%s</strong>: %s</li>',
h($entry['TaxonomyPredicate']['value']),
h($entry['TaxonomyPredicate']['expanded'])
);
}
if (!empty($entry['TaxonomyEntry'])) {
$taxonomyInfo .= sprintf(
'<li><strong>%s</strong>: %s</li>',
h($entry['TaxonomyEntry']['value']),
h($entry['TaxonomyEntry']['expanded'])
);
}
$taxonomyInfo .= '</ul>';
$htmlTags .= sprintf(
'<li>%s</li>%s',
$this->element('tag', ['tag' => $entry]),
$taxonomyInfo
);
}
if (!empty($entry['TaxonomyEntry'])) {
$taxonomyInfo .= sprintf(
'<li><strong>%s</strong>: %s</li>',
h($entry['TaxonomyEntry']['value']),
h($entry['TaxonomyEntry']['expanded'])
);
}
$taxonomyInfo .= '</ul>';
$htmlTags .= sprintf(
'<li>%s</li>%s',
$this->element('tag', ['tag' => $entry]),
$taxonomyInfo
);
$htmlTags .= '</ul>';
}
$htmlTags .= '</ul>';
}
echo $htmlTags;
?>
</div>
echo $htmlTags;
?>
</div>
<?php endif; ?>
<h2><?= __('Galaxy Clusters') ?></h2>
<div>
<?php
$htmlClusters = '';
foreach ($clusters as $tagname => $entries) {
$htmlClusters .= sprintf(
'<div><h4>%s %s</h4></div>',
sprintf('<i class="%s"></i>', $this->FontAwesome->getClass($entries[0]['Galaxy']['icon'])),
h($entries[0]['Galaxy']['name'])
);
if (!empty($entries[0]['Galaxy']['description'])) {
$htmlClusters .= sprintf('<div><i>%s</i></div>', h($entries[0]['Galaxy']['description']));
}
$htmlClusters .= '<ul>';
foreach ($entries as $cluster) {
<?php if (!empty($clusters)): ?>
<h2><?= __('Galaxy Clusters') ?></h2>
<div>
<?php
$htmlClusters = '';
foreach ($clusters as $tagname => $entries) {
$htmlClusters .= sprintf(
'<li><strong><a href="%s" target="_blank">%s</a></strong></li> %s',
$baseurl . '/galaxy_clusters/view/' . h($cluster['GalaxyCluster']['id']),
h($cluster['GalaxyCluster']['value']),
strlen(h($cluster['GalaxyCluster']['description'])) > 300 ?
(substr(h($cluster['GalaxyCluster']['description']), 0, 300) . '...') : h($cluster['GalaxyCluster']['description'])
'<div><h4>%s %s</h4></div>',
sprintf('<i class="%s"></i>', $this->FontAwesome->getClass($entries[0]['Galaxy']['icon'])),
h($entries[0]['Galaxy']['name'])
);
if (!empty($entries[0]['Galaxy']['description'])) {
$htmlClusters .= sprintf('<div><i>%s</i></div>', h($entries[0]['Galaxy']['description']));
}
$htmlClusters .= '<ul>';
foreach ($entries as $cluster) {
$htmlClusters .= sprintf(
'<li><strong><a href="%s" target="_blank">%s</a></strong></li> %s',
$baseurl . '/galaxy_clusters/view/' . h($cluster['GalaxyCluster']['id']),
h($cluster['GalaxyCluster']['value']),
strlen(h($cluster['GalaxyCluster']['description'])) > 300 ?
(substr(h($cluster['GalaxyCluster']['description']), 0, 300) . '...') : h($cluster['GalaxyCluster']['description'])
);
}
$htmlClusters .= '</ul>';
}
$htmlClusters .= '</ul>';
}
echo $htmlClusters;
?>
</div>
echo $htmlClusters;
?>
</div>
<?php endif; ?>
<h2><?= __('Mitre ATT&CK Matrix') ?></h2>
<div id="attackmatrix_div" style="position: relative; border: solid 1px;" class="statistics_attack_matrix">
<?= $this->element('view_galaxy_matrix', $attackData); ?>
</div>
<?php if (!empty($attackData)): ?>
<h2><?= __('Mitre ATT&CK Matrix') ?></h2>
<div style="position: relative;" class="statistics_attack_matrix">
<?= $this->element('view_galaxy_matrix', $attackData); ?>
</div>
<?php endif; ?>
</div>

View File

@ -96,6 +96,12 @@
echo $this->Form->input('disabled', array('type' => 'checkbox', 'label' => __('Immediately disable this user account')));
echo '</div>';
?>
<h5><?= __('Subscribe to the following notification periods:') ?></h5>
<?php
echo $this->Form->input('notification_daily', array('label' => __('Daily notifications'), 'type' => 'checkbox'));
echo $this->Form->input('notification_weekly', array('label' => __('Weekly notifications'), 'type' => 'checkbox'));
echo $this->Form->input('notification_monthly', array('label' => __('Monthly notifications'), 'type' => 'checkbox'));
?>
</fieldset>
<div style="border-bottom: 1px solid #e5e5e5;width:100%;">&nbsp;</div>
<div class="clear" style="margin-top:10px;">

View File

@ -154,6 +154,20 @@
'data_path' => 'User.contactalert',
'colors' => true,
),
array(
'name' => __('Periodic notif.'),
'element' => 'custom',
'class' => 'short',
'function' => function (array $user) use ($periodic_notifications) {
$period_subscriptions = [];
foreach ($periodic_notifications as $period) {
if (!empty($user['User'][$period])) {
$period_subscriptions[] = substr($period, 13, 1);
}
}
return implode('/', $period_subscriptions);
}
),
array(
'name' => __('PGP Key'),
'element' => 'boolean',

View File

@ -0,0 +1,87 @@
<?php
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'title' => __('Notification settings'),
'fields' => [
[
'field' => 'notification_daily',
'label' => __('Subscribe to daily notifications'),
'default' => 0,
'type' => 'checkbox'
],
[
'field' => 'notification_weekly',
'label' => __('Subscribe to weekly notifications'),
'default' => 0,
'type' => 'checkbox'
],
[
'field' => 'notification_monthly',
'label' => __('Subscribe to monthly notifications'),
'default' => 0,
'type' => 'checkbox'
],
sprintf('<h4>%s</h4>', __('Notification filters')),
[
'field' => 'periodic_settings.orgc_id',
'label' => __('Creator organisation'),
'options' => $orgs,
'type' => 'dropdown',
'multiple' => true,
'picker' => true,
],
[
'field' => 'periodic_settings.distribution',
'label' => __('Distribution level'),
'class' => 'input',
'options' => [-1 => ' '] + $distributionLevels,
'type' => 'dropdown'
],
[
'field' => 'periodic_settings.sharing_group_id',
'label' => __('Sharing Group'),
'class' => 'input',
'options' => $sharingGroups,
'type' => 'dropdown',
'multiple' => true,
],
[
'field' => 'periodic_settings.event_info',
'label' => __('Event info'),
'class' => 'input',
'placeholder' => 'Phishing URL',
],
[
'field' => 'periodic_settings.tags',
'label' => __('Event Tags'),
'type' => 'tagsPicker',
'placeholder' => '["tlp:red"]',
],
sprintf('<h4>%s</h4>', __('Notification filters')),
[
'field' => 'periodic_settings.trending_for_tags',
'label' => __('Generate trends for tag namespaces'),
'type' => 'textarea',
'placeholder' => '["misp-galaxy:mitre-attack-pattern", "admiralty-scale"]',
],
],
'submit' => [
'action' => $this->request->params['action'],
'ajaxSubmit' => 'submitGenericFormInPlace();'
]
]
]);
if (!$ajax) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'globalActions', 'menuItem' => 'notification_settings'));
}
?>
<script>
$(document).ready(function() {
checkSharingGroup('periodic_settings');
$('#periodic_settingsDistribution').change(function() {
checkSharingGroup('periodic_settings');
});
})
</script>

View File

@ -1,4 +1,13 @@
<?php
$periodic_notification_settings_html = '<table>';
foreach ($periodic_notifications as $periodic_notification) {
$active_html = $this->element('genericElements/IndexTable/Fields/boolean', [
'row' => ['active' => !empty($user['User'][$periodic_notification])],
'field' => ['data_path' => 'active', 'colors' => true, ],
]);
$periodic_notification_settings_html .= sprintf('<tr><td>%s</td><td>%s<td></tr>', Inflector::humanize($periodic_notification), $active_html);
}
$periodic_notification_settings_html .= '</table>';
$table_data = array();
$table_data[] = array('key' => __('ID'), 'value' => $user['User']['id']);
$table_data[] = array(
@ -16,6 +25,7 @@
);
$table_data[] = array('key' => __('Role'), 'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id'])));
$table_data[] = array('key' => __('Event alert enabled'), 'boolean' => $user['User']['autoalert']);
$table_data[] = ['key' => __('Periodic Notifications'), 'html' => $periodic_notification_settings_html];
$table_data[] = array('key' => __('Contact alert enabled'), 'boolean' => $user['User']['contactalert']);
if (!$admin_view && !$user['Role']['perm_auth']) {

View File

@ -0,0 +1,26 @@
<div class="index">
<div class="btn-group">
<a class="btn <?= $period == 'daily' ? 'btn-primary' : 'btn-inverse' ?>" href="<?= $baseurl . '/users/viewPeriodicSummary/daily' ?>"><?= __('Daily') ?></a>
<a class="btn <?= $period == 'weekly' ? 'btn-primary' : 'btn-inverse' ?>" href="<?= $baseurl . '/users/viewPeriodicSummary/weekly' ?>"><?= __('Weekly') ?></a>
<a class="btn <?= $period == 'monthly' ? 'btn-primary' : 'btn-inverse' ?>" href="<?= $baseurl . '/users/viewPeriodicSummary/monthly' ?>"><?= __('Monthly') ?></a>
</div>
<h2><?= __('MISP %s summary', h($period)); ?></h2>
<button type="button" class="btn btn-inverse" data-toggle="collapse" data-target="#summary-filters">
<?= __('Show settings used to generate the summary') ?>
</button>
<div id="summary-filters" class="collapse">
<pre>
<?= JsonTool::encode($periodic_settings, true) ?>
</pre>
</div>
<div class="report-container" style="margin-top: 2em;">
<?= $summary; ?>
</div>
</div>
<?php
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'event-collection', 'menuItem' => 'viewPeriodicSummary'));
?>

View File

@ -995,6 +995,14 @@ a.proposal_link_red:hover {
border-radius: 3px 0 3px 0;
}
.report-container {
margin: 0 0;
justify-content: center;
padding: 10px 15px;
overflow-y: auto;
box-shadow: 0 2px 6px 6px #eee;
}
.modal-body-long {
max-height: 600px;
}

View File

@ -6198,6 +6198,8 @@ components:
- $ref: "#/components/schemas/OrganisationName"
tags:
$ref: "#/components/schemas/TagsRestSearchFilter"
event_tags:
$ref: "#/components/schemas/TagsRestSearchFilter"
searchall:
description: "Search events by matching any tag names, event descriptions, attribute values or attribute comments"
type: string

View File

@ -8190,6 +8190,39 @@
"column_type": "int(11)",
"column_default": "0",
"extra": ""
},
{
"column_name": "notification_daily",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(1)",
"column_default": "0",
"extra": ""
},
{
"column_name": "notification_weekly",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(1)",
"column_default": "0",
"extra": ""
},
{
"column_name": "notification_monthly",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(1)",
"column_default": "0",
"extra": ""
}
],
"user_settings": [
@ -8698,7 +8731,7 @@
"restrict_to_org_id": false,
"restrict_to_permission_flag": false,
"user_id": false,
"uuid": false
"uuid": true
},
"decaying_models": {
"all_orgs": false,
@ -8836,7 +8869,7 @@
"title": false,
"type": false,
"user_agent_sha256": false,
"uuid": false
"uuid": true
},
"jobs": {
"id": true
@ -9029,7 +9062,7 @@
"id": true,
"org_id": false,
"user_id": false,
"uuid": false
"uuid": true
},
"tag_collection_tags": {
"id": true,
@ -9116,5 +9149,5 @@
"uuid": false
}
},
"db_version": "95"
"db_version": "97"
}