Detection rules › Sublime MQL

Sublime MQL rules: vip

VIP / Executive impersonation (strict match, untrusted)

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Sender display name matches the display name of a user in the $org_vips list, and the sender has never been seen before. The $org_vips list must first be manually connected to a VIP group of your upstream provider (Google Workspace and Microsoft 365 only) in order for this rule to work. Once connected, the list will be automatically synced and kept up-to-date. For more information, see the $org_vips documentation: https://docs.sublimesecurity.com/docs/configure-org_vips-list This rule is recommended to be used on a relatively small list of VIPs, and is meant to reduce attack surface by detecting any message that matches the protected list of display names from a first-time or unsolicited sender. Additional rule logic can be added to look for suspicious subjects, suspicious links, etc.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.current_thread
  • headers
  • headers.auth_summary
  • headers.reply_to
  • mailbox
  • recipients
  • sender.email
  • type

Rule Body

type.inbound
and (
  // the display name matches a name on the orgs vip list
  any($org_vips, .display_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)
    )
    // and we have confidence it's BEC
    and any(ml.nlu_classifier(body.current_thread.text).intents,
            .name == "bec" and .confidence == "high"
    )
  )
)
and (
  // ignore personal <> work emails
  // where the sender and mailbox's display name are the same
  length(recipients.to) > 0
  or length(recipients.cc) > 0
  or sender.display_name != mailbox.display_name
)
// bounce-back negations
and not strings.like(sender.email.local_part,
                     "*postmaster*",
                     "*mailer-daemon*",
                     "*administrator*"
)
and not any(attachments,
            .content_type in (
              "message/rfc822",
              "message/delivery-status",
              "text/calendar"
            )
)
and (
  (
    profile.by_sender_email().prevalence in ("new", "outlier")
    and not profile.by_sender_email().solicited
  )
  or (
    profile.by_sender_email().any_messages_malicious_or_spam
    and not profile.by_sender_email().any_messages_benign
  )
  // mismatched reply-to with unsoliticed reply-to address
  or (
    length(headers.reply_to) > 0
    and (sender.email.email not in map(headers.reply_to, .email.email))
    // negate reply-to addresses that are within the org domain
    and not all(headers.reply_to,
                .email.domain.domain in $org_domains
                or .email.domain.root_domain in $org_domains
    )
    //
    // This rule makes use of a beta feature and is subject to change without notice
    // using the beta feature in custom rules is not suggested until it has been formally released
    //
    and not beta.profile.by_reply_to().solicited
    and beta.profile.by_reply_to().prevalence != "common"
  )
)
and (
  (
    sender.email.domain.root_domain in $org_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $org_domains
)
and not profile.by_sender_email().any_messages_benign

Detection Logic

Scope: inbound message.

Sender display name matches the display name of a user in the $org_vips list, and the sender has never been seen before. The $org_vips list must first be manually connected to a VIP group of your upstream provider (Google Workspace and Microsoft 365 only) in order for this rule to work. Once connected, the list will be automatically synced and kept up-to-date. For more information, see the $org_vips documentation: https://docs.sublimesecurity.com/docs/configure-org_vips-list This rule is recommended to be used on a relatively small list of VIPs, and is meant to reduce attack surface by detecting any message that matches the protected list of display names from a first-time or unsolicited sender. Additional rule logic can be added to look for suspicious subjects, suspicious links, etc.

  1. inbound message
  2. any of:
    • any($org_vips, .display_name =~ sender.display_name)
    • all of:
      • 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) )
      • any(ml.nlu_classifier(body.current_thread.text).intents, .name == "bec" and .confidence == "high" )
  3. any of:
    • length(recipients.to) > 0
    • length(recipients.cc) > 0
    • sender.display_name is not mailbox.display_name
  4. not:
    • sender.email.local_part matches any of 3 patterns
      • *postmaster*
      • *mailer-daemon*
      • *administrator*
  5. not:
    • any of attachments where:
      • .content_type in ('message/rfc822', 'message/delivery-status', 'text/calendar')
  6. any of:
    • all of:
      • profile.by_sender_email().prevalence in ("new", "outlier")
      • not:
        • profile.by_sender_email().solicited
    • all of:
      • profile.by_sender_email().any_messages_malicious_or_spam
      • not:
        • profile.by_sender_email().any_messages_benign
    • all of:
      • length(headers.reply_to) > 0
      • sender.email.email not in map(headers.reply_to, .email.email)
      • not:
        • all of headers.reply_to where any holds:
          • .email.domain.domain in $org_domains
          • .email.domain.root_domain in $org_domains
      • not:
        • beta.profile.by_reply_to().solicited
      • beta.profile.by_reply_to().prevalence != "common"
  7. any of:
    • all of:
      • sender.email.domain.root_domain in $org_domains
      • not:
        • headers.auth_summary.dmarc.pass
    • sender.email.domain.root_domain not in $org_domains
  8. not:
    • profile.by_sender_email().any_messages_benign

Inspects: attachments[].content_type, body.current_thread.text, headers.auth_summary.dmarc.pass, headers.reply_to, headers.reply_to[].email.domain.domain, headers.reply_to[].email.domain.root_domain, headers.reply_to[].email.email, mailbox.display_name, recipients.cc, recipients.to, sender.display_name, sender.email.domain.root_domain, sender.email.email, sender.email.local_part, type.inbound. Sensors: beta.profile.by_reply_to, ml.nlu_classifier, profile.by_sender_email, strings.istarts_with, strings.like. Reference lists: $org_domains, $org_vips.

Indicators Matched (2)

FieldMatchValue
ml.nlu_classifier(body.current_thread.text).intents[].nameequalsbec
ml.nlu_classifier(body.current_thread.text).intents[].confidenceequalshigh

Stages and Predicates

Stage 1: mql_rule

and
  or
    and
      not
        beta.profile.by_reply_to func_call "beta.profile.by_reply_to().solicited"
      not
         macro "all(headers.reply_to,\n                .email.domain.domain in $org_domains\n                or .email.domain.root_domain in $org_domains\n    )"
      beta.profile.by_reply_to func_call "beta.profile.by_reply_to().prevalence != \"common\""
      headers.reply_to length_compare "0"
      sender.email.email macro "sender.email.email not in map(headers.reply_to, .email.email)"
    and
      not
        profile.by_sender_email func_call "profile.by_sender_email().any_messages_benign"
      profile.by_sender_email func_call "profile.by_sender_email().any_messages_malicious_or_spam"
    and
      not
        profile.by_sender_email func_call "profile.by_sender_email().solicited"
      profile.by_sender_email func_call "profile.by_sender_email().prevalence in (\"new\", \"outlier\")"
  or
    and
      not
        headers.auth_summary.dmarc.pass eq "true"
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
    sender.email.domain.root_domain macro "sender.email.domain.root_domain not in org_domains"
  not
    any(attachments)
      attachments.content_type in ["message/delivery-status", "message/rfc822", "text/calendar"]
  not
    or
      sender.email.local_part match "administrator"
      sender.email.local_part match "mailer-daemon"
      sender.email.local_part match "postmaster"
  or
    and
       macro "any($org_vips,\n        strings.istarts_with(sender.display_name, .display_name)\n        // and it is longer than just their name (eg. John Doe CEO)\n        and length(sender.display_name) > length(.display_name)\n    )"
       macro "any(ml.nlu_classifier(body.current_thread.text).intents,\n            .name == \"bec\" and .confidence == \"high\"\n    )"
     macro "any($org_vips, .display_name =~ sender.display_name)"
  not
    profile.by_sender_email func_call "profile.by_sender_email().any_messages_benign"
  or
    recipients.cc length_compare "0"
    recipients.to length_compare "0"
    sender.display_name cross_field_compare "mailbox.display_name"
  type.inbound eq "true"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
attachmentsarray_any(no value, null check)excludes:attachments
sender.email.local_partmatchadministratorexcludes:sender.email.local_part field:"sender.email.local_part" value:"administrator"
sender.email.local_partmatchmailer-daemonexcludes:sender.email.local_part field:"sender.email.local_part" value:"mailer-daemon"
sender.email.local_partmatchpostmasterexcludes:sender.email.local_part field:"sender.email.local_part" value:"postmaster"

VIP / Executive impersonation in subject (untrusted)

#
Severity
medium
Type
rule
Source
github.com/sublime-security/sublime-rules

Sender subject contains the display name of a user in the $org_vips list, and the sender has never been seen before. The $org_vips list must first be manually connected to a VIP group of your upstream provider (Google Workspace and Microsoft 365 only) in order for this rule to work. Once connected, the list will be automatically synced and kept up-to-date. For more information, see the $org_vips documentation: https://docs.sublimesecurity.com/docs/configure-org_vips-list This rule is recommended to be used on a relatively small list of VIPs, and is meant to reduce attack surface by detecting any message that matches the protected list of display names from a first-time or unsolicited sender. Additional rule logic can be added to look for suspicious subjects, suspicious links, etc.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • headers
  • headers.auth_summary
  • mailbox
  • recipients
  • recipients.to
  • sender.email
  • subject
  • type

Rule Body

type.inbound
and any($org_vips,
        strings.contains(subject.subject, .display_name)
        and strings.contains(.display_name, " ")
)
// not being sent to said VIP
and not (
  (
    length(recipients.to) == 1
    and all(recipients.to,
            any($org_vips,
                .email == ..email.email
                and strings.contains(subject.subject, .display_name)
                and strings.contains(.display_name, " ")
            )
    )
  )
)
and (
  // ignore personal <> work emails
  // where the sender and mailbox's display name are the same
  length(recipients.to) > 0
  or length(recipients.cc) > 0
  or sender.display_name != mailbox.display_name
)
// bounce-back negations
and not strings.like(sender.email.local_part,
                     "*postmaster*",
                     "*mailer-daemon*",
                     "*administrator*"
)
and not any(attachments,
            .content_type in (
              "message/rfc822",
              "message/delivery-status",
              "text/calendar"
            )
)
and (
  (
    profile.by_sender().prevalence in ("new", "outlier")
    and not profile.by_sender().solicited
  )
  or (
    profile.by_sender().any_messages_malicious_or_spam
    and not profile.by_sender().any_messages_benign
  )
)

