From 07ab11dcabe648d5fdd15a1d086e1d874bcef1a5 Mon Sep 17 00:00:00 2001 From: IndiaAce Date: Wed, 20 May 2026 12:49:29 -0400 Subject: [PATCH 1/2] Add first_name/last_name concat matching to org_vips sender rules Co-Authored-By: Claude Opus 4.6 --- .../impersonation_vip_bec_loose.yml | 2 ++ .../impersonation_vip_invoicing_request.yml | 6 +++++- .../impersonation_vip_urgent_request.yml | 6 +++++- .../impersonation_vip_w2_request.yml | 6 +++++- detection-rules/vip_impersonation.yml | 21 +++++++++++++++---- insights/sender/sender_contains_org_vip.yml | 6 +++++- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/detection-rules/impersonation_vip_bec_loose.yml b/detection-rules/impersonation_vip_bec_loose.yml index bb9e57f53b2..c96d189bf99 100644 --- a/detection-rules/impersonation_vip_bec_loose.yml +++ b/detection-rules/impersonation_vip_bec_loose.yml @@ -9,6 +9,8 @@ source: | type.inbound and any($org_vips, 0 <= strings.ilevenshtein(sender.display_name, .display_name) < 4 + or 0 <= strings.ilevenshtein(sender.display_name, strings.concat(.first_name, " ", .last_name)) < 4 + or 0 <= strings.ilevenshtein(sender.display_name, strings.concat(.last_name, ", ", .first_name)) < 4 ) and any(ml.nlu_classifier(body.current_thread.text).intents, .name == "bec" and .confidence in ("medium", "high") diff --git a/detection-rules/impersonation_vip_invoicing_request.yml b/detection-rules/impersonation_vip_invoicing_request.yml index 6e6e2ec76f9..3dcf27781f0 100644 --- a/detection-rules/impersonation_vip_invoicing_request.yml +++ b/detection-rules/impersonation_vip_invoicing_request.yml @@ -4,7 +4,11 @@ type: "rule" severity: "high" source: | type.inbound - and any($org_vips, strings.contains(sender.display_name, .display_name)) + and any($org_vips, + strings.contains(sender.display_name, .display_name) + or strings.contains(sender.display_name, strings.concat(.first_name, " ", .last_name)) + or strings.contains(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + ) and ( ( sender.email.domain.domain in $org_domains diff --git a/detection-rules/impersonation_vip_urgent_request.yml b/detection-rules/impersonation_vip_urgent_request.yml index 9d876acf77f..114da6cf758 100644 --- a/detection-rules/impersonation_vip_urgent_request.yml +++ b/detection-rules/impersonation_vip_urgent_request.yml @@ -7,7 +7,11 @@ type: "rule" severity: "high" source: | type.inbound - and any($org_vips, .display_name =~ sender.display_name) + and any($org_vips, + .display_name =~ sender.display_name + or strings.concat(.first_name, " ", .last_name) == sender.display_name + or strings.concat(.last_name, ", ", .first_name) == sender.display_name + ) and ( any(ml.nlu_classifier(body.current_thread.text).intents, .name == "bec" and .confidence in ("medium", "high") diff --git a/detection-rules/impersonation_vip_w2_request.yml b/detection-rules/impersonation_vip_w2_request.yml index 2c4fc6cc1a1..16d5e4c1c5b 100644 --- a/detection-rules/impersonation_vip_w2_request.yml +++ b/detection-rules/impersonation_vip_w2_request.yml @@ -5,7 +5,11 @@ severity: "high" source: | type.inbound and ( - any($org_vips, strings.contains(sender.display_name, .display_name)) + any($org_vips, + strings.contains(sender.display_name, .display_name) + or strings.contains(sender.display_name, strings.concat(.first_name, " ", .last_name)) + or strings.contains(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + ) or any(regex.extract(sender.display_name, '^(?\S+)\s+(?\S+)$'), any($org_vips, strings.contains(.display_name, ..named_groups["first"]) diff --git a/detection-rules/vip_impersonation.yml b/detection-rules/vip_impersonation.yml index 0dddec63618..8e221485e92 100644 --- a/detection-rules/vip_impersonation.yml +++ b/detection-rules/vip_impersonation.yml @@ -14,13 +14,26 @@ source: | type.inbound and ( // the display name matches a name on the orgs vip list - any($org_vips, .display_name =~ sender.display_name) + any($org_vips, + .display_name =~ sender.display_name + or strings.concat(.first_name, " ", .last_name) == sender.display_name + or strings.concat(.last_name, ", ", .first_name) == sender.display_name + ) // or the display name starts with the name on the orgs vip list or ( any($org_vips, - strings.istarts_with(sender.display_name, .display_name) - // and it is longer than just their name (eg. John Doe CEO) - and length(sender.display_name) > length(.display_name) + ( + strings.istarts_with(sender.display_name, .display_name) + and length(sender.display_name) > length(.display_name) + ) + or ( + strings.istarts_with(sender.display_name, strings.concat(.first_name, " ", .last_name)) + and length(sender.display_name) > length(strings.concat(.first_name, " ", .last_name)) + ) + or ( + strings.istarts_with(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + and length(sender.display_name) > length(strings.concat(.last_name, ", ", .first_name)) + ) ) // and we have confidence it's BEC and any(ml.nlu_classifier(body.current_thread.text).intents, diff --git a/insights/sender/sender_contains_org_vip.yml b/insights/sender/sender_contains_org_vip.yml index 7845dd5c028..14b2901da52 100644 --- a/insights/sender/sender_contains_org_vip.yml +++ b/insights/sender/sender_contains_org_vip.yml @@ -3,7 +3,11 @@ type: "query" source: | type.inbound and any($org_vips, - strings.icontains(sender.display_name, .display_name) + ( + strings.icontains(sender.display_name, .display_name) + or strings.icontains(sender.display_name, strings.concat(.first_name, " ", .last_name)) + or strings.icontains(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + ) and not sender.email.domain.root_domain in $high_trust_sender_root_domains and not sender.email.domain.root_domain in $org_domains and headers.auth_summary.dmarc.pass From 90a3176084fd25d367a7582d78b2cd7bb9c4b8b5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 20 May 2026 16:52:06 +0000 Subject: [PATCH 2/2] Auto-format MQL and add rule IDs --- .../impersonation_vip_bec_loose.yml | 14 +++++++++++-- .../impersonation_vip_invoicing_request.yml | 11 ++++++---- .../impersonation_vip_w2_request.yml | 8 ++++++-- detection-rules/vip_impersonation.yml | 20 +++++++++++++++---- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/detection-rules/impersonation_vip_bec_loose.yml b/detection-rules/impersonation_vip_bec_loose.yml index c96d189bf99..603681afc42 100644 --- a/detection-rules/impersonation_vip_bec_loose.yml +++ b/detection-rules/impersonation_vip_bec_loose.yml @@ -9,8 +9,18 @@ source: | type.inbound and any($org_vips, 0 <= strings.ilevenshtein(sender.display_name, .display_name) < 4 - or 0 <= strings.ilevenshtein(sender.display_name, strings.concat(.first_name, " ", .last_name)) < 4 - or 0 <= strings.ilevenshtein(sender.display_name, strings.concat(.last_name, ", ", .first_name)) < 4 + or 0 <= strings.ilevenshtein(sender.display_name, + strings.concat(.first_name, + " ", + .last_name + ) + ) < 4 + or 0 <= strings.ilevenshtein(sender.display_name, + strings.concat(.last_name, + ", ", + .first_name + ) + ) < 4 ) and any(ml.nlu_classifier(body.current_thread.text).intents, .name == "bec" and .confidence in ("medium", "high") diff --git a/detection-rules/impersonation_vip_invoicing_request.yml b/detection-rules/impersonation_vip_invoicing_request.yml index 3dcf27781f0..7b1aa71020d 100644 --- a/detection-rules/impersonation_vip_invoicing_request.yml +++ b/detection-rules/impersonation_vip_invoicing_request.yml @@ -6,8 +6,12 @@ source: | type.inbound and any($org_vips, strings.contains(sender.display_name, .display_name) - or strings.contains(sender.display_name, strings.concat(.first_name, " ", .last_name)) - or strings.contains(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + or strings.contains(sender.display_name, + strings.concat(.first_name, " ", .last_name) + ) + or strings.contains(sender.display_name, + strings.concat(.last_name, ", ", .first_name) + ) ) and ( ( @@ -30,7 +34,7 @@ source: | ) ) - // and the reply to email address has never been contacted + // and the reply to email address has never been contacted and any(headers.reply_to, .email.email not in $recipient_emails) // negate highly trusted sender domains unless they fail DMARC authentication @@ -41,7 +45,6 @@ source: | ) or sender.email.domain.root_domain not in $high_trust_sender_root_domains ) - attack_types: - "BEC/Fraud" tactics_and_techniques: diff --git a/detection-rules/impersonation_vip_w2_request.yml b/detection-rules/impersonation_vip_w2_request.yml index 16d5e4c1c5b..823845168de 100644 --- a/detection-rules/impersonation_vip_w2_request.yml +++ b/detection-rules/impersonation_vip_w2_request.yml @@ -7,8 +7,12 @@ source: | and ( any($org_vips, strings.contains(sender.display_name, .display_name) - or strings.contains(sender.display_name, strings.concat(.first_name, " ", .last_name)) - or strings.contains(sender.display_name, strings.concat(.last_name, ", ", .first_name)) + or strings.contains(sender.display_name, + strings.concat(.first_name, " ", .last_name) + ) + or strings.contains(sender.display_name, + strings.concat(.last_name, ", ", .first_name) + ) ) or any(regex.extract(sender.display_name, '^(?\S+)\s+(?\S+)$'), any($org_vips, diff --git a/detection-rules/vip_impersonation.yml b/detection-rules/vip_impersonation.yml index 8e221485e92..feef45d690b 100644 --- a/detection-rules/vip_impersonation.yml +++ b/detection-rules/vip_impersonation.yml @@ -27,12 +27,24 @@ source: | and length(sender.display_name) > length(.display_name) ) or ( - strings.istarts_with(sender.display_name, strings.concat(.first_name, " ", .last_name)) - and length(sender.display_name) > length(strings.concat(.first_name, " ", .last_name)) + strings.istarts_with(sender.display_name, + strings.concat(.first_name, " ", .last_name) + ) + and length(sender.display_name) > length(strings.concat(.first_name, + " ", + .last_name + ) + ) ) or ( - strings.istarts_with(sender.display_name, strings.concat(.last_name, ", ", .first_name)) - and length(sender.display_name) > length(strings.concat(.last_name, ", ", .first_name)) + strings.istarts_with(sender.display_name, + strings.concat(.last_name, ", ", .first_name) + ) + and length(sender.display_name) > length(strings.concat(.last_name, + ", ", + .first_name + ) + ) ) ) // and we have confidence it's BEC