Merge pull request #6967 from JakubOnderka/html-alert-email

HTML alert email
pull/7168/head
Jakub Onderka 2021-03-06 12:18:33 +01:00 committed by GitHub
commit 71e1d486fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 517 additions and 268 deletions

View File

@ -29,6 +29,13 @@ class EventShell extends AppShell
],
)
));
$parser->addSubcommand('testEventNotificationEmail', [
'help' => __('Generate event notification email in EML format.'),
'arguments' => [
'event_id' => ['help' => __('Event ID'), 'required' => true],
'user_id' => ['help' => __('User ID'), 'required' => true],
],
]);
return $parser;
}
@ -469,6 +476,34 @@ class EventShell extends AppShell
$this->Job->save($job);
}
public function testEventNotificationEmail()
{
list($eventId, $userId) = $this->args;
$user = $this->getUser($userId);
$eventForUser = $this->Event->fetchEvent($user, [
'eventid' => $eventId,
'includeAllTags' => true,
'includeEventCorrelations' => true,
'noEventReports' => true,
'noSightings' => true,
'metadata' => Configure::read('MISP.event_alert_metadata_only') ?: false,
]);
if (empty($eventForUser)) {
$this->error("Event with ID $eventId not exists or given user don't have permission to access it.");
}
$emailTemplate = $this->Event->prepareAlertEmail($eventForUser[0], $user);
App::uses('SendEmail', 'Tools');
App::uses('GpgTool', 'Tools');
$sendEmail = new SendEmail(GpgTool::initializeGpg());
$sendEmail->setTransport('Debug');
$result = $sendEmail->sendToUser(['User' => $user], null, $emailTemplate);
echo $result['contents']['headers'] . "\n\n" . $result['contents']['message'] . "\n";
}
/**
* @param int $userId
* @return array

View File

@ -838,27 +838,7 @@ class EventsController extends AppController
return $this->RestResponse->viewData($export->eventIndex($events), 'csv');
}
$user = $this->Auth->user();
$user = $this->Event->User->fillKeysToUser($user);
if (empty($user['gpgkey']) && Configure::read('GnuPG.onlyencrypted')) {
// No GnuPG
if (Configure::read('SMIME.enabled') && empty($user['certif_public'])) {
// No GnuPG and No SMIME
$this->Flash->info(__('No X.509 certificate or GnuPG key set in your profile. To receive emails, submit your public certificate or GnuPG key in your profile.'));
} elseif (!Configure::read('SMIME.enabled')) {
$this->Flash->info(__('No GnuPG key set in your profile. To receive emails, submit your public key in your profile.'));
}
} elseif ($this->Auth->user('autoalert') && empty($user['gpgkey']) && Configure::read('GnuPG.bodyonlyencrypted')) {
// No GnuPG & autoalert
if ($this->Auth->user('autoalert') && Configure::read('SMIME.enabled') && empty($user['certif_public'])) {
// No GnuPG and No SMIME & autoalert
$this->Flash->info(__('No X.509 certificate or GnuPG key set in your profile. To receive attributes in emails, submit your public certificate or GnuPG key in your profile.'));
} elseif (!Configure::read('SMIME.enabled')) {
$this->Flash->info(__('No GnuPG key set in your profile. To receive attributes in emails, submit your public key in your profile.'));
}
}
$this->__noKeyNotification();
$this->set('events', $events);
$this->set('eventDescriptions', $this->Event->fieldDescriptions);
$this->set('analysisLevels', $this->Event->analysisLevels);
@ -876,6 +856,34 @@ class EventsController extends AppController
}
}
private function __noKeyNotification()
{
$onlyEncrypted = Configure::read('GnuPG.onlyencrypted');
$bodyOnlyEncrypted = Configure::read('GnuPG.bodyonlyencrypted');
if (!$onlyEncrypted && !$bodyOnlyEncrypted) {
return;
}
$user = $this->Event->User->fillKeysToUser($this->Auth->user());
if (!empty($user['gpgkey'])) {
return; // use has PGP key
}
if ($onlyEncrypted) {
if (Configure::read('SMIME.enabled') && empty($user['certif_public'])) {
$this->Flash->info(__('No X.509 certificate or PGP key set in your profile. To receive emails, submit your public certificate or PGP key in your profile.'));
} elseif (!Configure::read('SMIME.enabled')) {
$this->Flash->info(__('No PGP key set in your profile. To receive emails, submit your public key in your profile.'));
}
} elseif ($bodyOnlyEncrypted && $user['autoalert']) {
if (Configure::read('SMIME.enabled') && empty($user['certif_public'])) {
$this->Flash->info(__('No X.509 certificate or PGP key set in your profile. To receive attributes in emails, submit your public certificate or PGP key in your profile.'));
} elseif (!Configure::read('SMIME.enabled')) {
$this->Flash->info(__('No PGP key set in your profile. To receive attributes in emails, submit your public key in your profile.'));
}
}
}
public function filterEventIndex()
{
$passedArgsArray = array();

View File

@ -3,6 +3,36 @@ App::uses('CakeEmail', 'Network/Email');
class SendEmailException extends Exception {}
class CakeEmailBody
{
/** @var string|null */
public $html;
/** @var string|null */
public $text;
public function __construct($text = null, $html = null)
{
$this->html = $html;
$this->text = $text;
}
/**
* @return string
*/
public function format()
{
if (!empty($this->html) && !empty($this->text)) {
return 'both';
}
if (!empty($this->html)) {
return 'html';
}
return 'text';
}
}
/**
* Class CakeEmailExtended
*
@ -14,7 +44,7 @@ class SendEmailException extends Exception {}
class CakeEmailExtended extends CakeEmail
{
/**
* @var MimeMultipart|MessagePart
* @var MimeMultipart|MessagePart|CakeEmailBody
*/
private $body;
@ -30,7 +60,7 @@ class CakeEmailExtended extends CakeEmail
$headers['Content-Type'] = $this->body->getContentType();
} else if ($this->body instanceof MessagePart) {
$headers = array_merge($headers, $this->body->getHeaders());
} else {
} else if ($this->_emailFormat !== 'both') { // generate correct content-type header for 'text' or 'html' format
$headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->boundary() . '"';
}
@ -71,18 +101,40 @@ class CakeEmailExtended extends CakeEmail
return $this->body->render();
} else if ($this->body instanceof MessagePart) {
return $this->body->render(false);
} else if ($this->body instanceof CakeEmailBody) {
return $this->_render([]); // @see _renderTemplates
}
return $this->_render($this->_wrap($this->body));
throw new InvalidArgumentException("Expected that body is instance of MimeMultipart, MessagePart or CakeEmailBody, " . gettype($this->body) . " given.");
}
// This is hack how to force CakeEmail to always generate multipart message.
protected function _renderTemplates($content)
{
if (!$this->body instanceof CakeEmailBody) {
throw new InvalidArgumentException("Expected instance of CakeEmailBody, " . gettype($this->body) . " given.");
}
$this->_boundary = md5(uniqid());
$output = parent::_renderTemplates($content);
$output[''] = '';
return $output;
$rendered = [];
if (!empty($this->body->text)) {
$rendered['text'] = $this->body->text;
}
if (!empty($this->body->html)) {
$rendered['html'] = $this->body->html;
}
foreach ($rendered as $type => $content) {
$content = str_replace(array("\r\n", "\r"), "\n", $content);
$content = $this->_encodeString($content, $this->charset);
$content = $this->_wrap($content);
$content = implode("\n", $content);
$rendered[$type] = rtrim($content, "\n");
}
// This is hack how to force CakeEmail to always generate multipart message.
$rendered[''] = '';
return $rendered;
}
protected function _render($content)
@ -101,7 +153,7 @@ class CakeEmailExtended extends CakeEmail
if ($content !== null) {
throw new InvalidArgumentException("Content must be null for CakeEmailExtended.");
}
return parent::send($this->body);
return parent::send();
}
public function __toString()
@ -252,11 +304,12 @@ class MessagePart
class SendEmail
{
/**
* @var CryptGpgExtended
*/
/** @var CryptGpgExtended */
private $gpg;
/** @var string|null */
private $transport;
/**
* @param CryptGpgExtended|null $gpg
*/
@ -271,6 +324,14 @@ class SendEmail
}
}
/**
* @param string $transport
*/
public function setTransport($transport)
{
$this->transport = $transport;
}
/**
* @param array $params
* @return array|bool
@ -285,7 +346,8 @@ class SendEmail
}
}
$params['body'] = str_replace('\n', PHP_EOL, $params['body']); // TODO: Why this?
$body = str_replace('\n', PHP_EOL, $params['body']); // TODO: Why this?
$body = new CakeEmailBody($body);
$attachments = array();
if (!empty($params['requestor_gpgkey'])) {
@ -306,10 +368,14 @@ class SendEmail
$email->returnPath(Configure::read('MISP.email'));
$email->to($params['to']);
$email->subject($params['subject']);
$email->emailFormat('text');
$email->body($params['body']);
$email->emailFormat($body->format());
$email->body($body);
$email->attachments($attachments);
if ($this->transport) {
$email->transport($this->transport);
}
$mock = false;
if (!empty(Configure::read('MISP.disable_emailing')) || !empty($params['mock'])) {
$email->transport('Debug');
@ -352,16 +418,20 @@ class SendEmail
/**
* @param array $user
* @param string $subject
* @param string $body
* @param string|null $bodyWithoutEncryption
* @param SendEmailTemplate|string $body
* @param string|false $bodyWithoutEncryption
* @param array $replyToUser
* @return bool True if e-mail is encrypted, false if not.
* @return array
* @throws Crypt_GPG_BadPassphraseException
* @throws Crypt_GPG_Exception
* @throws SendEmailException
*/
public function sendToUser(array $user, $subject, $body, $bodyWithoutEncryption = null, array $replyToUser = array())
public function sendToUser(array $user, $subject, $body, $bodyWithoutEncryption = false, array $replyToUser = array())
{
if ($body instanceof SendEmailTemplate && $bodyWithoutEncryption !== false) {
throw new InvalidArgumentException("When body is instance of SendEmailTemplate, \$bodyWithoutEncryption must be false.");
}
if (Configure::read('MISP.disable_emailing')) {
throw new SendEmailException('Emailing is currently disabled on this instance.');
}
@ -378,14 +448,36 @@ class SendEmail
throw new SendEmailException('Encrypted messages are enforced and the message could not be encrypted for this user as no valid encryption key was found.');
}
// If 'bodyonlyencrypted' is enabled and the user has no encryption key, use the alternate body (if it exists)
if (Configure::read('GnuPG.bodyonlyencrypted') && !$canEncryptSmime && !$canEncryptGpg && $bodyWithoutEncryption) {
$body = $bodyWithoutEncryption;
// If 'GnuPG.bodyonlyencrypted' is enabled and the user has no encryption key, use the alternate body
$hideDetails = Configure::read('GnuPG.bodyonlyencrypted') && !$canEncryptSmime && !$canEncryptGpg;
if ($body instanceof SendEmailTemplate) {
$body->set('canEncryptSmime', $canEncryptSmime);
$body->set('canEncryptGpg', $canEncryptGpg);
$bodyContent = $body->render($hideDetails);
$subject = $body->subject() ?: $subject; // Get generated subject from template
} else {
if ($hideDetails && $bodyWithoutEncryption) {
$body = $bodyWithoutEncryption;
}
$bodyContent = new CakeEmailBody($body);
}
$body = str_replace('\n', PHP_EOL, $body); // TODO: Why this?
$email = $this->create($user, $subject, $bodyContent, [], $replyToUser);
$email = $this->create($user, $subject, $body, array(), $replyToUser);
if ($this->transport) {
$email->transport($this->transport);
}
// Generate `In-Reply-To` and `References` headers to group emails
if ($body instanceof SendEmailTemplate && $body->referenceId()) {
$reference = sha1($body->referenceId() . '|' . Configure::read('MISP.uuid'));
$reference = "<$reference@{$email->domain()}>";
$email->addHeaders([
'In-Reply-To' => $reference,
'References' => $reference,
]);
}
$signed = false;
if (Configure::read('GnuPG.sign')) {
@ -454,8 +546,11 @@ class SendEmail
}
try {
$email->send();
return $encrypted;
return [
'contents' => $email->send(),
'encrypted' => $encrypted,
'subject' => $subject,
];
} catch (Exception $e) {
throw new SendEmailException('The message could not be sent.', 0, $e);
}
@ -499,15 +594,23 @@ class SendEmail
/**
* @param array $user User model
* @param string $subject
* @param string $body
* @param CakeEmailBody $body
* @param array $attachments
* @param array $replyToUser User model
* @return CakeEmailExtended
*/
private function create(array $user, $subject, $body, array $attachments = array(), array $replyToUser = array())
private function create(array $user, $subject, CakeEmailBody $body, array $attachments = array(), array $replyToUser = array())
{
$email = new CakeEmailExtended();
$fromEmail = Configure::read('MISP.email');
// Set correct domain when sending email from CLI
$fromEmailParts = explode('@', $fromEmail, 2);
if (isset($fromEmailParts[1])) {
$email->domain($fromEmailParts[1]);
}
// We must generate message ID by own, because CakeEmail returns different message ID for every call of
// getHeaders() method.
$email->messageId($this->generateMessageId($email));
@ -530,11 +633,11 @@ class SendEmail
$email->replyTo(Configure::read('MISP.email_reply_to'));
}
$email->from(Configure::read('MISP.email'));
$email->returnPath(Configure::read('MISP.email')); // TODO?
$email->from($fromEmail, Configure::read('MISP.email_from_name'));
$email->returnPath($fromEmail); // TODO?
$email->to($user['User']['email']);
$email->subject($subject);
$email->emailFormat('text');
$email->emailFormat($body->format());
$email->body($body);
foreach ($attachments as $key => $value) {
@ -558,14 +661,15 @@ class SendEmail
$messagePart = new MessagePart();
$messagePart->addHeader('Content-Type', array(
'multipart/mixed',
$email->emailFormat() === 'both' ? 'multipart/alternative' : 'multipart/mixed',
'boundary="' . $email->boundary() . '"',
'protected-headers="v1"',
));
// Protect User-Facing Headers according to https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-01.html
// Protect User-Facing Headers and Structural Headers according to
// https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html
$originalHeaders = $email->getHeaders(array('subject', 'from', 'to'));
$protectedHeaders = array('From', 'To', 'Date', 'Message-ID', 'Subject', 'Reply-To');
$protectedHeaders = ['From', 'To', 'Date', 'Message-ID', 'Subject', 'Reply-To', 'In-Reply-To', 'References'];
foreach ($protectedHeaders as $header) {
if (isset($originalHeaders[$header])) {
$messagePart->addHeader($header, $originalHeaders[$header]);
@ -654,7 +758,7 @@ class SendEmail
$messagePart = new MessagePart();
$messagePart->addHeader('Content-Type', array(
'multipart/mixed',
$email->emailFormat() === 'both' ? 'multipart/alternative' : 'multipart/mixed',
'boundary="' . $email->boundary() . '"',
));
$messagePart->setPayload($renderedEmail);

View File

@ -0,0 +1,88 @@
<?php
class SendEmailTemplate
{
/** @var array */
private $viewVars = [];
/** @var string */
private $viewName;
/** @var string|null */
private $referenceId;
/** @var string|null */
private $subject;
public function __construct($viewName)
{
$this->viewName = $viewName;
}
/**
* This value will be used for grouping emails in mail client.
* @param string|null $referenceId
* @return string
*/
public function referenceId($referenceId = null)
{
if ($referenceId === null) {
return $this->referenceId;
}
$this->referenceId = $referenceId;
}
/**
* Get subject from template. Must be called after render method.
* @param string|null $subject
* @return string
*/
public function subject($subject = null)
{
if ($subject === null) {
return $this->subject;
}
$this->subject = $subject;
}
/**
* Set template variable.
* @param string $key
* @param mixed $value
*/
public function set($key, $value)
{
$this->viewVars[$key] = $value;
}
/**
* @param bool $hideDetails True when GnuPG.bodyonlyencrypted is enabled and e-mail cannot be send in encrypted form
* @return CakeEmailBody
* @throws CakeException
*/
public function render($hideDetails = false)
{
$View = new View();
$View->autoLayout = false;
$View->helpers = ['TextColour'];
$View->loadHelpers();
$View->set($this->viewVars);
$View->set('hideDetails', $hideDetails);
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'html';
try {
$html = $View->render($this->viewName);
} catch (MissingViewException $e) {
$html = null; // HTMl template is optional
}
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'text';
$View->hasRendered = false;
$text = $View->render($this->viewName);
// Template can change default subject.
$this->subject = $View->get('subject');
return new CakeEmailBody($text, $html);
}
}

View File

@ -4,6 +4,7 @@ App::uses('CakeEmail', 'Network/Email');
App::uses('RandomTool', 'Tools');
App::uses('AttachmentTool', 'Tools');
App::uses('TmpFileTool', 'Tools');
App::uses('SendEmailTemplate', 'Tools');
/**
* @property User $User
@ -3155,7 +3156,7 @@ class Event extends AppModel
{
$event = $this->find('first', [
'conditions' => ['Event.id' => $id],
'contain' => ['EventTag' => ['Tag'], 'ThreatLevel'],
'recursive' => -1,
]);
if (empty($event)) {
throw new NotFoundException('Invalid Event.');
@ -3184,27 +3185,6 @@ class Event extends AppModel
$event['Event']['sharing_group_id'],
$userConditions
);
if (Configure::read('MISP.extended_alert_subject')) {
$subject = preg_replace("/\r|\n/", "", $event['Event']['info']);
if (strlen($subject) > 58) {
$subject = substr($subject, 0, 55) . '... - ';
} else {
$subject .= " - ";
}
} else {
$subject = '';
}
$subjMarkingString = $this->getEmailSubjectMarkForEvent($event);
if (Configure::read('MISP.threatlevel_in_email_subject') === false) {
$threatLevel = '';
} else {
$threatLevel = $event['ThreatLevel']['name'] . " - ";
}
$subject = "[" . Configure::read('MISP.org') . " MISP] Event $id - $subject$threatLevel" . strtoupper($subjMarkingString);
$eventUrl = $this->__getAnnounceBaseurl() . "/events/view/" . $id;
$bodyNoEnc = __("A new or modified event was just published on %s", $eventUrl) . "\n\n";
$bodyNoEnc .= __("If you would like to unsubscribe from receiving such alert e-mails, simply\ndisable publish alerts via %s", $this->__getAnnounceBaseurl() . '/users/edit');
$userCount = count($usersWithAccess);
$this->UserSetting = ClassRegistry::init('UserSetting');
@ -3216,11 +3196,12 @@ class Event extends AppModel
'includeEventCorrelations' => true,
'noEventReports' => true,
'noSightings' => true,
'metadata' => Configure::read('MISP.event_alert_metadata_only') ?: false,
])[0];
if ($this->UserSetting->checkPublishFilter($user, $eventForUser)) {
$body = $this->__buildAlertEmailBody($eventForUser, $user, $oldpublish);
$this->User->sendEmail(['User' => $user], $body, $bodyNoEnc, $subject);
$body = $this->prepareAlertEmail($eventForUser, $user, $oldpublish);
$this->User->sendEmail(['User' => $user], $body, false, null);
}
if ($jobId) {
$this->Job->saveProgress($jobId, null, $k / $userCount * 100);
@ -3233,136 +3214,46 @@ class Event extends AppModel
return true;
}
/**
* @param string $body
* @param string $bodyTempOther
* @param array $objects
* @param int|null $oldpublish Timestamp of latest publish
*/
private function __buildAlertEmailObject(&$body, &$bodyTempOther, array $objects, $oldpublish)
{
foreach ($objects as $object) {
if (isset($oldpublish) && isset($object['timestamp']) && $object['timestamp'] > $oldpublish) {
$body .= '* ';
} else {
$body .= ' ';
}
$body .= $object['name'] . '/' . $object['meta-category'] . "\n";
if (!empty($object['Attribute'])) {
$body .= $this->__buildAlertEmailAttribute($body, $bodyTempOther, $object['Attribute'], $oldpublish, ' ');
}
}
}
/**
* @param string $body
* @param string $bodyTempOther
* @param array $attributes
* @param int|null $oldpublish Timestamp of latest publish
* @param string $indent
*/
private function __buildAlertEmailAttribute(&$body, &$bodyTempOther, array $attributes, $oldpublish, $indent = ' ')
{
$appendlen = 20;
foreach ($attributes as $attribute) {
$ids = $attribute['to_ids'] ? ' (IDS)' : '';
// Defanging URLs (Not "links") emails domains/ips in notification emails
$value = $attribute['value'];
if ('url' == $attribute['type'] || 'uri' == $attribute['type']) {
$value = str_ireplace("http", "hxxp", $value);
$value = str_ireplace(".", "[.]", $value);
} elseif (in_array($attribute['type'], array('email-src', 'email-dst', 'whois-registrant-email', 'dns-soa-email', 'email-reply-to'))) {
$value = str_replace("@", "[at]", $value);
} elseif (in_array($attribute['type'], array('hostname', 'domain', 'ip-src', 'ip-dst', 'domain|ip'))) {
$value = str_replace(".", "[.]", $value);
}
$strRepeatCount = $appendlen - 2 - strlen($attribute['type']);
$strRepeat = ($strRepeatCount > 0) ? str_repeat(' ', $strRepeatCount) : '';
if (isset($oldpublish) && isset($attribute['timestamp']) && $attribute['timestamp'] > $oldpublish) {
$line = '* ' . $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . " *\n";
} else {
$line = $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . "\n";
}
if (!empty($attribute['AttributeTag'])) {
$tags = [];
foreach ($attribute['AttributeTag'] as $aT) {
$tags[] = $aT['Tag']['name'];
}
$line .= ' - Tags: ' . implode(', ', $tags) . "\n";
}
if ('other' == $attribute['type']) { // append the 'other' attribute types to the bottom.
$bodyTempOther .= $line;
} else {
$body .= $line;
}
}
}
/**
* @param array $event
* @param array $user
* @param int|null $oldpublish Timestamp of latest publish
* @return string
* @param array $user E-mail receiver
* @param int|null $oldpublish Timestamp of previous publishing.
* @return SendEmailTemplate
* @throws CakeException
*/
private function __buildAlertEmailBody(array $event, array $user, $oldpublish)
public function prepareAlertEmail(array $event, array $user, $oldpublish = null)
{
// The mail body, h() is NOT needed as we are sending plain-text mails.
$body = "";
$body .= '==============================================' . "\n";
$body .= 'URL : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $event['Event']['id'] . "\n";
$body .= 'Event ID : ' . $event['Event']['id'] . "\n";
$body .= 'Date : ' . $event['Event']['date'] . "\n";
if (Configure::read('MISP.showorg')) {
$body .= 'Reported by : ' . $event['Orgc']['name'] . "\n";
$body .= 'Local owner of the event : ' . $event['Org']['name'] . "\n";
}
$body .= 'Distribution: ' . $this->distributionLevels[$event['Event']['distribution']] . "\n";
if ($event['Event']['distribution'] == 4) {
$body .= 'Sharing Group: ' . $event['SharingGroup']['name'] . "\n";
}
$tags = [];
foreach ($event['EventTag'] as $tag) {
$tags[] = $tag['Tag']['name'];
}
$body .= 'Tags: ' . implode(', ', $tags) . "\n";
$body .= 'Threat Level: ' . $event['ThreatLevel']['name'] . "\n";
$body .= 'Analysis : ' . $this->analysisLevels[$event['Event']['analysis']] . "\n";
$body .= 'Description : ' . $event['Event']['info'] . "\n";
if (!empty($event['RelatedEvent'])) {
$body .= '==============================================' . "\n";
$body .= 'Related to: '. "\n";
foreach ($event['RelatedEvent'] as $relatedEvent) {
$body .= $this->__getAnnounceBaseurl() . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ') ' ."\n";
if (Configure::read('MISP.extended_alert_subject')) {
$subject = preg_replace("/\r|\n/", "", $event['Event']['info']);
if (strlen($subject) > 58) {
$subject = substr($subject, 0, 55) . '... - ';
} else {
$subject .= " - ";
}
$body .= '==============================================' . "\n";
} else {
$subject = '';
}
$bodyTempOther = "";
if (!empty($event['Attribute'])) {
$body .= 'Attributes (* indicates a new or modified attribute):' . "\n";
$this->__buildAlertEmailAttribute($body, $bodyTempOther, $event['Attribute'], $oldpublish);
if (Configure::read('MISP.threatlevel_in_email_subject') === false) {
$threatLevel = '';
} else {
$threatLevel = $event['ThreatLevel']['name'] . " - ";
}
if (!empty($event['Object'])) {
$body .= 'Objects (* indicates a new or modified object):' . "\n";
$this->__buildAlertEmailObject($body, $bodyTempOther, $event['Object'], $oldpublish);
}
if (!empty($bodyTempOther)) {
$body .= "\n";
}
$body .= $bodyTempOther; // append the 'other' attribute types to the bottom.
$body .= '==============================================' . "\n";
$body .= sprintf(
"You receive this e-mail because the e-mail address %s is set to receive publish alerts on the MISP instance at %s.%s%s",
$user['email'],
$this->__getAnnounceBaseurl(),
PHP_EOL,
PHP_EOL
);
$body .= "If you would like to unsubscribe from receiving such alert e-mails, simply\ndisable publish alerts via " . $this->__getAnnounceBaseurl() . '/users/edit' . PHP_EOL;
$body .= '==============================================' . "\n";
return $body;
$subjMarkingString = $this->getEmailSubjectMarkForEvent($event);
$subject = "[" . Configure::read('MISP.org') . " MISP] Event {$event['Event']['id']} - $subject$threatLevel" . strtoupper($subjMarkingString);
$template = new SendEmailTemplate('alert');
$template->set('event', $event);
$template->set('user', $user);
$template->set('oldPublishTimestamp', $oldpublish);
$template->set('baseurl', $this->__getAnnounceBaseurl());
$template->set('distributionLevels', $this->distributionLevels);
$template->set('analysisLevels', $this->analysisLevels);
$template->set('tlp', $subjMarkingString);
$template->subject($subject);
$template->referenceId("event-alert|{$event['Event']['id']}");
return $template;
}
/**
@ -3422,69 +3313,33 @@ class Event extends AppModel
$tplColorString = $this->getEmailSubjectMarkForEvent($event);
$subject = "[" . Configure::read('MISP.org') . " MISP] Need info about event $id - " . strtoupper($tplColorString);
$result = true;
foreach ($orgMembers as $reporter) {
list($bodyevent, $body) = $this->__buildContactEventEmailBody($user, $message, $event);
$result = $this->User->sendEmail($reporter, $bodyevent, $body, $subject, $user) && $result;
foreach ($orgMembers as $eventReporter) {
$body = $this->prepareContactAlertEmail($user, $eventReporter, $message, $event);
$result = $this->User->sendEmail($eventReporter, $body, false, $subject, $user) && $result;
}
return $result;
}
private function __buildContactEventEmailBody(array $user, $message, array $event)
/**
* @param array $user
* @param array $eventReporter
* @param string $message
* @param array $event
* @return SendEmailTemplate
*/
private function prepareContactAlertEmail(array $user, array $eventReporter, $message, array $event)
{
// The mail body, h() is NOT needed as we are sending plain-text mails.
$body = "";
$body .= "Hello, \n";
$body .= "\n";
$body .= "Someone wants to get in touch with you concerning a MISP event. \n";
$body .= "\n";
$body .= "You can reach them at " . $user['User']['email'] . "\n";
if (!empty($user['User']['gpgkey'])) {
$body .= "Their GnuPG key is added as attachment to this email. \n";
}
if (!empty($user['User']['certif_public'])) {
$body .= "Their Public certificate is added as attachment to this email. \n";
}
$body .= "\n";
$body .= "They wrote the following message: \n";
$body .= $message . "\n";
$body .= "\n";
$body .= "\n";
$body .= "The event is the following: \n";
// print the event in mail-format
// LATER place event-to-email-layout in a function
$body .= 'URL : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $event['Event']['id'] . "\n";
$bodyevent = $body;
$bodyevent .= 'Event ID : ' . $event['Event']['id'] . "\n";
$bodyevent .= 'Date : ' . $event['Event']['date'] . "\n";
if (Configure::read('MISP.showorg')) {
$bodyevent .= 'Reported by : ' . $event['Orgc']['name'] . "\n";
}
$bodyevent .= 'Risk : ' . $event['ThreatLevel']['name'] . "\n";
$bodyevent .= 'Analysis : ' . $this->analysisLevels[$event['Event']['analysis']] . "\n";
foreach ($event['RelatedEvent'] as $relatedEvent) {
$bodyevent .= 'Related to : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ')' . "\n";
}
$bodyevent .= 'Info : ' . "\n";
$bodyevent .= $event['Event']['info'] . "\n";
$bodyTempOther = "";
if (!empty($event['Attribute'])) {
$bodyevent .= 'Attributes:' . "\n";
$this->__buildAlertEmailAttribute($bodyevent, $bodyTempOther, $event['Attribute'], null);
}
if (!empty($event['Object'])) {
$bodyevent .= 'Objects:' . "\n";
$this->__buildAlertEmailObject($bodyevent, $bodyTempOther, $event['Object'], null);
}
if (!empty($bodyTempOther)) {
$bodyevent .= "\n";
}
$bodyevent .= $bodyTempOther; // append the 'other' attribute types to the bottom.
return array($bodyevent, $body);
$template = new SendEmailTemplate('alert_contact');
$template->set('event', $event);
$template->set('requestor', $user);
$template->set('message', $message);
$template->set('user', $this->User->rearrangeToAuthForm($eventReporter));
$template->set('baseurl', $this->__getAnnounceBaseurl());
$template->set('distributionLevels', $this->distributionLevels);
$template->set('analysisLevels', $this->analysisLevels);
$template->set('contactAlert', true);
$template->set('tlp', $this->getEmailSubjectMarkForEvent($event));
return $template;
}
public function captureSGForElement($element, $user, $server=false)

View File

@ -4739,6 +4739,14 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
),
'email_from_name' => [
'level' => 2,
'description' => __('Notification e-mail sender name.'),
'value' => '',
'errorMessage' => '',
'test' => 'testForEmpty',
'type' => 'string',
],
'taxii_sync' => array(
'level' => 3,
'description' => __('This setting is deprecated and can be safely removed.'),
@ -4856,12 +4864,20 @@ class Server extends AppModel
),
'extended_alert_subject' => array(
'level' => 1,
'description' => __('enabling this flag will allow the event description to be transmitted in the alert e-mail\'s subject. Be aware that this is not encrypted by GnuPG, so only enable it if you accept that part of the event description will be sent out in clear-text.'),
'description' => __('Enabling this flag will allow the event description to be transmitted in the alert e-mail\'s subject. Be aware that this is not encrypted by GnuPG, so only enable it if you accept that part of the event description will be sent out in clear-text.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean'
),
'event_alert_metadata_only' => [
'level' => self::SETTING_OPTIONAL,
'description' => __('Send just event metadata (attributes and objects will be omitted) for event alert.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'type' => 'boolean'
],
'default_event_distribution' => array(
'level' => 0,
'description' => __('The default distribution setting for events (0-3).'),

View File

@ -757,7 +757,7 @@ class User extends AppModel
'conditions' => $conditions,
'recursive' => -1,
'fields' => array('id', 'email', 'gpgkey', 'certif_public', 'org_id', 'disabled'),
'contain' => ['Role' => ['fields' => ['perm_site_admin', 'perm_audit']], 'Organisation' => ['fields' => ['id']]],
'contain' => ['Role' => ['fields' => ['perm_site_admin', 'perm_audit']], 'Organisation' => ['fields' => ['id', 'name']]],
));
foreach ($users as $k => $user) {
$user = $user['User'];
@ -786,7 +786,7 @@ class User extends AppModel
* the remaining two parameters are the e-mail subject and a secondary user object which will be used as the replyto address if set. If it is set and an encryption key for the replyTo user exists, then his/her public key will also be attached
*
* @param array $user
* @param string $body
* @param SendEmailTemplate|string $body
* @param string|false $bodyNoEnc
* @param string $subject
* @param array|false $replyToUser
@ -800,13 +800,14 @@ class User extends AppModel
return true;
}
$this->Log = ClassRegistry::init('Log');
$this->loadLog();
$replyToLog = $replyToUser ? ' from ' . $replyToUser['User']['email'] : '';
$gpg = $this->initializeGpg();
$sendEmail = new SendEmail($gpg);
try {
$encrypted = $sendEmail->sendToUser($user, $subject, $body, $bodyNoEnc ?: null, $replyToUser ?: array());
$result = $sendEmail->sendToUser($user, $subject, $body, $bodyNoEnc,$replyToUser ?: []);
} catch (SendEmailException $e) {
$this->logException("Exception during sending e-mail", $e);
@ -823,9 +824,9 @@ class User extends AppModel
return false;
}
$logTitle = $encrypted ? 'Encrypted email' : 'Email';
$logTitle = $result['encrypted'] ? 'Encrypted email' : 'Email';
// Intentional two spaces to pass test :)
$logTitle .= $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $subject . '".';
$logTitle .= $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $result['subject'] . '".';
$this->Log->create();
$this->Log->save(array(

View File

@ -0,0 +1,116 @@
<?php
if (!isset($oldPublishTimestamp)) {
$oldPublishTimestamp = null;
}
if (!isset($contactAlert)) {
$contactAlert = false;
}
if ($hideDetails) { // Used when GnuPG.bodyonlyencrypted is enabled and e-mail cannot be send in encrypted form
$eventUrl = $baseurl . "/events/view/" . $event['Event']['id'];
echo __("A new or modified event was just published on %s", $eventUrl) . PHP_EOL . PHP_EOL;
echo __("If you would like to unsubscribe from receiving such alert e-mails, simply\ndisable publish alerts via %s", $baseurl . '/users/edit');
return;
}
$renderAttributes = function(array $attributes, $indent = ' ') use ($oldPublishTimestamp) {
$appendlen = 20;
foreach ($attributes as $attribute) {
$ids = $attribute['to_ids'] ? ' (IDS)' : '';
// Defanging URLs (Not "links") emails domains/ips in notification emails
$value = $attribute['value'];
if ('url' === $attribute['type'] || 'uri' === $attribute['type']) {
$value = str_ireplace("http", "hxxp", $value);
$value = str_ireplace(".", "[.]", $value);
} elseif (in_array($attribute['type'], ['email-src', 'email-dst', 'whois-registrant-email', 'dns-soa-email', 'email-reply-to'], true)) {
$value = str_replace("@", "[at]", $value);
} elseif (in_array($attribute['type'], ['hostname', 'domain', 'ip-src', 'ip-dst', 'domain|ip'], true)) {
$value = str_replace(".", "[.]", $value);
}
$strRepeatCount = $appendlen - 2 - strlen($attribute['type']);
$strRepeat = ($strRepeatCount > 0) ? str_repeat(' ', $strRepeatCount) : '';
if (isset($oldPublishTimestamp) && isset($attribute['timestamp']) && $attribute['timestamp'] > $oldPublishTimestamp) {
$line = '* ' . $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . " *\n";
} else {
$line = $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . "\n";
}
if (!empty($attribute['AttributeTag'])) {
$tags = [];
foreach ($attribute['AttributeTag'] as $aT) {
$tags[] = $aT['Tag']['name'];
}
$line .= ' - Tags: ' . implode(', ', $tags) . "\n";
}
echo $line;
}
};
$renderObjects = function(array $objects) use ($renderAttributes, $oldPublishTimestamp) {
foreach ($objects as $object) {
$body = '';
if (isset($oldPublishTimestamp) && isset($object['timestamp']) && $object['timestamp'] > $oldPublishTimestamp) {
$body .= '* ';
} else {
$body .= ' ';
}
$body .= $object['name'] . '/' . $object['meta-category'] . "\n";
if (!empty($object['Attribute'])) {
$body .= $renderAttributes($object['Attribute'], ' ');
}
echo $body;
}
};
$tags = [];
foreach ($event['EventTag'] as $tag) {
$tags[] = $tag['Tag']['name'];
}
?>
==============================================
URL : <?= $baseurl ?>/events/view/<?= $event['Event']['id'] . PHP_EOL ?>
Event ID : <?= $event['Event']['id'] . PHP_EOL ?>
Date : <?= $event['Event']['date'] . PHP_EOL ?>
<?php if (Configure::read('MISP.showorg')): ?>
Reported by : <?= $event['Orgc']['name'] . PHP_EOL ?>
Local owner of the event : <?= $event['Org']['name'] . PHP_EOL ?>
<?php endif; ?>
Distribution: <?= $distributionLevels[$event['Event']['distribution']] . PHP_EOL ?>
<?php if ($event['Event']['distribution'] == 4): ?>
Sharing Group: <?= $event['SharingGroup']['name'] . PHP_EOL ?>
<?php endif; ?>
Tags: <?= implode(", ", $tags) . PHP_EOL ?>
Threat Level: <?= $event['ThreatLevel']['name'] . PHP_EOL ?>
Analysis : <?= $analysisLevels[$event['Event']['analysis']] . PHP_EOL ?>
Description : <?= $event['Event']['info'] . PHP_EOL ?>
<?php if (!empty($event['RelatedEvent'])): ?>
==============================================
Related to:
<?php
foreach ($event['RelatedEvent'] as $relatedEvent) {
echo $baseurl . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ') ' . "\n";
}
?>
==============================================
<?php endif; ?>
<?php if (!empty($event['Attribute'])): ?>
Attributes<?= isset($oldPublishTimestamp) ? " (* indicates a new or modified attribute since last update):\n" : ":\n" ?>
<?= $renderAttributes($event['Attribute']) ?>
<?php endif; ?>
<?php if (!empty($event['Object'])): ?>
Objects<?= isset($oldPublishTimestamp) ? " (* indicates a new or modified object since last update):\n" : ":\n" ?>
<?= $renderObjects($event['Object']) ?>
<?php endif; ?>
==============================================
You receive this e-mail because the e-mail address <?= $user['email'] ?> is set
to receive <?= $contactAlert ? 'contact' : 'publish' ?> alerts on the MISP instance at <?= $baseurl ?>.
If you would like to unsubscribe from receiving such alert e-mails, simply
disable <?= $contactAlert ? 'contact' : 'publish' ?> alerts via <?= $baseurl ?>/users/edit
==============================================

View File

@ -0,0 +1,23 @@
Hello,
Someone wants to get in touch with you concerning a MISP event.
You can reach them at <?= $requestor['User']['email'] ?>
<?php if (!empty($requestor['User']['gpgkey'])): ?>
Their PGP key is added as attachment to this email.
<?php endif; ?>
<?php if (!empty($requestor['User']['certif_public'])): ?>
Their Public certificate is added as attachment to this email.
<?php endif; ?>
They wrote the following message:
<?= $message ?>
The event is the following:
<?php
if ($hideDetails) {
echo $baseurl . ' /events/view/' . $event['Event']['id'];
} else {
require __DIR__ . '/alert.ctp'; // include event details
}

View File

@ -6,9 +6,12 @@ set -x
AUTH="$1"
HOST="$2"
curl -i -H "Accept: application/json" -H "content-type: application/json" -H "Authorization: $AUTH" --data "@event.json" -X POST http://${HOST}/events
curl -i -H "Accept: application/json" -H "content-type: application/json" -H "Authorization: $AUTH" --data "@event.json" -X POST http://${HOST}/events > /dev/null
curl -H "Authorization: $AUTH" -X GET http://${HOST}/events/csv/download/1/ignore:1 | sed -e 's/^M//g' | cut -d, -f2 --complement | sort > 1.csv
cat 1.csv
cut -d, -f2 --complement event.csv | sort > compare.csv
diff compare.csv 1.csv
# Test alert email generating
sudo -E su $USER -c '../app/Console/cake Event testEventNotificationEmail 1 1' > /dev/null
# Delete created event
curl -i -H "Accept: application/json" -H "content-type: application/json" -H "Authorization: $AUTH" -X POST http://${HOST}/events/delete/1