// negate org domains unless they fail DMARC authentication
and (
  (
    sender.email.domain.root_domain in $org_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $org_domains
)

// negate highly trusted sender domains unless they fail DMARC authentication
and (
  (
    sender.email.domain.root_domain in $high_trust_sender_root_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $high_trust_sender_root_domains
)

Detection Logic

Scope: inbound message.

Sender subject contains the display name of a user in the $org_vips list, and the sender has never been seen before. The $org_vips list must first be manually connected to a VIP group of your upstream provider (Google Workspace and Microsoft 365 only) in order for this rule to work. Once connected, the list will be automatically synced and kept up-to-date. For more information, see the $org_vips documentation: https://docs.sublimesecurity.com/docs/configure-org_vips-list This rule is recommended to be used on a relatively small list of VIPs, and is meant to reduce attack surface by detecting any message that matches the protected list of display names from a first-time or unsolicited sender. Additional rule logic can be added to look for suspicious subjects, suspicious links, etc.

  1. inbound message
  2. any($org_vips, strings.contains(subject.subject, .display_name) and strings.contains(.display_name, " ") )
  3. not:
    • all of:
      • length(recipients.to) == 1
      • all of recipients.to where:
        • any($org_vips, .email == ..email.email and strings.contains(subject.subject, .display_name) and strings.contains(.display_name, " ") )
  4. any of:
    • length(recipients.to) > 0
    • length(recipients.cc) > 0
    • sender.display_name is not mailbox.display_name
  5. not:
    • sender.email.local_part matches any of 3 patterns
      • *postmaster*
      • *mailer-daemon*
      • *administrator*
  6. not:
    • any of attachments where:
      • .content_type in ('message/rfc822', 'message/delivery-status', 'text/calendar')
  7. any of:
    • all of:
      • profile.by_sender().prevalence in ("new", "outlier")
      • not:
        • profile.by_sender().solicited
    • all of:
      • profile.by_sender().any_messages_malicious_or_spam
      • not:
        • profile.by_sender().any_messages_benign
  8. any of:
    • all of:
      • sender.email.domain.root_domain in $org_domains
      • not:
        • headers.auth_summary.dmarc.pass
    • sender.email.domain.root_domain not in $org_domains
  9. any of:
    • all of:
      • sender.email.domain.root_domain in $high_trust_sender_root_domains
      • not:
        • headers.auth_summary.dmarc.pass
    • sender.email.domain.root_domain not in $high_trust_sender_root_domains

Inspects: attachments[].content_type, headers.auth_summary.dmarc.pass, mailbox.display_name, recipients.cc, recipients.to, recipients.to[].email.email, sender.display_name, sender.email.domain.root_domain, sender.email.local_part, subject.subject, type.inbound. Sensors: profile.by_sender, strings.contains, strings.like. Reference lists: $high_trust_sender_root_domains, $org_domains, $org_vips.

Indicators Matched (1)

FieldMatchValue
strings.containssubstring

Stages and Predicates

Stage 1: mql_rule

and
  or
    and
      not
        headers.auth_summary.dmarc.pass eq "true"
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in high_trust_sender_root_domains"
    sender.email.domain.root_domain macro "sender.email.domain.root_domain not in high_trust_sender_root_domains"
  or
    and
      not
        headers.auth_summary.dmarc.pass eq "true"
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
    sender.email.domain.root_domain macro "sender.email.domain.root_domain not in org_domains"
  or
    and
      not
        profile.by_sender func_call "profile.by_sender().any_messages_benign"
      profile.by_sender func_call "profile.by_sender().any_messages_malicious_or_spam"
    and
      not
        profile.by_sender func_call "profile.by_sender().solicited"
      profile.by_sender func_call "profile.by_sender().prevalence in (\"new\", \"outlier\")"
  not
    any(attachments)
      attachments.content_type in ["message/delivery-status", "message/rfc822", "text/calendar"]
  not
    and
      recipients.to length_compare "1"
       macro "all(recipients.to,\n            any($org_vips,\n                .email == ..email.email\n                and strings.contains(subject.subject, .display_name)\n                and strings.contains(.display_name, \" \")\n            )\n    )"
  not
    or
      sender.email.local_part match "administrator"
      sender.email.local_part match "mailer-daemon"
      sender.email.local_part match "postmaster"
  or
    recipients.cc length_compare "0"
    recipients.to length_compare "0"
    sender.display_name cross_field_compare "mailbox.display_name"
  type.inbound eq "true"
   macro "any($org_vips,\n        strings.contains(subject.subject, .display_name)\n        and strings.contains(.display_name, \" \")\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
attachmentsarray_any(no value, null check)excludes:attachments
recipients.tolength_compare1excludes:recipients.to field:"recipients.to" value:"1"
sender.email.local_partmatchadministratorexcludes:sender.email.local_part field:"sender.email.local_part" value:"administrator"
sender.email.local_partmatchmailer-daemonexcludes:sender.email.local_part field:"sender.email.local_part" value:"mailer-daemon"
sender.email.local_partmatchpostmasterexcludes:sender.email.local_part field:"sender.email.local_part" value:"postmaster"

VIP impersonation with charitable donation fraud

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Fake email thread shows a VIP requesting a donation to a charity, usually addressed to Accounts Payable departments. Can result in monetary loss.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: Employee, Impersonation: VIP, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.current_thread
  • body.html
  • body.plain
  • headers
  • subject
  • type

Rule Body

type.inbound
and strings.ilike(body.current_thread.text,
                  "*charity*",
                  "*gala*",
                  "*donation*",
                  "*donor*"
)
and any(ml.nlu_classifier(body.current_thread.text).entities,
        .name == "financial"
)
and any(ml.nlu_classifier(body.current_thread.text).entities,
        .name == "request"
)
and (
  any($org_vips, strings.icontains(body.html.inner_text, .display_name))
  or any($org_vips, strings.icontains(body.plain.raw, .display_name))
)
and (
  (
    (subject.is_forward or subject.is_reply)
    and (
      (length(headers.references) == 0 and headers.in_reply_to is null)
      or headers.in_reply_to is null
    )
  )
  // fake thread, but no indication in the subject line
  // current_thread pulls the recent thread, but the full body contains the fake "original" email
  or (
    not ((subject.is_forward or subject.is_reply))
    and (
      3 of (
        strings.icontains(body.html.display_text, "from:"),
        strings.icontains(body.html.display_text, "to:"),
        strings.icontains(body.html.display_text, "sent:"),
        strings.icontains(body.html.display_text, "subject:")
      )
      or length(body.previous_threads) > 0
    )
    and (
      length(body.current_thread.text) + 100 < length(body.html.display_text)
    )
    // negating bouncebacks
    and not any(attachments,
                .content_type in ("message/delivery-status", "message/rfc822")
    )
  )
)
and (
  profile.by_sender().prevalence in ("new", "rare")
  or profile.by_sender().days_known > 30
)
and not profile.by_sender().any_messages_benign

Detection Logic

Scope: inbound message.

Fake email thread shows a VIP requesting a donation to a charity, usually addressed to Accounts Payable departments. Can result in monetary loss.

  1. inbound message
  2. body.current_thread.text matches any of 4 patterns
    • *charity*
    • *gala*
    • *donation*
    • *donor*
  3. any(ml.nlu_classifier(body.current_thread.text).entities, .name == "financial" )
  4. any(ml.nlu_classifier(body.current_thread.text).entities, .name == "request" )
  5. any of:
    • any($org_vips, strings.icontains(body.html.inner_text, .display_name))
    • any($org_vips, strings.icontains(body.plain.raw, .display_name))
  6. any of:
    • all of:
      • any of:
        • subject.is_forward
        • subject.is_reply
      • any of:
        • all of:
          • length(headers.references) == 0
          • headers.in_reply_to is missing
        • headers.in_reply_to is missing
    • all of:
      • none of:
        • subject.is_forward
        • subject.is_reply
      • any of:
        • at least 3 of 4: body.html.display_text contains any of 4 patterns
          • from:
          • to:
          • sent:
          • subject:
        • length(body.previous_threads) > 0
      • ( length(body.current_thread.text) + 100 < length(body.html.display_text) )
      • not:
        • any of attachments where:
          • .content_type in ('message/delivery-status', 'message/rfc822')
  7. any of:
    • profile.by_sender().prevalence in ("new", "rare")
    • profile.by_sender().days_known > 30
  8. not:
    • profile.by_sender().any_messages_benign

Inspects: attachments[].content_type, body.current_thread.text, body.html.display_text, body.html.inner_text, body.plain.raw, body.previous_threads, headers.in_reply_to, headers.references, subject.is_forward, subject.is_reply, type.inbound. Sensors: ml.nlu_classifier, profile.by_sender, strings.icontains, strings.ilike. Reference lists: $org_vips.

Indicators Matched (10)

FieldMatchValue
strings.ilikesubstring*charity*
strings.ilikesubstring*gala*
strings.ilikesubstring*donation*
strings.ilikesubstring*donor*
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsfinancial
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsrequest
strings.icontainssubstringfrom:
strings.icontainssubstringto:
strings.icontainssubstringsent:
strings.icontainssubstringsubject:

Stages and Predicates

Stage 1: mql_rule

and
  or
    and
      not
        any(attachments)
          attachments.content_type in ["message/delivery-status", "message/rfc822"]
      or
        and
          body.html.display_text contains "from:"
          body.html.display_text contains "sent:"
          body.html.display_text contains "subject:"
        and
          body.html.display_text contains "from:"
          body.html.display_text contains "sent:"
          body.html.display_text contains "to:"
        and
          body.html.display_text contains "from:"
          body.html.display_text contains "subject:"
          body.html.display_text contains "to:"
        and
          body.html.display_text contains "sent:"
          body.html.display_text contains "subject:"
          body.html.display_text contains "to:"
        body.previous_threads length_compare "0"
      not
        or
          subject.is_forward eq "true"
          subject.is_reply eq "true"
       macro "(\n      length(body.current_thread.text) + 100 < length(body.html.display_text)\n    )"
    and
      or
        and
          headers.in_reply_to is_null
          headers.references length_compare "0"
        headers.in_reply_to is_null
      or
        subject.is_forward eq "true"
        subject.is_reply eq "true"
  or
    body.current_thread.text match "charity"
    body.current_thread.text match "donation"
    body.current_thread.text match "donor"
    body.current_thread.text match "gala"
  not
    profile.by_sender func_call "profile.by_sender().any_messages_benign"
  or
    profile.by_sender func_call "profile.by_sender().days_known > 30"
    profile.by_sender func_call "profile.by_sender().prevalence in (\"new\", \"rare\")"
  or
     macro "any($org_vips, strings.icontains(body.html.inner_text, .display_name))"
     macro "any($org_vips, strings.icontains(body.plain.raw, .display_name))"
  type.inbound eq "true"
   macro "any(ml.nlu_classifier(body.current_thread.text).entities,\n        .name == \"financial\"\n)"
   macro "any(ml.nlu_classifier(body.current_thread.text).entities,\n        .name == \"request\"\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
body.current_thread.textwildcard
  • *charity*
  • *donation*
  • *donor*
  • *gala*
field:"body.current_thread.text" kind:wildcard
body.html.display_textcontains
  • from:
  • sent:
  • subject:
  • to:
field:"body.html.display_text" kind:contains
headers.in_reply_tois_null
  • (no value, null check)
field:"headers.in_reply_to" kind:is_null
subject.is_forwardeq
  • true transforms: boolean corpus 8 (mql 8)
field:"subject.is_forward" kind:eq value:"true"
subject.is_replyeq
  • true transforms: boolean corpus 11 (mql 11)
field:"subject.is_reply" kind:eq value:"true"
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: Fabricated thread history with fake VIP recipients

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages that contain forged prior thread histories where the fake headers use abnormal spacing around colons - a hallmark of programmatically generated preambles. The fabricated threads reference VIP recipients from the organization who are absent from the live message's actual recipients, suggesting the thread was constructed to manufacture legitimacy. Observed messages impersonate finance or accounts payable workflows, referencing overdue invoices, balance statements, and payment requests targeting real vendors and internal stakeholders.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Evasion, Spoofing

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.previous_threads
  • recipients
  • recipients.cc
  • recipients.to
  • sender.email
  • type

Rule Body

type.inbound
and any(filter(body.previous_threads,
               // single recipient in the previous thread
               length(.recipients.to) == 1
               and length(.recipients.cc) == 0
               // contains spaces around the colons of the "headers"
               and strings.count(.preamble, ' : ') >= 2
               and regex.icount(.preamble, '(?m)^\s*[a-z]+ +: +\S') == regex.icount(.preamble,
                                                                                    '(?m)^.'
               )
        ),
        // the previous thread with the goofy spaces
        // includes a VIP as a recipient
        any(map(filter(.recipients.to,
                       // via email
                       .email.email != ""
                       and any($org_vips,
                               strings.icontains(..email.email, .email)
                               or strings.icontains(..display_name,
                                                    .display_name
                               )
                       )
                ),
                .email.email
            ),
            // email is not in the "live" messages
            not strings.icontains(sender.email.email, .)
            and not any(recipients.to, strings.icontains(.email.email, ..))
            and not any(recipients.cc, strings.icontains(.email.email, ..))
        )
        or any(map(filter(.recipients.to,
                          // via display_name
                          .email.email == ""
                          and any($org_vips,
                                  strings.icontains(..display_name,
                                                    .display_name
                                  )
                          )
                   ),
                   .display_name
               ),
               // display name is not in the "live" messages
               not any(recipients.to, .display_name == ..)
               and not any(recipients.cc, .display_name == ..)
               and sender.display_name != .
        )
)

Detection Logic

Scope: inbound message.

Detects inbound messages that contain forged prior thread histories where the fake headers use abnormal spacing around colons - a hallmark of programmatically generated preambles. The fabricated threads reference VIP recipients from the organization who are absent from the live message's actual recipients, suggesting the thread was constructed to manufacture legitimacy. Observed messages impersonate finance or accounts payable workflows, referencing overdue invoices, balance statements, and payment requests targeting real vendors and internal stakeholders.

  1. inbound message
  2. any(filter(body.previous_threads, // single recipient in the previous thread length(.recipients.to) == 1 and length(.recipients.cc) == 0 // contains spaces around the colons of the "headers" and strings.count(.preamble, ' : ') >= 2 and regex.icount(.preamble, '(?m)^\s*[a-z]+ +: +\S') == regex.icount(.preamble, '(?m)^.' ) ), // the previous thread with the goofy spaces // includes a VIP as a recipient any(map(filter(.recipients.to, // via email .email.email != "" and any($org_vips, strings.icontains(..email.email, .email) or strings.icontains(..display_name, .display_name ) ) ), .email.email ), // email is not in the "live" messages not strings.icontains(sender.email.email, .) and not any(recipients.to, strings.icontains(.email.email, ..)) and not any(recipients.cc, strings.icontains(.email.email, ..)) ) or any(map(filter(.recipients.to, // via display_name .email.email == "" and any($org_vips, strings.icontains(..display_name, .display_name ) ) ), .display_name ), // display name is not in the "live" messages not any(recipients.to, .display_name == ..) and not any(recipients.cc, .display_name == ..) and sender.display_name != . ) )

Inspects: body.previous_threads, body.previous_threads[].preamble, body.previous_threads[].recipients.cc, body.previous_threads[].recipients.to, recipients.cc, recipients.cc[].display_name, recipients.cc[].email.email, recipients.to, recipients.to[].display_name, recipients.to[].email.email, sender.display_name, sender.email.email, type.inbound. Sensors: regex.icount, strings.count, strings.icontains. Reference lists: $org_vips.

Indicators Matched (3)

FieldMatchValue
regex.icountregex(?m)^\s*[a-z]+ +: +\S
regex.icountregex(?m)^.
filter(body.previous_threads)[].recipients.to[].email.emailequals

Stages and Predicates

Stage 1: mql_rule

and
  type.inbound eq "true"
   macro "any(filter(body.previous_threads,\n               // single recipient in the previous thread\n               length(.recipients.to) == 1\n               and length(.recipients.cc) == 0\n               // contains spaces around the colons of the \"headers\"\n               and strings.count(.preamble, ' : ') >= 2\n               and regex.icount(.preamble, '(?m)^\\s*[a-z]+ +: +\\S') == regex.icount(.preamble,\n                                                                                    '(?m)^.'\n               )\n        ),\n        // the previous thread with the goofy spaces\n        // includes a VIP as a recipient\n        any(map(filter(.recipients.to,\n                       // via email\n                       .email.email != \"\"\n                       and any($org_vips,\n                               strings.icontains(..email.email, .email)\n                               or strings.icontains(..display_name,\n                                                    .display_name\n                               )\n                       )\n                ),\n                .email.email\n            ),\n            // email is not in the \"live\" messages\n            not strings.icontains(sender.email.email, .)\n            and not any(recipients.to, strings.icontains(.email.email, ..))\n            and not any(recipients.cc, strings.icontains(.email.email, ..))\n        )\n        or any(map(filter(.recipients.to,\n                          // via display_name\n                          .email.email == \"\"\n                          and any($org_vips,\n                                  strings.icontains(..display_name,\n                                                    .display_name\n                                  )\n                          )\n                   ),\n                   .display_name\n               ),\n               // display name is not in the \"live\" messages\n               not any(recipients.to, .display_name == ..)\n               and not any(recipients.cc, .display_name == ..)\n               and sender.display_name != .\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: Fake forwarded indicator with VIP recipient impersonation

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages that fabricate a forwarded email thread — using patterns like '=== Forwarded Message:' — where a prior thread appears to involve an organizational VIP as a recipient. The messages observed follow an invoice lure pattern referencing admired leadership figures by name, designed to create the appearance of legitimacy by spoofing internal executive communication chains.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Evasion

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.current_thread
  • body.html
  • body.plain
  • body.previous_threads
  • type

Rule Body

type.inbound
and length(coalesce(body.plain.raw, body.html.display_text)) < 6000
and (
  // the current thread contains a goofy forwarded message
  (
    strings.icontains(body.current_thread.text, 'forwarded')
    // not this specific format
    and not strings.icontains(body.current_thread.text,
                              '============ Forwarded Message ============'
    )
    and regex.icontains(body.current_thread.text,
                        '(?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded',
                        '={3,}\s*(?:forwarded message|message that has been forwarded)',
                        '[\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)'
    )
  )
  // any previous thread contains a goofy forwarded message
  or any(body.previous_threads,
         strings.icontains(.text, 'forwarded')
         // not this specific format
         and not strings.icontains(.text,
                                   '============ Forwarded Message ============'
         )
         and regex.icontains(.text,
                             '(?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded',
                             '={3,}\s*(?:forwarded message|message that has been forwarded)',
                             '[\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)'
         )
  )
)
// any of the previous threads were sent to a VIP
and any(body.previous_threads,
        any(.recipients.to,
            any($org_vips,
                strings.icontains(..display_name, .display_name)
                or (.email != "" and strings.icontains(..email.email, .email))
            )
        )
)

Detection Logic

Scope: inbound message.

Detects inbound messages that fabricate a forwarded email thread — using patterns like '=== Forwarded Message:' — where a prior thread appears to involve an organizational VIP as a recipient. The messages observed follow an invoice lure pattern referencing admired leadership figures by name, designed to create the appearance of legitimacy by spoofing internal executive communication chains.

  1. inbound message
  2. length(coalesce(body.plain.raw, body.html.display_text)) < 6000
  3. any of:
    • all of:
      • body.current_thread.text contains 'forwarded'
      • not:
        • body.current_thread.text contains '============ Forwarded Message ============'
      • body.current_thread.text matches any of 3 patterns
        • (?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded
        • ={3,}\s*(?:forwarded message|message that has been forwarded)
        • [\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)
    • any of body.previous_threads where all hold:
      • .text contains 'forwarded'
      • not:
        • .text contains '============ Forwarded Message ============'
      • .text matches any of 3 patterns
        • (?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded
        • ={3,}\s*(?:forwarded message|message that has been forwarded)
        • [\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)
  4. any of body.previous_threads where:
    • any of .recipients.to where:
      • any($org_vips, strings.icontains(..display_name, .display_name) or (.email != "" and strings.icontains(..email.email, .email)) )

Inspects: body.current_thread.text, body.html.display_text, body.plain.raw, body.previous_threads, body.previous_threads[].recipients.to, body.previous_threads[].recipients.to[].display_name, body.previous_threads[].recipients.to[].email.email, body.previous_threads[].text, type.inbound. Sensors: regex.icontains, strings.icontains. Reference lists: $org_vips.

Indicators Matched (4)

FieldMatchValue
strings.icontainssubstringforwarded
regex.icontainsregex(?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded
regex.icontainsregex={3,}\s*(?:forwarded message|message that has been forwarded)
regex.icontainsregex[\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)

Stages and Predicates

Stage 1: mql_rule

and
  or
    any(body.previous_threads)
      and
        not
          body.previous_threads.text contains "============ Forwarded Message ============"
        or
          body.previous_threads.text regex_match "(?m)^[=\\x{2012}\\x{2013}\\x{2014}\\x{2015}\\s]*message that has been forwarded"
          body.previous_threads.text regex_match "={3,}\\s*(?:forwarded message|message that has been forwarded)"
          body.previous_threads.text regex_match "[\\x{2013}]{2,}\\s*(?:forwarded message|message that has been forwarded)"
        body.previous_threads.text contains "forwarded"
    and
      not
        body.current_thread.text contains "============ Forwarded Message ============"
      or
        body.current_thread.text regex_match "(?m)^[=\\x{2012}\\x{2013}\\x{2014}\\x{2015}\\s]*message that has been forwarded"
        body.current_thread.text regex_match "={3,}\\s*(?:forwarded message|message that has been forwarded)"
        body.current_thread.text regex_match "[\\x{2013}]{2,}\\s*(?:forwarded message|message that has been forwarded)"
      body.current_thread.text contains "forwarded"
  any(body.previous_threads)
    any(body.previous_threads.recipients.to)
       macro "any($org_vips,\n                strings.icontains(..display_name, .display_name)\n                or (.email != \"\" and strings.icontains(..email.email, .email))\n            )"
  length func_call "length(coalesce(body.plain.raw, body.html.display_text)) < 6000"
  type.inbound eq "true"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
body.current_thread.textcontains
  • forwarded
field:"body.current_thread.text" kind:contains value:"forwarded"
body.current_thread.textregex_match
  • (?m)^[=\x{2012}\x{2013}\x{2014}\x{2015}\s]*message that has been forwarded
  • ={3,}\s*(?:forwarded message|message that has been forwarded)
  • [\x{2013}]{2,}\s*(?:forwarded message|message that has been forwarded)
field:"body.current_thread.text" kind:regex_match
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: Fake thread with display name match, email mismatch

#
Severity
medium
Type
rule
Source
github.com/sublime-security/sublime-rules

This rule is intended to detect fake threads that are impersonating a VIP. It looks for a matching $org_vips display name and checks the email address following it does not match what is in the $org_vips list.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesEvasion, Impersonation: VIP, Social engineering, Spoofing

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.current_thread
  • body.html
  • body.plain
  • headers
  • type

Rule Body

type.inbound
and any($org_vips,
        strings.icontains(body.html.display_text,
                          strings.concat("From: ", .display_name, " <")
        )
        and not strings.icontains(body.html.display_text,
                                  strings.concat("From: ",
                                                 .display_name,
                                                 " <",
                                                 .email,
                                                 ">"
                                  )
        )
)
and any([body.current_thread.text, body.html.display_text, body.plain.raw],
        3 of (
          strings.icontains(., "from:"),
          strings.icontains(., "to:"),
          strings.icontains(., "sent:"),
          strings.icontains(., "date:"),
          strings.icontains(., "cc:"),
          strings.icontains(., "subject:")
        )
)
and (length(headers.references) == 0 or headers.in_reply_to is null)

Detection Logic

Scope: inbound message.

This rule is intended to detect fake threads that are impersonating a VIP. It looks for a matching $org_vips display name and checks the email address following it does not match what is in the $org_vips list.

  1. inbound message
  2. any($org_vips, strings.icontains(body.html.display_text, strings.concat("From: ", .display_name, " <") ) and not strings.icontains(body.html.display_text, strings.concat("From: ", .display_name, " <", .email, ">" ) ) )
  3. any([body.current_thread.text, body.html.display_text, body.plain.raw], 3 of ( strings.icontains(., "from:"), strings.icontains(., "to:"), strings.icontains(., "sent:"), strings.icontains(., "date:"), strings.icontains(., "cc:"), strings.icontains(., "subject:") ) )
  4. any of:
    • length(headers.references) == 0
    • headers.in_reply_to is missing

Inspects: body.current_thread.text, body.html.display_text, body.plain.raw, headers.in_reply_to, headers.references, type.inbound. Sensors: strings.concat, strings.icontains. Reference lists: $org_vips.

Indicators Matched (6)

FieldMatchValue
strings.icontainssubstringfrom:
strings.icontainssubstringto:
strings.icontainssubstringsent:
strings.icontainssubstringdate:
strings.icontainssubstringcc:
strings.icontainssubstringsubject:

Stages and Predicates

Stage 1: mql_rule

and
  or
    headers.in_reply_to is_null
    headers.references length_compare "0"
  type.inbound eq "true"
   macro "any($org_vips,\n        strings.icontains(body.html.display_text,\n                          strings.concat(\"From: \", .display_name, \" <\")\n        )\n        and not strings.icontains(body.html.display_text,\n                                  strings.concat(\"From: \",\n                                                 .display_name,\n                                                 \" <\",\n                                                 .email,\n                                                 \">\"\n                                  )\n        )\n)"
   macro "any([body.current_thread.text, body.html.display_text, body.plain.raw],\n        3 of (\n          strings.icontains(., \"from:\"),\n          strings.icontains(., \"to:\"),\n          strings.icontains(., \"sent:\"),\n          strings.icontains(., \"date:\"),\n          strings.icontains(., \"cc:\"),\n          strings.icontains(., \"subject:\")\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
headers.in_reply_tois_null
  • (no value, null check)
field:"headers.in_reply_to" kind:is_null
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: Fake thread with VIPs missing email metadata

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages that weaponize fabricated invoice or payment thread histories to impersonate or involve organizational VIPs. The rule identifies conversations where the two oldest visible threads discuss invoices, payments, or executive engagements, but contain incomplete sender or recipient email addresses — a hallmark of stitched-together or forged thread context. The targeted VIP appears in the fabricated thread history but is conspicuously absent from the current message's recipients, suggesting the VIP's name is being leveraged to establish false legitimacy while routing the live message away from their oversight. Matched messages span executive search retainer invoices, past-due account notices, wire transfer instructions, and advisory billing lures targeting named executives at known organizations.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Spoofing

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.previous_threads
  • headers
  • headers.auth_summary
  • recipients
  • sender.email
  • type

Rule Body

type.inbound
// we need at least two threads
and length(body.previous_threads) > 1
and (
  // NLU on previous threads
  ( // oldest thread is invoice/payment related
    (
      any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text,
                            subject=body.previous_threads[length(body.previous_threads
                            ) - 1].subject.base
          ).tags,
          .name in ("invoice", "payment") and .confidence != "low"
      )
      or any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads
                               ) - 1].text,
                               subject=body.previous_threads[length(body.previous_threads
                               ) - 1].subject.base
             ).topics,
             .name in ("Request to View Invoice", "Payment Information")
             and .confidence != "low"
      )
    )
    // second to oldest thread is invoice/payment related
    and (
      any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text,
                            subject=body.previous_threads[length(body.previous_threads
                            ) - 2].subject.base
          ).tags,
          .name in ("invoice", "payment") and .confidence != "low"
      )
      or any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads
                               ) - 2].text,
                               subject=body.previous_threads[length(body.previous_threads
                               ) - 2].subject.base
             ).topics,
             .name in ("Request to View Invoice", "Payment Information")
             and .confidence != "low"
      )
    )
  )
  // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related
  or any(attachments, strings.istarts_with(.file_name, 'INV', "W-9", 'W9'))
)
// each of the two oldest threads have 1 or less recipients (sometimes the "to" header is missing)
and length(body.previous_threads[length(body.previous_threads) - 1].recipients.to
) <= 1
and length(body.previous_threads[length(body.previous_threads) - 2].recipients.to
) <= 1
// at least one party in the two oldest threads is missing an email
and (
  body.previous_threads[length(body.previous_threads) - 1].sender.email.email == ""
  or body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].email.email == ""
  or body.previous_threads[length(body.previous_threads) - 2].sender.email.email == ""
  or body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].email.email == ""
)
// an org vip sent/received messages
and any($org_vips,
        (
          body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name
          and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name
        )
        or (
          body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name
          and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name
        )
)
// the VIP is no longer in the current message
and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
            any($org_vips,
                .email != ""
                and strings.icontains(..email.email, .email)
                and (
                  (
                    body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name
                    and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name
                  )
                  or (
                    body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name
                    and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name
                  )
                )
            )
)
// not from authorized senders on the org's domain
and not (
  sender.email.domain.domain in $org_domains
  and coalesce(headers.auth_summary.dmarc.pass, false)
)

