Skip to content

Commit

Permalink
Merge pull request #1269 from christer77/Improved-detection-and-prote…
Browse files Browse the repository at this point in the history
…ction-against-business-email-compromise-BEC-such-as-CEO-fraud

feat: Improved detection and protection against business email compromise (BEC) such as CEO fraud
  • Loading branch information
kroky authored Nov 26, 2024
2 parents 8b689f9 + 13ad26b commit cf725c7
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 7 deletions.
2 changes: 1 addition & 1 deletion modules/imap/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ function format_imap_message_list($msg_list, $output_module, $parent_list=false,
if (isset($msg['preview_msg'])) {
$preview_msg = $msg['preview_msg'];
}

if ($parent_list == 'sent') {
$icon = 'sent';
$from = $msg['to'];
Expand Down
113 changes: 109 additions & 4 deletions modules/imap/handler_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,12 @@ public function process() {
$offset = 0;
$msgs = array();
$list_page = 1;
$include_content_body = false;
$include_preview = $this->user_config->get('active_preview_message_setting', false);
$ceo_use_detect_ceo_fraud = $this->user_config->get('ceo_use_detect_ceo_fraud_setting', false);
if ($include_preview || $ceo_use_detect_ceo_fraud) {
$include_content_body = true;
}

list($success, $form) = $this->process_form(array('imap_server_id', 'folder'));
if ($success) {
Expand All @@ -840,16 +845,44 @@ public function process() {
$existingEmails = array_map(function($c){
return $c->value('email_address');
},$contact_list);
list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, $existingEmails, $include_preview);
list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, $existingEmails, $include_content_body);
} else {
list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, null, $include_preview);
list($total, $results) = $imap->get_mailbox_page(hex2bin($form['folder']), $sort, $rev, $filter, $offset, $limit, $keyword, null, $include_content_body);
}
foreach ($results as $msg) {
$msg['server_id'] = $form['imap_server_id'];
$msg['server_name'] = $details['name'];
$msg['folder'] = $form['folder'];
$msgs[] = $msg;
$uid = $msg['uid'];

if ($ceo_use_detect_ceo_fraud && hex2bin($form['folder']) == 'INBOX') {
if ($this->isCeoFraud($msg['to'], $msg['subject'], $msg['preview_msg'])) {

$folder = "Suspicious emails";
if (!count($imap->get_mailbox_status($folder))) {
$imap->create_mailbox($folder);
}
$dest_folder = bin2hex($folder);
$server_ids = array(
$form['imap_server_id'] => [
$form['folder'] => $uid
]
);
imap_move_same_server($server_ids, "move", $this->cache, [null, null, $dest_folder]);
$msg = [];
$total--;
}
}

if ($msg) {
if (! $include_preview && isset($msg['preview_msg'])) {
$msg['preview_msg'] = "";
}
$msgs[] = $msg;
}

}

if ($imap->selected_mailbox) {
$imap->selected_mailbox['detail']['exists'] = $total;
$this->out('imap_folder_detail', array_merge($imap->selected_mailbox, array('offset' => $offset, 'limit' => $limit)));
Expand All @@ -862,6 +895,61 @@ public function process() {
$this->out('do_not_flag_as_read_on_open', $this->user_config->get('unread_on_open_setting', false));
}
}
public function isCeoFraud($email, $subject, $msg) {
// 1. Check Suspicious Terms or Requests
$suspiciousTerms = explode(",", $this->user_config->get("ceo_suspicious_terms_setting"));
if ($this->detectSuspiciousTerms($msg, $suspiciousTerms) || $this->detectSuspiciousTerms($subject, $suspiciousTerms)) {

// 2. check ceo_rate_limit
$amounts = $this->extractAmountFromEmail($msg);
$amountLimit = $this->user_config->get("ceo_amount_limit_setting");
$isUpperAmount = array_reduce($amounts, function ($carry, $value) use ($amountLimit) {
return $carry || $value > $amountLimit;
}, false);

if ($isUpperAmount) {
if ($this->user_config->get("ceo_use_trusted_contact_setting")) {
$contacts = $this->get('contact_store');
$contact_list = $contacts->getAll();
$existingEmails = array_map(function($c){
return $c->value('email_address');
},$contact_list);
if (!$this->isEmailInTrustedDomainList(array_values($existingEmails), $email)) {
return true;
}
} else {
return true;
}
}
}
return false;
}
private function detectSuspiciousTerms($msg, $suspiciousTerms) {
foreach ($suspiciousTerms as $phrase) {
if (stripos($msg, trim($phrase)) !== false) {
return true;
}
}
return false;
}
private function isEmailInTrustedDomainList($trustedDomain, $email) {
if (in_array($email, $trustedDomain)) {
return true;
}
return false;
}
private function extractAmountFromEmail($emailBody) {
$pattern = '/\b\d+(?:,\d+)?\.?\d*\s*(?:USD|dollars?|US\$?|EUR|euros?|€|JPY|yen|¥|GBP|pounds?|£|CAD|CAD\$|AUD|AUD\$)/i';

preg_match_all($pattern, $emailBody, $matches);

if ($matches) {
return array_map(function($value) {
return floatval(preg_replace('/[^0-9]/', '', $value));
}, $matches[0]);
}
}

}