Detection Logic

Scope: inbound message.

Detects inbound messages that weaponize fabricated invoice or payment thread histories to impersonate or involve organizational VIPs. The rule identifies conversations where the two oldest visible threads discuss invoices, payments, or executive engagements, but contain incomplete sender or recipient email addresses — a hallmark of stitched-together or forged thread context. The targeted VIP appears in the fabricated thread history but is conspicuously absent from the current message's recipients, suggesting the VIP's name is being leveraged to establish false legitimacy while routing the live message away from their oversight. Matched messages span executive search retainer invoices, past-due account notices, wire transfer instructions, and advisory billing lures targeting named executives at known organizations.

  1. inbound message
  2. length(body.previous_threads) > 1
  3. any of:
    • all of:
      • any of:
        • any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text, subject=body.previous_threads[length(body.previous_threads ) - 1].subject.base ).tags, .name in ("invoice", "payment") and .confidence != "low" )
        • any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads ) - 1].text, subject=body.previous_threads[length(body.previous_threads ) - 1].subject.base ).topics, .name in ("Request to View Invoice", "Payment Information") and .confidence != "low" )
      • any of:
        • any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text, subject=body.previous_threads[length(body.previous_threads ) - 2].subject.base ).tags, .name in ("invoice", "payment") and .confidence != "low" )
        • any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads ) - 2].text, subject=body.previous_threads[length(body.previous_threads ) - 2].subject.base ).topics, .name in ("Request to View Invoice", "Payment Information") and .confidence != "low" )
    • any of attachments where:
      • .file_name starts with any of 3 patterns
        • INV
        • W-9
        • W9
  4. length(body.previous_threads[length(body.previous_threads) - 1].recipients.to ) <= 1
  5. length(body.previous_threads[length(body.previous_threads) - 2].recipients.to ) <= 1
  6. any of:
    • body.previous_threads[].sender.email.email is ''
    • body.previous_threads[].recipients.to[0].email.email is ''
    • body.previous_threads[].sender.email.email is ''
    • body.previous_threads[].recipients.to[0].email.email is ''
  7. any($org_vips, ( body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name ) or ( body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name ) )
  8. not:
    • any(flatten([recipients.to, recipients.cc, recipients.bcc]), any($org_vips, .email != "" and strings.icontains(..email.email, .email) and ( ( body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name ) or ( body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name ) ) ) )
  9. not:
    • all of:
      • sender.email.domain.domain in $org_domains
      • coalesce(headers.auth_summary.dmarc.pass, false)

Inspects: attachments[].file_name, body.previous_threads, body.previous_threads[].recipients.to, body.previous_threads[].recipients.to[0].display_name, body.previous_threads[].recipients.to[0].email.email, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, body.previous_threads[].subject.base, body.previous_threads[].text, headers.auth_summary.dmarc.pass, recipients.bcc, recipients.cc, recipients.to, sender.email.domain.domain, type.inbound. Sensors: ml.nlu_classifier, strings.icontains, strings.istarts_with. Reference lists: $org_domains, $org_vips.

Indicators Matched (13)

FieldMatchValue
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text, subject=body.previous_threads[length(body.previous_threads) - 1].subject.base).tags[].namememberinvoice
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text, subject=body.previous_threads[length(body.previous_threads) - 1].subject.base).tags[].namememberpayment
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text, subject=body.previous_threads[length(body.previous_threads) - 1].subject.base).topics[].namememberRequest to View Invoice
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text, subject=body.previous_threads[length(body.previous_threads) - 1].subject.base).topics[].namememberPayment Information
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text, subject=body.previous_threads[length(body.previous_threads) - 2].subject.base).tags[].namememberinvoice
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text, subject=body.previous_threads[length(body.previous_threads) - 2].subject.base).tags[].namememberpayment
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text, subject=body.previous_threads[length(body.previous_threads) - 2].subject.base).topics[].namememberRequest to View Invoice
ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text, subject=body.previous_threads[length(body.previous_threads) - 2].subject.base).topics[].namememberPayment Information
strings.istarts_withprefixINV
strings.istarts_withprefixW-9
strings.istarts_withprefixW9
body.previous_threads[].sender.email.emailequals
1 more
body.previous_threads[].recipients.to[0].email.emailequals

Stages and Predicates

Stage 1: mql_rule

and
  or
    any(attachments)
      or
        attachments.file_name starts_with "INV"
        attachments.file_name starts_with "W-9"
        attachments.file_name starts_with "W9"
    and
      or
         macro "any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 1].text,\n                            subject=body.previous_threads[length(body.previous_threads\n                            ) - 1].subject.base\n          ).tags,\n          .name in (\"invoice\", \"payment\") and .confidence != \"low\"\n      )"
         macro "any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads\n                               ) - 1].text,\n                               subject=body.previous_threads[length(body.previous_threads\n                               ) - 1].subject.base\n             ).topics,\n             .name in (\"Request to View Invoice\", \"Payment Information\")\n             and .confidence != \"low\"\n      )"
      or
         macro "any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads) - 2].text,\n                            subject=body.previous_threads[length(body.previous_threads\n                            ) - 2].subject.base\n          ).tags,\n          .name in (\"invoice\", \"payment\") and .confidence != \"low\"\n      )"
         macro "any(ml.nlu_classifier(body.previous_threads[length(body.previous_threads\n                               ) - 2].text,\n                               subject=body.previous_threads[length(body.previous_threads\n                               ) - 2].subject.base\n             ).topics,\n             .name in (\"Request to View Invoice\", \"Payment Information\")\n             and .confidence != \"low\"\n      )"
  not
    and
      headers.auth_summary.dmarc.pass eq "true"
      headers.auth_summary.dmarc.pass is_not_null
      sender.email.domain.domain macro "sender.email.domain.domain in org_domains"
  or
    body.previous_threads[].recipients.to[0].email.email eq ""
    body.previous_threads[].sender.email.email eq ""
  not
     macro "any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n            any($org_vips,\n                .email != \"\"\n                and strings.icontains(..email.email, .email)\n                and (\n                  (\n                    body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name\n                    and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name\n                  )\n                  or (\n                    body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name\n                    and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name\n                  )\n                )\n            )\n)"
  body.previous_threads length_compare "1"
  body.previous_threads[].recipients.to length_compare "1"
  type.inbound eq "true"
   macro "any($org_vips,\n        (\n          body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name == .display_name\n          and body.previous_threads[length(body.previous_threads) - 2].sender.display_name == .display_name\n        )\n        or (\n          body.previous_threads[length(body.previous_threads) - 1].sender.display_name == .display_name\n          and body.previous_threads[length(body.previous_threads) - 2].recipients.to[0].display_name == .display_name\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