/**
Expand Down Expand Up @@ -2129,11 +2217,28 @@ function process_move_messages_in_screen_email_enabled_callback($val) { return $
process_site_setting('move_messages_in_screen_email', $this, 'process_move_messages_in_screen_email_enabled_callback', true, true);
}
}

class Hm_Handler_process_setting_active_preview_message extends Hm_Handler_Module {
public function process() {
function process_active_preview_message_callback($val) { return $val; }
process_site_setting('active_preview_message', $this, 'process_active_preview_message_callback', true, true);
}
}


/**
* Process setting_ceo_detection_fraud in the settings page
* @subpackage core/handler
*/
class Hm_Handler_process_setting_ceo_detection_fraud extends Hm_Handler_Module {
public function process() {
function process_ceo_use_detect_ceo_fraud_callback($val) { return $val; }
function process_ceo_use_trusted_contact_callback($val) { return $val; }
function process_ceo_suspicious_terms_callback($val) { return $val; }
function process_ceo_amount_limit_callback($val) { return $val; }

process_site_setting('ceo_use_detect_ceo_fraud', $this, 'process_ceo_use_detect_ceo_fraud_callback');
process_site_setting('ceo_use_trusted_contact', $this, 'process_ceo_use_trusted_contact_callback');
process_site_setting('ceo_suspicious_terms', $this, 'process_ceo_suspicious_terms_callback');
process_site_setting('ceo_rate_limit', $this, 'process_ceo_amount_limit_callback');
}
}
4 changes: 2 additions & 2 deletions modules/imap/hm-imap.php
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ public function poll() {
* @param bool $raw flag to disable decoding header values
* @return array list of headers and values for the specified uids
*/
public function get_message_list($uids, $raw=false, $include_preview = false) {
public function get_message_list($uids, $raw=false, $include_content_body = false) {
if (is_array($uids)) {
sort($uids);
$sorted_string = implode(',', $uids);
Expand All @@ -891,7 +891,7 @@ public function get_message_list($uids, $raw=false, $include_preview = false) {
$command .= 'X-GM-MSGID X-GM-THRID X-GM-LABELS ';
}
$command .= "BODY.PEEK[HEADER.FIELDS (SUBJECT X-AUTO-BCC FROM DATE CONTENT-TYPE X-PRIORITY TO LIST-ARCHIVE REFERENCES MESSAGE-ID X-SNOOZED)]";
if ($include_preview) {
if ($include_content_body) {
$command .= " BODY.PEEK[0.1]";
}
$command .= ")\r\n";
Expand Down
49 changes: 49 additions & 0 deletions modules/imap/output_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,7 @@ protected function output() {
return $res;
}
}

class Hm_Output_setting_active_preview_message extends Hm_Output_Module {
protected function output() {
$settings = $this->get('user_settings', array());
Expand All @@ -1498,4 +1499,52 @@ protected function output() {
return $res;
}
}
class Hm_Output_setting_ceo_detection_fraud extends Hm_Output_Module {
protected function output() {
$settings = $this->get('user_settings', array());
$ceo_use_detect_ceo_fraud = "";
$ceo_use_trusted_contact = "checked";
$ceo_suspicious_terms = "wire transfer, urgent, account details, payment instruction";
$ceo_rate_limit = "100";
if (array_key_exists('ceo_use_detect_ceo_fraud', $settings)) {
if ($settings['ceo_use_detect_ceo_fraud']) {
$ceo_use_detect_ceo_fraud = "checked";
} else {
$ceo_use_detect_ceo_fraud = "";
}
}

if (array_key_exists('ceo_use_trusted_contact', $settings)) {
if ($settings['ceo_use_trusted_contact']) {
$ceo_use_trusted_contact = "checked";
} else {
$ceo_use_trusted_contact = "";
}
}

if (array_key_exists('ceo_suspicious_terms', $settings) && $settings['ceo_suspicious_terms']) {
if ($settings['ceo_suspicious_terms']) {
$ceo_suspicious_terms = $settings['ceo_suspicious_terms'];
}
}
if (array_key_exists('ceo_rate_limit', $settings) && $settings['ceo_rate_limit']) {
if ($settings['ceo_rate_limit']) {
$ceo_rate_limit = $settings['ceo_rate_limit'];
}
}

$res = '<tr class="general_setting"><td><label for="ceo_use_detect_ceo_fraud">'.
$this->trans('CEO fraud: Use Detect CEO Fraud').
'</label></td><td><input class="form-check-input" type="checkbox" role="switch" id="ceo_use_detect_ceo_fraud" name="ceo_use_detect_ceo_fraud" '. $ceo_use_detect_ceo_fraud .' ></td></tr>';
$res .= '<tr class="general_setting"><td><label for="ceo_use_trusted_contact">'.
$this->trans('CEO fraud: Use Trusted Contacts as Valid emails ').
'</label></td><td><input class="form-check-input" type="checkbox" role="switch" id="ceo_use_trusted_contact" name="ceo_use_trusted_contact" '. $ceo_use_trusted_contact .' ></td></tr>';
$res .= '<tr class="general_setting"><td><label for="ceo_suspicious_terms">'.
$this->trans('CEO fraud: Suspicious Phrases or Requests(separate by ",")').
'</label></td><td><textarea class="form-control form-control-sm w-auto" role="switch" id="ceo_suspicious_terms" name="ceo_suspicious_terms">'. $ceo_suspicious_terms .'</textarea></td></tr>';
$res .= '<tr class="general_setting"><td><label for="ceo_rate_limit">'.
$this->trans('CEO fraud: Rate-Limit or Monitor Unusual Requests').
'</label></td><td><input class="form-control form-control-sm w-auto" type="number" min="0" role="switch" id="ceo_rate_limit" name="ceo_rate_limit" value="'. $ceo_rate_limit .'" ></td></tr>';
return $res;
}
}
6 changes: 6 additions & 0 deletions modules/imap/setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
add_handler('settings', 'process_first_time_screen_emails_per_page_setting', true, 'imap', 'date', 'after');
add_handler('settings', 'process_setting_move_messages_in_screen_email', true, 'imap', 'process_first_time_screen_emails_per_page_setting', 'after');
add_handler('settings', 'process_setting_active_preview_message', true, 'imap', 'process_setting_move_messages_in_screen_email', 'after');
add_handler('settings', 'process_setting_ceo_detection_fraud', true, 'imap', 'process_setting_move_messages_in_screen_email', 'after');
add_output('settings', 'imap_server_ids', true, 'imap', 'page_js', 'before');
add_output('settings', 'start_sent_settings', true, 'imap', 'end_settings_form', 'before');
add_output('settings', 'sent_since_setting', true, 'imap', 'start_sent_settings', 'after');
Expand All @@ -64,6 +65,7 @@
add_output('settings', 'first_time_screen_emails_per_page_setting', true, 'imap', 'imap_auto_advance_email', 'after');
add_output('settings', 'setting_move_messages_in_screen_email', true, 'imap', 'first_time_screen_emails_per_page_setting', 'after');
add_output('settings', 'setting_active_preview_message', true, 'imap', 'setting_move_messages_in_screen_email', 'after');
add_output('settings', 'setting_ceo_detection_fraud', true, 'imap', 'default_sort_order_setting', 'after');

/* compose page data */
add_output('compose', 'imap_server_ids', true, 'imap', 'page_js', 'before');
Expand Down Expand Up @@ -442,5 +444,9 @@
'permissions' => FILTER_DEFAULT,
'action' => FILTER_DEFAULT,
'active_preview_message' => FILTER_VALIDATE_BOOLEAN,
'ceo_use_detect_ceo_fraud' => FILTER_VALIDATE_BOOLEAN,
'ceo_use_trusted_contact' => FILTER_VALIDATE_BOOLEAN,
'ceo_suspicious_terms' => FILTER_DEFAULT,
'ceo_rate_limit' => FILTER_VALIDATE_INT,
)
);

0 comments on commit cf725c7

Please sign in to comment.