headers.auth_summary.dmarc.passeqtrueexcludes:headers.auth_summary.dmarc.pass field:"headers.auth_summary.dmarc.pass" value:"true"
headers.auth_summary.dmarc.passis_not_null(no value, null check)excludes:headers.auth_summary.dmarc.pass

VIP impersonation: Invoice fraud with mobile device sign-off

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages where a prior thread contains a reply from a known VIP - identifiable by display name or email address and signed off with a 'Sent from my iPhone' or 'Sent from my iPad' footer. However that VIP has been silently dropped from the current message's recipients. This pattern is consistent with fraudulent invoice and payment request lures where an attacker impersonates an internal executive in a thread, then removes them before requesting payment action from the remaining recipients.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.previous_threads
  • recipients
  • sender.email
  • type

Rule Body

type.inbound
and any(map(filter(body.previous_threads,
                   any($org_vips,
                       strings.icontains(..sender.display_name, .display_name)
                       or (
                         .email != ""
                         and strings.icontains(..sender.email.email, .email)
                       )
                   )
                   and (
                     strings.iends_with(.text, "sent from my iphone.")
                     or strings.iends_with(.text, "sent from my ipad.")
                   )
                   and (
                     // any previous thread authored by the "VIP" has invoice/payment
                     any(ml.nlu_classifier(.text, subject=.subject.base).tags,
                         .name in ("invoice", "payment")
                         and .confidence != "low"
                     )
                     or any(ml.nlu_classifier(.text, subject=.subject.base).topics,
                            .name in (
                              "Request to View Invoice",
                              "Payment Information"
                            )
                            and .confidence != "low"
                     )

                     // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related
                     or any(attachments,
                            strings.istarts_with(.file_name, 'INV', "W-9", 'W9')
                     )
                   )
            ),
            .sender.email.email
        ),
        . == ""
        or (
          not strings.icontains(sender.email.email, .)
          and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
                      strings.icontains(.email.email, ..)
          )
        )
)

Detection Logic

Scope: inbound message.

Detects inbound messages where a prior thread contains a reply from a known VIP - identifiable by display name or email address and signed off with a 'Sent from my iPhone' or 'Sent from my iPad' footer. However that VIP has been silently dropped from the current message's recipients. This pattern is consistent with fraudulent invoice and payment request lures where an attacker impersonates an internal executive in a thread, then removes them before requesting payment action from the remaining recipients.

  1. inbound message
  2. any(map(filter(body.previous_threads, any($org_vips, strings.icontains(..sender.display_name, .display_name) or ( .email != "" and strings.icontains(..sender.email.email, .email) ) ) and ( strings.iends_with(.text, "sent from my iphone.") or strings.iends_with(.text, "sent from my ipad.") ) and ( // any previous thread authored by the "VIP" has invoice/payment any(ml.nlu_classifier(.text, subject=.subject.base).tags, .name in ("invoice", "payment") and .confidence != "low" ) or any(ml.nlu_classifier(.text, subject=.subject.base).topics, .name in ( "Request to View Invoice", "Payment Information" ) and .confidence != "low" ) // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related or any(attachments, strings.istarts_with(.file_name, 'INV', "W-9", 'W9') ) ) ), .sender.email.email ), . == "" or ( not strings.icontains(sender.email.email, .) and not any(flatten([recipients.to, recipients.cc, recipients.bcc]), strings.icontains(.email.email, ..) ) ) )

Inspects: attachments[].file_name, body.previous_threads, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, body.previous_threads[].subject.base, body.previous_threads[].text, recipients.bcc, recipients.cc, recipients.to, sender.email.email, type.inbound. Sensors: ml.nlu_classifier, strings.icontains, strings.iends_with, strings.istarts_with. Reference lists: $org_vips.

Indicators Matched (10)

FieldMatchValue
strings.iends_withsuffixsent from my iphone.
strings.iends_withsuffixsent from my ipad.
ml.nlu_classifier(body.previous_threads[].text, subject=body.previous_threads[].subject.base).tags[].namememberinvoice
ml.nlu_classifier(body.previous_threads[].text, subject=body.previous_threads[].subject.base).tags[].namememberpayment
ml.nlu_classifier(body.previous_threads[].text, subject=body.previous_threads[].subject.base).topics[].namememberRequest to View Invoice
ml.nlu_classifier(body.previous_threads[].text, subject=body.previous_threads[].subject.base).topics[].namememberPayment Information
strings.istarts_withprefixINV
strings.istarts_withprefixW-9
strings.istarts_withprefixW9
map(...)[]equals

Stages and Predicates

Stage 1: mql_rule

and
  type.inbound eq "true"
   macro "any(map(filter(body.previous_threads,\n                   any($org_vips,\n                       strings.icontains(..sender.display_name, .display_name)\n                       or (\n                         .email != \"\"\n                         and strings.icontains(..sender.email.email, .email)\n                       )\n                   )\n                   and (\n                     strings.iends_with(.text, \"sent from my iphone.\")\n                     or strings.iends_with(.text, \"sent from my ipad.\")\n                   )\n                   and (\n                     // any previous thread authored by the \"VIP\" has invoice/payment\n                     any(ml.nlu_classifier(.text, subject=.subject.base).tags,\n                         .name in (\"invoice\", \"payment\")\n                         and .confidence != \"low\"\n                     )\n                     or any(ml.nlu_classifier(.text, subject=.subject.base).topics,\n                            .name in (\n                              \"Request to View Invoice\",\n                              \"Payment Information\"\n                            )\n                            and .confidence != \"low\"\n                     )\n\n                     // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related\n                     or any(attachments,\n                            strings.istarts_with(.file_name, 'INV', \"W-9\", 'W9')\n                     )\n                   )\n            ),\n            .sender.email.email\n        ),\n        . == \"\"\n        or (\n          not strings.icontains(sender.email.email, .)\n          and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                      strings.icontains(.email.email, ..)\n          )\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: Payment handoff with VIP display name authored fake threads

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages where an external sender, absent from the original conversation, targets a single internal recipient after a VIP previously instructed that party to handle invoice or payment correspondence. The rule identifies prior threads where a known VIP explicitly redirected billing or payment contact to an internal address — using language such as forwarding instructions, accounts payable references, or billing handoff phrasing — and flags follow-up messages from external senders who are no longer accompanied by the VIP. NLU classifiers confirm financial context in the prior thread. This pattern is consistent with adversaries monitoring or fabricating invoice-related threads to insert themselves at the point of payment handoff.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.previous_threads
  • headers
  • headers.auth_summary
  • recipients
  • recipients.to
  • sender.email
  • type

Rule Body

// note to rule writers
// this rule has a related rule which covers the same logic, but handles the "email address" but no display name in preivous threads
// very likely the logic will need updated in the corresponding rule
// see vip_impersonation_fake_thread_with_invoice_handoff_email.yml
type.inbound
and any(map(filter(body.previous_threads,
                   .sender.email.email == ""
                   and .sender.display_name != ""
                   and any($org_vips,
                           strings.icontains(..sender.display_name,
                                             .display_name
                           )
                   )
                   and not coalesce(.subject.is_auto_reply, false)
                   and any(recipients.to,
                           .email.domain.root_domain in $org_domains
                           and strings.icontains(..text, .email.email)
                   )
                   // payment "handoff" phrasing.
                   and regex.icontains(.text,
                                       'accounts? payable',
                                       '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to',
                                       '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)',
                                       'for payment processing,? please contact',
                                       '(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b',
                                       '(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:',
                                       'billing (?:contact|correspondence|team|department)',
                                       'a copy.{0,20}sent to',
                   )
            ),
            .sender.display_name
        ),
        . != ""
        // any previous thread authored by the "VIP" has invoice/payment
        and (
          any(filter(body.previous_threads, .sender.display_name == ..),
              any(ml.nlu_classifier(.text, subject=.subject.base).tags,
                  .name in ("invoice", "payment") and .confidence != "low"
              )
              or any(ml.nlu_classifier(.text, subject=.subject.base).topics,
                     .name in ("Request to View Invoice", "Payment Information")
                     and .confidence != "low"
              )
          )
          // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related
          or any(attachments,
                 strings.istarts_with(.file_name, 'INV', "W-9", 'W9')
          )
        )
        and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
                    any($org_vips,
                        strings.icontains(..., .display_name)
                        and .email != ""
                        and strings.icontains(..email.email, .email)
                    )
        )
)
and length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),
                  .email.domain.root_domain in $org_domains
           )
) == 1
and not (
  sender.email.domain.root_domain in $org_domains
  and coalesce(headers.auth_summary.dmarc.pass, false)
)

Detection Logic

Scope: inbound message.

Detects inbound messages where an external sender, absent from the original conversation, targets a single internal recipient after a VIP previously instructed that party to handle invoice or payment correspondence. The rule identifies prior threads where a known VIP explicitly redirected billing or payment contact to an internal address — using language such as forwarding instructions, accounts payable references, or billing handoff phrasing — and flags follow-up messages from external senders who are no longer accompanied by the VIP. NLU classifiers confirm financial context in the prior thread. This pattern is consistent with adversaries monitoring or fabricating invoice-related threads to insert themselves at the point of payment handoff.

  1. inbound message
  2. any(map(filter(body.previous_threads, .sender.email.email == "" and .sender.display_name != "" and any($org_vips, strings.icontains(..sender.display_name, .display_name ) ) and not coalesce(.subject.is_auto_reply, false) and any(recipients.to, .email.domain.root_domain in $org_domains and strings.icontains(..text, .email.email) ) // payment "handoff" phrasing. and regex.icontains(.text, 'accounts? payable', '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to', '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)', 'for payment processing,? please contact', '(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b', '(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:', 'billing (?:contact|correspondence|team|department)', 'a copy.{0,20}sent to', ) ), .sender.display_name ), . != "" // any previous thread authored by the "VIP" has invoice/payment and ( any(filter(body.previous_threads, .sender.display_name == ..), any(ml.nlu_classifier(.text, subject=.subject.base).tags, .name in ("invoice", "payment") and .confidence != "low" ) or any(ml.nlu_classifier(.text, subject=.subject.base).topics, .name in ("Request to View Invoice", "Payment Information") and .confidence != "low" ) ) // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related or any(attachments, strings.istarts_with(.file_name, 'INV', "W-9", 'W9') ) ) and not any(flatten([recipients.to, recipients.cc, recipients.bcc]), any($org_vips, strings.icontains(..., .display_name) and .email != "" and strings.icontains(..email.email, .email) ) ) )
  3. length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]), .email.domain.root_domain in $org_domains ) ) == 1
  4. not:
    • all of:
      • sender.email.domain.root_domain in $org_domains
      • coalesce(headers.auth_summary.dmarc.pass, false)

Inspects: attachments[].file_name, body.previous_threads, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, body.previous_threads[].subject.is_auto_reply, body.previous_threads[].text, headers.auth_summary.dmarc.pass, recipients.bcc, recipients.cc, recipients.to, recipients.to[].email.domain.root_domain, recipients.to[].email.email, sender.email.domain.root_domain, type.inbound. Sensors: ml.nlu_classifier, regex.icontains, strings.icontains, strings.istarts_with. Reference lists: $org_domains, $org_vips.

Indicators Matched (16)

FieldMatchValue
body.previous_threads[].sender.email.emailequals
regex.icontainsregexaccounts? payable
regex.icontainsregex(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to
regex.icontainsregex(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)
regex.icontainsregexfor payment processing,? please contact
regex.icontainsregex(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b
regex.icontainsregex(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:
regex.icontainsregexbilling (?:contact|correspondence|team|department)
regex.icontainsregexa copy.{0,20}sent to
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).tags[].namememberinvoice
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).tags[].namememberpayment
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).topics[].namememberRequest to View Invoice
4 more
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).topics[].namememberPayment Information
strings.istarts_withprefixINV
strings.istarts_withprefixW-9
strings.istarts_withprefixW9

Stages and Predicates

Stage 1: mql_rule

and
  not
    and
      headers.auth_summary.dmarc.pass eq "true"
      headers.auth_summary.dmarc.pass is_not_null
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
  length func_call "length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                  .email.domain.root_domain in $org_domains\n           )\n) == 1"
  type.inbound eq "true"
   macro "any(map(filter(body.previous_threads,\n                   .sender.email.email == \"\"\n                   and .sender.display_name != \"\"\n                   and any($org_vips,\n                           strings.icontains(..sender.display_name,\n                                             .display_name\n                           )\n                   )\n                   and not coalesce(.subject.is_auto_reply, false)\n                   and any(recipients.to,\n                           .email.domain.root_domain in $org_domains\n                           and strings.icontains(..text, .email.email)\n                   )\n                   // payment \"handoff\" phrasing.\n                   and regex.icontains(.text,\n                                       'accounts? payable',\n                                       '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to',\n                                       '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)',\n                                       'for payment processing,? please contact',\n                                       '(?:please )?direct (?:it|all|the)[^\\n]{0,60}\\bto\\b',\n                                       '(?:as follows|provided below|find below|details are|contact is)[^\\n]{0,10}:',\n                                       'billing (?:contact|correspondence|team|department)',\n                                       'a copy.{0,20}sent to',\n                   )\n            ),\n            .sender.display_name\n        ),\n        . != \"\"\n        // any previous thread authored by the \"VIP\" has invoice/payment\n        and (\n          any(filter(body.previous_threads, .sender.display_name == ..),\n              any(ml.nlu_classifier(.text, subject=.subject.base).tags,\n                  .name in (\"invoice\", \"payment\") and .confidence != \"low\"\n              )\n              or any(ml.nlu_classifier(.text, subject=.subject.base).topics,\n                     .name in (\"Request to View Invoice\", \"Payment Information\")\n                     and .confidence != \"low\"\n              )\n          )\n          // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related\n          or any(attachments,\n                 strings.istarts_with(.file_name, 'INV', \"W-9\", 'W9')\n          )\n        )\n        and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                    any($org_vips,\n                        strings.icontains(..., .display_name)\n                        and .email != \"\"\n                        and strings.icontains(..email.email, .email)\n                    )\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
headers.auth_summary.dmarc.passeqtrueexcludes:headers.auth_summary.dmarc.pass field:"headers.auth_summary.dmarc.pass" value:"true"
headers.auth_summary.dmarc.passis_not_null(no value, null check)excludes:headers.auth_summary.dmarc.pass

VIP Impersonation: VIP handoff with fake forwarded invoice thread

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Flags inbound messages that fabricate a forwarded thread to impersonate a VIP and redirect payment on a fake past-due invoice. The oldest thread segment shows a forward addressed to the VIP, immediately preceded by a message purportedly from that same VIP containing a mailto link pointing to an attacker-controlled address. The live message routes to that mailto address instead of the VIP's legitimate address, which has been dropped from the recipient list, while only a single organizational recipient remains. This pattern is consistent with invoice or overdue-account lures that exploit a hijacked thread to trick a target into paying a fraudulent account.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Spoofing, Out of band pivot

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.previous_threads
  • headers
  • headers.auth_summary
  • recipients
  • sender.email
  • type

Rule Body

type.inbound
and length(body.previous_threads) > 1

// the oldest segment is a forward and has a single recipient which is the VIP
and length(body.previous_threads[length(body.previous_threads) - 1].recipients.to
) == 1
// the segment is a forward
// handle where there is no subject
and coalesce(body.previous_threads[length(body.previous_threads) - 1].subject.is_forward,
             false
)
and any($org_vips,
        // oldest thread (len-1) forward is TO the VIP
        body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name =~ .display_name
        // next oldest thread (len-2) is FROM the same VIP;
        and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name
)
// the rcpt email format of the oldest thread matches the sender email format of the next oldest thread
and body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].email.email =~ body.previous_threads[length(body.previous_threads
) - 2].sender.email.email

// the message authored by the "VIP" links to an email (parsed mailto:) that the live message now routes to
// this is the "VIP handoff" step, the fake VIP mentions the email address, which becomes the "victim", the actor sends it to that address and includes the faked emails emails in the chain.
and any(body.previous_threads[length(body.previous_threads) - 2].links,
        .href_url.url != ""
        and .href_url.scheme == "mailto"
        and any(flatten([recipients.to, recipients.cc, recipients.bcc]),
                strings.icontains(.email.email, ..href_url.url)
        )
)

// the VIP's authoritative list-email is NOT among the live recipients (dropped)
and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
            any($org_vips,
                .email != ""
                and strings.icontains(..email.email, .email)
                and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name
            )
)
// we only have a single org_domain recipient
and length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),
                  .email.domain.root_domain in $org_domains
           )
) == 1
// this message is not from the org itself
and not (
  sender.email.domain.root_domain in $org_domains
  and coalesce(headers.auth_summary.dmarc.pass, false)
)

Detection Logic

Scope: inbound message.

Flags inbound messages that fabricate a forwarded thread to impersonate a VIP and redirect payment on a fake past-due invoice. The oldest thread segment shows a forward addressed to the VIP, immediately preceded by a message purportedly from that same VIP containing a mailto link pointing to an attacker-controlled address. The live message routes to that mailto address instead of the VIP's legitimate address, which has been dropped from the recipient list, while only a single organizational recipient remains. This pattern is consistent with invoice or overdue-account lures that exploit a hijacked thread to trick a target into paying a fraudulent account.

  1. inbound message
  2. length(body.previous_threads) > 1
  3. length(body.previous_threads[length(body.previous_threads) - 1].recipients.to ) == 1
  4. coalesce(body.previous_threads[length(body.previous_threads) - 1].subject.is_forward, false )
  5. any($org_vips, // oldest thread (len-1) forward is TO the VIP body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name =~ .display_name // next oldest thread (len-2) is FROM the same VIP; and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name )
  6. body.previous_threads[].recipients.to[0].email.email is body.previous_threads[].sender.email.email
  7. any(body.previous_threads[length(body.previous_threads) - 2].links, .href_url.url != "" and .href_url.scheme == "mailto" and any(flatten([recipients.to, recipients.cc, recipients.bcc]), strings.icontains(.email.email, ..href_url.url) ) )
  8. not:
    • any(flatten([recipients.to, recipients.cc, recipients.bcc]), any($org_vips, .email != "" and strings.icontains(..email.email, .email) and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name ) )
  9. length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]), .email.domain.root_domain in $org_domains ) ) == 1
  10. not:
    • all of:
      • sender.email.domain.root_domain in $org_domains
      • coalesce(headers.auth_summary.dmarc.pass, false)

Inspects: body.previous_threads, body.previous_threads[].links, body.previous_threads[].links[].href_url.scheme, body.previous_threads[].links[].href_url.url, body.previous_threads[].recipients.to, body.previous_threads[].recipients.to[0].display_name, body.previous_threads[].recipients.to[0].email.email, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, body.previous_threads[].subject.is_forward, headers.auth_summary.dmarc.pass, recipients.bcc, recipients.cc, recipients.to, sender.email.domain.root_domain, type.inbound. Sensors: strings.icontains. Reference lists: $org_domains, $org_vips.

Indicators Matched (1)

FieldMatchValue
body.previous_threads[].links[].href_url.schemeequalsmailto

Stages and Predicates

Stage 1: mql_rule

and
  not
    and
      headers.auth_summary.dmarc.pass eq "true"
      headers.auth_summary.dmarc.pass is_not_null
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
  not
     macro "any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n            any($org_vips,\n                .email != \"\"\n                and strings.icontains(..email.email, .email)\n                and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name\n            )\n)"
  body.previous_threads length_compare "1"
  body.previous_threads[].recipients.to length_compare "1"
  body.previous_threads[].recipients.to[0].email.email cross_field_compare "body.previous_threads[].sender.email.email"
  coalesce func_call "coalesce(body.previous_threads[length(body.previous_threads) - 1].subject.is_forward,\n             false\n)"
  length func_call "length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                  .email.domain.root_domain in $org_domains\n           )\n) == 1"
  type.inbound eq "true"
   macro "any($org_vips,\n        // oldest thread (len-1) forward is TO the VIP\n        body.previous_threads[length(body.previous_threads) - 1].recipients.to[0].display_name =~ .display_name\n        // next oldest thread (len-2) is FROM the same VIP;\n        and body.previous_threads[length(body.previous_threads) - 2].sender.display_name =~ .display_name\n)"
   macro "any(body.previous_threads[length(body.previous_threads) - 2].links,\n        .href_url.url != \"\"\n        and .href_url.scheme == \"mailto\"\n        and any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                strings.icontains(.email.email, ..href_url.url)\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
headers.auth_summary.dmarc.passeqtrueexcludes:headers.auth_summary.dmarc.pass field:"headers.auth_summary.dmarc.pass" value:"true"
headers.auth_summary.dmarc.passis_not_null(no value, null check)excludes:headers.auth_summary.dmarc.pass

VIP impersonation: VIP name within a delimited subject with fake previous threads

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages where a known VIP's display name appears in the subject line surrounded by quotation marks or forward-slash delimiters (e.g., /Name/, Name (in quotes), -//Name), and where prior thread history confirms the VIP either authored a previous message or is being actively impersonated by a mismatched sender. Observed lures include fabricated invoices, leadership recognition offers, and forwarded billing threads referencing real executives by name and company. The pattern spans multiple sender domains and spoofed invoice references, all anchored to VIP identity signals extracted from thread context.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Spoofing

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.current_thread
  • body.html
  • body.plain
  • body.previous_threads
  • sender.email
  • subject
  • type

Rule Body

type.inbound
and length(coalesce(body.plain.raw, body.html.display_text)) < 6000
and strings.icontains(strings.replace_confusables(subject.base),
                      '"',
                      '/-/',
                      '-//',
                      '-/',
                      '-#'
)
and any(body.previous_threads,
        any($org_vips,
            (
              strings.icontains(strings.replace_confusables(subject.base),
                                strings.concat('"', .display_name)
              )
              or strings.icontains(strings.replace_confusables(subject.base),
                                   strings.concat(.display_name, '"')
              )
              or strings.icontains(strings.replace_confusables(subject.base),
                                   strings.concat('/-/', .display_name)
              )
              or strings.icontains(strings.replace_confusables(subject.base),
                                   strings.concat('-//', .display_name)
              )
              or strings.icontains(strings.replace_confusables(subject.base),
                                   strings.concat('-/', .display_name)
              )
              or strings.icontains(strings.replace_confusables(subject.base),
                                   strings.concat('-#', .display_name)
              )
            )
            and (
              strings.icontains(..sender.display_name, .display_name)
              or (
                .email != "" and strings.icontains(..sender.email.email, .email)
              )
              or (
                strings.icontains(sender.display_name, .display_name)
                and sender.email.email != .email
              )
            )
        )
)
and any(ml.nlu_classifier(body.current_thread.text).topics,
        .name in (
          "Financial Communications",
          "Request to View Invoice",
          "Payment Information"
        )
        and .confidence != "low"
)

Detection Logic

Scope: inbound message.

Detects inbound messages where a known VIP's display name appears in the subject line surrounded by quotation marks or forward-slash delimiters (e.g., /Name/, Name (in quotes), -//Name), and where prior thread history confirms the VIP either authored a previous message or is being actively impersonated by a mismatched sender. Observed lures include fabricated invoices, leadership recognition offers, and forwarded billing threads referencing real executives by name and company. The pattern spans multiple sender domains and spoofed invoice references, all anchored to VIP identity signals extracted from thread context.

  1. inbound message
  2. length(coalesce(body.plain.raw, body.html.display_text)) < 6000
  3. strings.icontains(strings.replace_confusables(subject.base), '"', '/-/', '-//', '-/', '-#' )
  4. any of body.previous_threads where:
    • any($org_vips, ( strings.icontains(strings.replace_confusables(subject.base), strings.concat('"', .display_name) ) or strings.icontains(strings.replace_confusables(subject.base), strings.concat(.display_name, '"') ) or strings.icontains(strings.replace_confusables(subject.base), strings.concat('/-/', .display_name) ) or strings.icontains(strings.replace_confusables(subject.base), strings.concat('-//', .display_name) ) or strings.icontains(strings.replace_confusables(subject.base), strings.concat('-/', .display_name) ) or strings.icontains(strings.replace_confusables(subject.base), strings.concat('-#', .display_name) ) ) and ( strings.icontains(..sender.display_name, .display_name) or ( .email != "" and strings.icontains(..sender.email.email, .email) ) or ( strings.icontains(sender.display_name, .display_name) and sender.email.email != .email ) ) )
  5. any(ml.nlu_classifier(body.current_thread.text).topics, .name in ( "Financial Communications", "Request to View Invoice", "Payment Information" ) and .confidence != "low" )

Inspects: body.current_thread.text, body.html.display_text, body.plain.raw, body.previous_threads, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, sender.display_name, sender.email.email, subject.base, type.inbound. Sensors: ml.nlu_classifier, strings.concat, strings.icontains, strings.replace_confusables. Reference lists: $org_vips.

Indicators Matched (8)

FieldMatchValue
strings.icontainssubstring"
strings.icontainssubstring/-/
strings.icontainssubstring-//
strings.icontainssubstring-/
strings.icontainssubstring-#
ml.nlu_classifier(body.current_thread.text).topics[].namememberFinancial Communications
ml.nlu_classifier(body.current_thread.text).topics[].namememberRequest to View Invoice
ml.nlu_classifier(body.current_thread.text).topics[].namememberPayment Information

Stages and Predicates

Stage 1: mql_rule

and
  any(body.previous_threads)
     macro "any($org_vips,\n            (\n              strings.icontains(strings.replace_confusables(subject.base),\n                                strings.concat('\"', .display_name)\n              )\n              or strings.icontains(strings.replace_confusables(subject.base),\n                                   strings.concat(.display_name, '\"')\n              )\n              or strings.icontains(strings.replace_confusables(subject.base),\n                                   strings.concat('/-/', .display_name)\n              )\n              or strings.icontains(strings.replace_confusables(subject.base),\n                                   strings.concat('-//', .display_name)\n              )\n              or strings.icontains(strings.replace_confusables(subject.base),\n                                   strings.concat('-/', .display_name)\n              )\n              or strings.icontains(strings.replace_confusables(subject.base),\n                                   strings.concat('-#', .display_name)\n              )\n            )\n            and (\n              strings.icontains(..sender.display_name, .display_name)\n              or (\n                .email != \"\" and strings.icontains(..sender.email.email, .email)\n              )\n              or (\n                strings.icontains(sender.display_name, .display_name)\n                and sender.email.email != .email\n              )\n            )\n        )"
  length func_call "length(coalesce(body.plain.raw, body.html.display_text)) < 6000"
  strings.icontains func_call "strings.icontains(strings.replace_confusables(subject.base),\n                      '\"',\n                      '/-/',\n                      '-//',\n                      '-/',\n                      '-#'\n)"
  type.inbound eq "true"
   macro "any(ml.nlu_classifier(body.current_thread.text).topics,\n        .name in (\n          \"Financial Communications\",\n          \"Request to View Invoice\",\n          \"Payment Information\"\n        )\n        and .confidence != \"low\"\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: VIP payment redirect handoff via fake threads

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages where an external sender, absent from the original conversation, targets a single internal recipient after a VIP previously instructed that party to handle invoice or payment correspondence. The rule identifies prior threads where a known VIP explicitly redirected billing or payment contact to an internal address — using language such as forwarding instructions, accounts payable references, or billing handoff phrasing — and flags follow-up messages from external senders who are no longer accompanied by the VIP. NLU classifiers confirm financial context in the prior thread. This pattern is consistent with adversaries monitoring or fabricating invoice-related threads to insert themselves at the point of payment handoff.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • attachments
  • body
  • body.previous_threads
  • recipients
  • recipients.to
  • sender.email
  • type

Rule Body

// note to rule writers
// this rule has a related rule which covers the same logic, but handles the display name but no email address in preivous threads
// very likely the logic will need updated in the corresponding rule
// see vip_impersonation_fake_thread_with_invoice_handoff_display_name.yml
type.inbound
and any(map(filter(body.previous_threads,
                   // VIP authored the segment: display_name OR real email (OR, not AND -- the actor guesses
                   // the exec's address, so a fabricated From: is often the wrong format at the right org).
                   any($org_vips,
                       strings.icontains(..sender.display_name, .display_name)
                       or (
                         .email != ""
                         and strings.icontains(..sender.email.email, .email)
                       )
                   )
                   // skip auto-replies; coalesce the null on subject-less segments before `not`.
                   and not coalesce(.subject.is_auto_reply, false)
                   // the live recipient is the payee the VIP names, at an org domain.
                   and any(recipients.to,
                           .email.email != ..sender.email.email
                           and .email.domain.root_domain in $org_domains
                           and strings.icontains(..text, .email.email)
                   )
                   // payment "handoff" phrasing.
                   and regex.icontains(.text,
                                       'accounts? payable',
                                       '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to',
                                       '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)',
                                       'for payment processing,? please contact',
                                       '(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b',
                                       '(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:',
                                       'billing (?:contact|correspondence|team|department)',
                                       'a copy.{0,20}sent to',
                   )
            ),
            .sender.email.email
        ),
        . != ""
        // the VIP is no longer present in the live message.
        and not strings.icontains(sender.email.email, .)
        and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),
                    strings.icontains(.email.email, ..)
        )
        // any previous thread authored by the "VIP" has invoice/payment
        and (
          any(filter(body.previous_threads, .sender.email.email == ..),
              any(ml.nlu_classifier(.text, subject=.subject.base).tags,
                  .name in ("invoice", "payment") and .confidence != "low"
              )
              or any(ml.nlu_classifier(.text, subject=.subject.base).topics,
                     .name in ("Request to View Invoice", "Payment Information")
                     and .confidence != "low"
              )
          )
          // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related
          or any(attachments,
                 strings.istarts_with(.file_name, 'INV', "W-9", 'W9')
          )
        )
)
// exactly one org-domain recipient (the payee); external sender.
and length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),
                  .email.domain.root_domain in $org_domains
           )
) == 1
and not sender.email.domain.root_domain in $org_domains

Detection Logic

Scope: inbound message.

Detects inbound messages where an external sender, absent from the original conversation, targets a single internal recipient after a VIP previously instructed that party to handle invoice or payment correspondence. The rule identifies prior threads where a known VIP explicitly redirected billing or payment contact to an internal address — using language such as forwarding instructions, accounts payable references, or billing handoff phrasing — and flags follow-up messages from external senders who are no longer accompanied by the VIP. NLU classifiers confirm financial context in the prior thread. This pattern is consistent with adversaries monitoring or fabricating invoice-related threads to insert themselves at the point of payment handoff.

  1. inbound message
  2. any(map(filter(body.previous_threads, // VIP authored the segment: display_name OR real email (OR, not AND -- the actor guesses // the exec's address, so a fabricated From: is often the wrong format at the right org). any($org_vips, strings.icontains(..sender.display_name, .display_name) or ( .email != "" and strings.icontains(..sender.email.email, .email) ) ) // skip auto-replies; coalesce the null on subject-less segments before `not`. and not coalesce(.subject.is_auto_reply, false) // the live recipient is the payee the VIP names, at an org domain. and any(recipients.to, .email.email != ..sender.email.email and .email.domain.root_domain in $org_domains and strings.icontains(..text, .email.email) ) // payment "handoff" phrasing. and regex.icontains(.text, 'accounts? payable', '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to', '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)', 'for payment processing,? please contact', '(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b', '(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:', 'billing (?:contact|correspondence|team|department)', 'a copy.{0,20}sent to', ) ), .sender.email.email ), . != "" // the VIP is no longer present in the live message. and not strings.icontains(sender.email.email, .) and not any(flatten([recipients.to, recipients.cc, recipients.bcc]), strings.icontains(.email.email, ..) ) // any previous thread authored by the "VIP" has invoice/payment and ( any(filter(body.previous_threads, .sender.email.email == ..), any(ml.nlu_classifier(.text, subject=.subject.base).tags, .name in ("invoice", "payment") and .confidence != "low" ) or any(ml.nlu_classifier(.text, subject=.subject.base).topics, .name in ("Request to View Invoice", "Payment Information") and .confidence != "low" ) ) // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related or any(attachments, strings.istarts_with(.file_name, 'INV', "W-9", 'W9') ) ) )
  3. length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]), .email.domain.root_domain in $org_domains ) ) == 1
  4. not:
    • sender.email.domain.root_domain in $org_domains

Inspects: attachments[].file_name, body.previous_threads, body.previous_threads[].sender.display_name, body.previous_threads[].sender.email.email, body.previous_threads[].subject.is_auto_reply, body.previous_threads[].text, recipients.bcc, recipients.cc, recipients.to, recipients.to[].email.domain.root_domain, recipients.to[].email.email, sender.email.domain.root_domain, sender.email.email, type.inbound. Sensors: ml.nlu_classifier, regex.icontains, strings.icontains, strings.istarts_with. Reference lists: $org_domains, $org_vips.

Indicators Matched (15)

FieldMatchValue
regex.icontainsregexaccounts? payable
regex.icontainsregex(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to
regex.icontainsregex(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)
regex.icontainsregexfor payment processing,? please contact
regex.icontainsregex(?:please )?direct (?:it|all|the)[^\n]{0,60}\bto\b
regex.icontainsregex(?:as follows|provided below|find below|details are|contact is)[^\n]{0,10}:
regex.icontainsregexbilling (?:contact|correspondence|team|department)
regex.icontainsregexa copy.{0,20}sent to
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).tags[].namememberinvoice
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).tags[].namememberpayment
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).topics[].namememberRequest to View Invoice
ml.nlu_classifier(filter(body.previous_threads)[].text, subject=filter(body.previous_threads)[].subject.base).topics[].namememberPayment Information
3 more
strings.istarts_withprefixINV
strings.istarts_withprefixW-9
strings.istarts_withprefixW9

Stages and Predicates

Stage 1: mql_rule

and
  not
    sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
  length func_call "length(filter(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                  .email.domain.root_domain in $org_domains\n           )\n) == 1"
  type.inbound eq "true"
   macro "any(map(filter(body.previous_threads,\n                   // VIP authored the segment: display_name OR real email (OR, not AND -- the actor guesses\n                   // the exec's address, so a fabricated From: is often the wrong format at the right org).\n                   any($org_vips,\n                       strings.icontains(..sender.display_name, .display_name)\n                       or (\n                         .email != \"\"\n                         and strings.icontains(..sender.email.email, .email)\n                       )\n                   )\n                   // skip auto-replies; coalesce the null on subject-less segments before `not`.\n                   and not coalesce(.subject.is_auto_reply, false)\n                   // the live recipient is the payee the VIP names, at an org domain.\n                   and any(recipients.to,\n                           .email.email != ..sender.email.email\n                           and .email.domain.root_domain in $org_domains\n                           and strings.icontains(..text, .email.email)\n                   )\n                   // payment \"handoff\" phrasing.\n                   and regex.icontains(.text,\n                                       'accounts? payable',\n                                       '(?:forward|send|rout|direct|remit|submit|issue) (?:it|a copy) (?:directly )?to',\n                                       '(?:forward|send|direct|remit|submit|route) (?:the |all |related |for )?(?:invoice|correspondence|payment|billing|processing)',\n                                       'for payment processing,? please contact',\n                                       '(?:please )?direct (?:it|all|the)[^\\n]{0,60}\\bto\\b',\n                                       '(?:as follows|provided below|find below|details are|contact is)[^\\n]{0,10}:',\n                                       'billing (?:contact|correspondence|team|department)',\n                                       'a copy.{0,20}sent to',\n                   )\n            ),\n            .sender.email.email\n        ),\n        . != \"\"\n        // the VIP is no longer present in the live message.\n        and not strings.icontains(sender.email.email, .)\n        and not any(flatten([recipients.to, recipients.cc, recipients.bcc]),\n                    strings.icontains(.email.email, ..)\n        )\n        // any previous thread authored by the \"VIP\" has invoice/payment\n        and (\n          any(filter(body.previous_threads, .sender.email.email == ..),\n              any(ml.nlu_classifier(.text, subject=.subject.base).tags,\n                  .name in (\"invoice\", \"payment\") and .confidence != \"low\"\n              )\n              or any(ml.nlu_classifier(.text, subject=.subject.base).topics,\n                     .name in (\"Request to View Invoice\", \"Payment Information\")\n                     and .confidence != \"low\"\n              )\n          )\n          // if we don't get NLU but there is a W9 or Inv attached, we can assume it's invoice related\n          or any(attachments,\n                 strings.istarts_with(.file_name, 'INV', \"W-9\", 'W9')\n          )\n        )\n)"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
type.inboundeq
  • true transforms: boolean corpus 1212 (mql 1212)
field:"type.inbound" kind:eq value:"true"

VIP impersonation: VIP recipient of previous thread with HTML generator

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

Detects inbound messages that impersonate invoice communications impersonating organizational VIPs, where the message appears in a thread previously involving VIP recipients. The rule identifies a specific template technique where the HTML body contains markers associated with 'Advanced HTML parser' tooling or a suspicious '<title>HTML Message</title>' tag. Both are artifacts commonly left by tools used to craft or obfuscate invoice lure content. Observed samples spoof leadership-recognition invoice themes naming specific executives as the invoiced party.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesImpersonation: VIP, Social engineering, Evasion

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • body
  • body.html
  • body.previous_threads
  • type

Rule Body

type.inbound
and any(body.previous_threads,
        any(.recipients.to,
            any($org_vips,
                strings.icontains(..display_name, .display_name)
                or strings.icontains(..email.email, .email)
            )
        )
)
and (
  strings.icontains(body.html.raw, 'Advanced HTML parser')
  or regex.icontains(body.html.raw, '<title>\s*HTML Message\s*</title>')
)

Detection Logic

Scope: inbound message.

Detects inbound messages that impersonate invoice communications impersonating organizational VIPs, where the message appears in a thread previously involving VIP recipients. The rule identifies a specific template technique where the HTML body contains markers associated with 'Advanced HTML parser' tooling or a suspicious '<title>HTML Message</title>' tag. Both are artifacts commonly left by tools used to craft or obfuscate invoice lure content. Observed samples spoof leadership-recognition invoice themes naming specific executives as the invoiced party.

  1. inbound message
  2. any of body.previous_threads where:
    • any of .recipients.to where:
      • any($org_vips, strings.icontains(..display_name, .display_name) or strings.icontains(..email.email, .email) )
  3. any of:
    • body.html.raw contains 'Advanced HTML parser'
    • body.html.raw matches '<title>\\s*HTML Message\\s*</title>'

Inspects: body.html.raw, body.previous_threads, body.previous_threads[].recipients.to, body.previous_threads[].recipients.to[].display_name, body.previous_threads[].recipients.to[].email.email, type.inbound. Sensors: regex.icontains, strings.icontains. Reference lists: $org_vips.

Indicators Matched (2)

FieldMatchValue
strings.icontainssubstringAdvanced HTML parser
regex.icontainsregex<title>\s*HTML Message\s*</title>

Stages and Predicates

Stage 1: mql_rule

and
  any(body.previous_threads)
    any(body.previous_threads.recipients.to)
       macro "any($org_vips,\n                strings.icontains(..display_name, .display_name)\n                or strings.icontains(..email.email, .email)\n            )"
  or
    body.html.raw contains "Advanced HTML parser"
    body.html.raw regex_match "<title>\\s*HTML Message\\s*</title>"
  type.inbound eq "true"

Indicators

These rows show field, operator, and value matches.

VIP local_part impersonation from unsolicited sender

#
Severity
high
Type
rule
Source
github.com/sublime-security/sublime-rules

This rule identifies potential impersonation attempts involving the local part of an $org_vip email address. Specifically, it checks for cases where the local part of an $org_vip email (e.g., local_part@domain.com) appears with a different domain (e.g., local_part@foreigndomain.com). Additionally, the rule flags messages that match an $org_vip address exactly but fail authentication.

Threat Classification

Sublime's own taxonomy (not MITRE ATT&CK).

CategoryValues
Tactics and techniquesImpersonation: VIP, Spoofing

Telemetry Coverage

PlatformRecord / event type
SublimeInbound email message

Message Attributes

  • headers
  • headers.auth_summary
  • mailbox
  • recipients
  • sender.email
  • type

Rule Body

type.inbound
and strings.contains(sender.email.local_part, ".")
and any($org_vips,
        strings.starts_with(.email, sender.email.local_part)
        and (
          sender.email.email != .email
          or (
            sender.email.email == .email and not headers.auth_summary.dmarc.pass
          )
        )
)
and (
  // ignore personal <> work emails
  // where the sender and mailbox's display name are the same
  length(recipients.to) > 0
  or length(recipients.cc) > 0
  or sender.display_name != mailbox.display_name
)
// bounce-back negations
and not strings.like(sender.email.local_part,
                     "*postmaster*",
                     "*mailer-daemon*",
                     "*administrator*"
)

// negate org domains unless they fail DMARC authentication
and (
  (
    sender.email.domain.root_domain in $org_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $org_domains
)

// negate highly trusted sender domains unless they fail DMARC authentication
and (
  (
    sender.email.domain.root_domain in $high_trust_sender_root_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $high_trust_sender_root_domains
)
// sender profile
and (
  not profile.by_sender_email().solicited
  or not profile.by_sender_email().any_messages_benign
  or (
    profile.by_sender_email().any_messages_malicious_or_spam
    and not profile.by_sender_email().any_messages_benign
  )
  or (
    sender.email.domain.domain in $org_domains
    and not coalesce(headers.auth_summary.dmarc.pass, false)
  )
)

Detection Logic

Scope: inbound message.

This rule identifies potential impersonation attempts involving the local part of an $org_vip email address. Specifically, it checks for cases where the local part of an $org_vip email (e.g., local_part@domain.com) appears with a different domain (e.g., local_part@foreigndomain.com). Additionally, the rule flags messages that match an $org_vip address exactly but fail authentication.

  1. inbound message
  2. sender.email.local_part contains '.'
  3. any($org_vips, strings.starts_with(.email, sender.email.local_part) and ( sender.email.email != .email or ( sender.email.email == .email and not headers.auth_summary.dmarc.pass ) ) )
  4. any of:
    • length(recipients.to) > 0
    • length(recipients.cc) > 0
    • sender.display_name is not mailbox.display_name
  5. not:
    • sender.email.local_part matches any of 3 patterns
      • *postmaster*
      • *mailer-daemon*
      • *administrator*
  6. any of:
    • all of:
      • sender.email.domain.root_domain in $org_domains
      • not:
        • headers.auth_summary.dmarc.pass
    • sender.email.domain.root_domain not in $org_domains
  7. any of:
    • all of:
      • sender.email.domain.root_domain in $high_trust_sender_root_domains
      • not:
        • headers.auth_summary.dmarc.pass
    • sender.email.domain.root_domain not in $high_trust_sender_root_domains
  8. any of:
    • not:
      • profile.by_sender_email().solicited
    • not:
      • profile.by_sender_email().any_messages_benign
    • all of:
      • profile.by_sender_email().any_messages_malicious_or_spam
      • not:
        • profile.by_sender_email().any_messages_benign
    • all of:
      • sender.email.domain.domain in $org_domains
      • not:
        • coalesce(headers.auth_summary.dmarc.pass, false)

Inspects: headers.auth_summary.dmarc.pass, mailbox.display_name, recipients.cc, recipients.to, sender.display_name, sender.email.domain.domain, sender.email.domain.root_domain, sender.email.email, sender.email.local_part, type.inbound. Sensors: profile.by_sender_email, strings.contains, strings.like, strings.starts_with. Reference lists: $high_trust_sender_root_domains, $org_domains, $org_vips.

Indicators Matched (1)

FieldMatchValue
strings.containssubstring.

Stages and Predicates

Stage 1: mql_rule

and
  or
    and
      not
        and
          headers.auth_summary.dmarc.pass eq "true"
          headers.auth_summary.dmarc.pass is_not_null
      sender.email.domain.domain macro "sender.email.domain.domain in org_domains"
    and
      not
        profile.by_sender_email func_call "profile.by_sender_email().any_messages_benign"
      profile.by_sender_email func_call "profile.by_sender_email().any_messages_malicious_or_spam"
    not
      profile.by_sender_email func_call "profile.by_sender_email().any_messages_benign"
    not
      profile.by_sender_email func_call "profile.by_sender_email().solicited"
  or
    and
      not
        headers.auth_summary.dmarc.pass eq "true"
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in high_trust_sender_root_domains"
    sender.email.domain.root_domain macro "sender.email.domain.root_domain not in high_trust_sender_root_domains"
  or
    and
      not
        headers.auth_summary.dmarc.pass eq "true"
      sender.email.domain.root_domain macro "sender.email.domain.root_domain in org_domains"
    sender.email.domain.root_domain macro "sender.email.domain.root_domain not in org_domains"
  not
    or
      sender.email.local_part match "administrator"
      sender.email.local_part match "mailer-daemon"
      sender.email.local_part match "postmaster"
  or
    recipients.cc length_compare "0"
    recipients.to length_compare "0"
    sender.display_name cross_field_compare "mailbox.display_name"
  sender.email.local_part contains "."
  type.inbound eq "true"
   macro "any($org_vips,\n        strings.starts_with(.email, sender.email.local_part)\n        and (\n          sender.email.email != .email\n          or (\n            sender.email.email == .email and not headers.auth_summary.dmarc.pass\n          )\n        )\n)"

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Back to all Sublime MQL rules