Detection rules › Sublime MQL

Request for Quote or Purchase (RFQ|RFP) with suspicious sender or recipient pattern

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

RFQ/RFP scams involve fraudulent emails posing as legitimate requests for quotations or purchases, often sent by scammers impersonating reputable organizations. These scams aim to deceive recipients into providing sensitive information or conducting unauthorized transactions, often leading to financial loss, or data leakage.

Threat classification

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

CategoryValues
Attack typesBEC/Fraud
Tactics and techniquesEvasion, Free email provider

Event coverage

Rule body MQL

type.inbound
and (
  (
    (
      length(recipients.to) == 0
      or all(recipients.to,
             .display_name in (
               "Undisclosed recipients",
               "undisclosed-recipients"
             )
      )
    )
    and length(recipients.cc) == 0
  )
  or (
    sender.email.domain.root_domain in $free_email_providers
    and any(headers.reply_to, .email.email != sender.email.email)
    and any(headers.reply_to, .email.email not in $recipient_emails)
  )
  or (
    length(headers.reply_to) > 0
    and all(headers.reply_to,
            .email.domain.root_domain != sender.email.domain.root_domain
            and not .email.domain.root_domain in $org_domains
    )
  )
  or (
    length(recipients.to) == 1
    and all(recipients.to, .email.email == sender.email.email)
    and (length(recipients.cc) > 0 or length(recipients.bcc) > 0)
  )
  or (
    length(recipients.to) == 0
    and length(recipients.cc) == 1
    and sender.email.email == recipients.cc[0].email.email
  )
  or (
    length(recipients.to) == 1
    and length(recipients.cc) == 0
    and sender.email.email == recipients.to[0].email.email
  )
)
and (
  // Group the keyword patterns that specifically indicate RFQ/RFP
  (
    (
      // RFQ/RFP specific language patterns
      regex.icontains(body.current_thread.text,
                      '(discuss.{0,15}purchas(e|ing))'
      )
      or regex.icontains(body.current_thread.text,
                         '(sign(ed?)|view).{0,10}(purchase order)|Request for (a Quot(e|ation)|Proposal)'
      )
      or regex.icontains(body.current_thread.text,
                         '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
      )
      or regex.icontains(subject.subject,
                         '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
      )
      or any(attachments,
             regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
      )
      or any(ml.nlu_classifier(body.current_thread.text).tags,
             .name == "purchase_order" and .confidence == "high"
      )
      or any(ml.nlu_classifier(body.current_thread.text).entities,
             .name == "financial" and regex.imatch(.text, "rfp|rfq")
      )
      or any(ml.nlu_classifier(body.current_thread.text).entities,
             .name == "request" and strings.icontains(.text, 'submit bid')
      )
    )
    // Required: at least one RFQ/RFP keyword pattern

    // Optional: at least one additional indicator (can be another keyword pattern or a non-keyword indicator)
    and (
      2 of (
        // RFQ/RFP keyword patterns (same as above)
        regex.icontains(body.current_thread.text,
                        '(discuss.{0,15}purchas(e|ing))'
        ),
        regex.icontains(body.current_thread.text,
                        '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
        ),
        regex.icontains(body.current_thread.text,
                        '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
        ),
        regex.icontains(body.current_thread.text,
                        '(?:invitation|intent) to bid'
        ),
        regex.icontains(subject.subject,
                        '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
        ),
        any(attachments,
            regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
        ),
        any(ml.nlu_classifier(body.current_thread.text).tags,
            .name == "purchase_order" and .confidence == "high"
        ),
        any(ml.nlu_classifier(body.current_thread.text).entities,
            .name == "financial" and regex.imatch(.text, "rfp|rfq")
        ),

        // Non-keyword indicators
        (
          any(ml.nlu_classifier(body.current_thread.text).entities,
              .name == "request"
          )
          and any(ml.nlu_classifier(body.current_thread.text).entities,
                  .name == "urgency"
          )
          and not any(ml.nlu_classifier(body.current_thread.text).topics,
                      .name == "Advertising and Promotions"
                      and .confidence == "high"
          )
        ),
        (
          0 < length(filter(body.links,
                            (
                              .href_url.domain.domain in $free_subdomain_hosts
                              or .href_url.domain.domain in $free_file_hosts
                              or network.whois(.href_url.domain).days_old < 30
                            )
                            and (
                              regex.match(.display_text, '[A-Z ]+')
                              or any(ml.nlu_classifier(.display_text).entities,
                                     .name in ("request", "urgency")
                              )
                              or any(ml.nlu_classifier(.display_text).intents,
                                     .name in ("cred_theft")
                              )
                            )
                     )
          ) < 3
        ),
        // mentions an attachment that does not exist
        (
          length(attachments) == 0
          and strings.icontains(body.current_thread.text, "attached")
        ),
        any(body.current_thread.links, regex.icontains(.href_url.url, 'RFP'))
      )
    )
  )
  or (
    length(attachments) == 1
    and length(body.current_thread.text) < 100
    and all(attachments,
            .file_type in $file_types_images
            and any(file.explode(.),
                    2 of (
                      regex.icontains(.scan.ocr.raw,
                                      '(discuss.{0,15}purchas(e|ing))'
                      ),
                      regex.icontains(.scan.ocr.raw,
                                      '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
                      ),
                      regex.icontains(.scan.ocr.raw,
                                      '(please|kindly).{0,30}quote'
                      ),
                      (
                        any(ml.nlu_classifier(.scan.ocr.raw).entities,
                            .name == "request"
                        )
                        and any(ml.nlu_classifier(.scan.ocr.raw).entities,
                                .name == "urgency"
                        )
                      ),
                      any(ml.nlu_classifier(.scan.ocr.raw).tags,
                          .name == "purchase_order" and .confidence == "high"
                      ),
                      any(ml.nlu_classifier(.scan.ocr.raw).entities,
                          .name == "financial"
                          and regex.imatch(.text, "rfp|rfq")
                      ),
                    )
            )
    )
  )
  // fake PDF file icon used as a link lure with bid solicitation language
  or (
    regex.icontains(subject.subject, 'project\s+summary')
    and any(html.xpath(body.html, '//div[.//img[contains(@src,"pdf")]]//a').nodes,
            regex.icontains(.display_text, 'project\s+summary')
    )
    and regex.icontains(body.current_thread.text,
                        '(put a bid|\bbid\s+(for|on)\b|submit.{1,20}(bid|quot(e|ation))|request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|project\s+summary)'
    )
  )
)
// wetransfer includes user specific reply-to's & link display text which triggers NLU logic further within the rule
and not (
  sender.email.domain.root_domain == "wetransfer.com"
  and coalesce(headers.auth_summary.dmarc.pass, false)
)

// 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
)
and (
  (
    (
      not profile.by_sender().solicited
      or profile.by_sender().days_since.last_contact > 30
    )
    and not profile.by_sender().any_messages_benign
  )
  // sender address listed as a recipient
  or (
    length(recipients.to) == 1
    and sender.email.email in map(recipients.to, .email.email)
  )
)

Detection logic

Scope: inbound message.

RFQ/RFP scams involve fraudulent emails posing as legitimate requests for quotations or purchases, often sent by scammers impersonating reputable organizations. These scams aim to deceive recipients into providing sensitive information or conducting unauthorized transactions, often leading to financial loss, or data leakage.

  1. inbound message
  2. any of:
    • all of:
      • any of:
        • length(recipients.to) is 0
        • all of recipients.to where:
          • .display_name in ('Undisclosed recipients', 'undisclosed-recipients')
      • length(recipients.cc) is 0
    • all of:
      • sender.email.domain.root_domain in $free_email_providers
      • any of headers.reply_to where:
        • .email.email is not sender.email.email
      • any of headers.reply_to where:
        • .email.email not in $recipient_emails
    • all of:
      • length(headers.reply_to) > 0
      • all of headers.reply_to where all hold:
        • .email.domain.root_domain is not sender.email.domain.root_domain
        • not:
          • .email.domain.root_domain in $org_domains
    • all of:
      • length(recipients.to) is 1
      • all of recipients.to where:
        • .email.email is sender.email.email
      • any of:
        • length(recipients.cc) > 0
        • length(recipients.bcc) > 0
    • all of:
      • length(recipients.to) is 0
      • length(recipients.cc) is 1
      • sender.email.email is recipients.cc[0].email.email
    • all of:
      • length(recipients.to) is 1
      • length(recipients.cc) is 0
      • sender.email.email is recipients.to[0].email.email
  3. any of:
    • all of:
      • any of:
        • body.current_thread.text matches '(discuss.{0,15}purchas(e|ing))'
        • body.current_thread.text matches '(sign(ed?)|view).{0,10}(purchase order)|Request for (a Quot(e|ation)|Proposal)'
        • body.current_thread.text matches '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
        • subject.subject matches '(request for (purchase|quot(e|ation))|\\bRFQ\\b|\\bRFP\\b|bid invit(e|ation))'
        • any of attachments where:
          • .file_name matches '(purchase.?order|Quot(e|ation))'
        • any of ml.nlu_classifier(body.current_thread.text).tags where all hold:
          • .name is 'purchase_order'
          • .confidence is 'high'
        • any of ml.nlu_classifier(body.current_thread.text).entities where all hold:
          • .name is 'financial'
          • .text matches 'rfp|rfq'
        • any of ml.nlu_classifier(body.current_thread.text).entities where all hold:
          • .name is 'request'
          • .text contains 'submit bid'
      • at least 2 of:
        • body.current_thread.text matches '(discuss.{0,15}purchas(e|ing))'
        • body.current_thread.text matches '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
        • body.current_thread.text matches '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
        • body.current_thread.text matches '(?:invitation|intent) to bid'
        • subject.subject matches '(request for (purchase|quot(e|ation))|\\bRFQ\\b|\\bRFP\\b|bid invit(e|ation))'
        • any of attachments where:
          • .file_name matches '(purchase.?order|Quot(e|ation))'
        • any of ml.nlu_classifier(body.current_thread.text).tags where all hold:
          • .name is 'purchase_order'
          • .confidence is 'high'
        • any of ml.nlu_classifier(body.current_thread.text).entities where all hold:
          • .name is 'financial'
          • .text matches 'rfp|rfq'
        • all of:
          • any of ml.nlu_classifier(body.current_thread.text).entities where:
            • .name is 'request'
          • any of ml.nlu_classifier(body.current_thread.text).entities where:
            • .name is 'urgency'
          • not:
            • any of ml.nlu_classifier(body.current_thread.text).topics where all hold:
              • .name is 'Advertising and Promotions'
              • .confidence is 'high'
        • all of:
          • length(filter(body.links, .href_url.domain.domain in $free_subdomain_hosts or .href_url.domain.domain in $free_file_hosts or network.whois(.href_url.domain).days_old < 30 and regex.match(.display_text, '[A-Z ]+') or any(ml.nlu_classifier(.display_text).entities, .name in ('request', 'urgency')) or any(ml.nlu_classifier(.display_text).intents, .name in ('cred_theft')))) > 0
          • length(filter(body.links, .href_url.domain.domain in $free_subdomain_hosts or .href_url.domain.domain in $free_file_hosts or network.whois(.href_url.domain).days_old < 30 and regex.match(.display_text, '[A-Z ]+') or any(ml.nlu_classifier(.display_text).entities, .name in ('request', 'urgency')) or any(ml.nlu_classifier(.display_text).intents, .name in ('cred_theft')))) < 3
        • all of:
          • length(attachments) is 0
          • body.current_thread.text contains 'attached'
        • any of body.current_thread.links where:
          • .href_url.url matches 'RFP'
    • all of:
      • length(attachments) is 1
      • length(body.current_thread.text) < 100
      • all of attachments where all hold:
        • .file_type in $file_types_images
        • any of file.explode(.) where:
          • at least 2 of:
            • .scan.ocr.raw matches '(discuss.{0,15}purchas(e|ing))'
            • .scan.ocr.raw matches '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
            • .scan.ocr.raw matches '(please|kindly).{0,30}quote'
            • all of:
              • any of ml.nlu_classifier(.scan.ocr.raw).entities where:
                • .name is 'request'
              • any of ml.nlu_classifier(.scan.ocr.raw).entities where:
                • .name is 'urgency'
            • any of ml.nlu_classifier(.scan.ocr.raw).tags where all hold:
              • .name is 'purchase_order'
              • .confidence is 'high'
            • any of ml.nlu_classifier(.scan.ocr.raw).entities where all hold:
              • .name is 'financial'
              • .text matches 'rfp|rfq'
    • all of:
      • subject.subject matches 'project\\s+summary'
      • any of html.xpath(body.html, '//div[.//img[contains(@src,"pdf")]]//a').nodes where:
        • .display_text matches 'project\\s+summary'
      • body.current_thread.text matches '(put a bid|\\bbid\\s+(for|on)\\b|submit.{1,20}(bid|quot(e|ation))|request for (purchase|quot(e|ation))|\\bRFQ\\b|\\bRFP\\b|project\\s+summary)'
  4. not:
    • all of:
      • sender.email.domain.root_domain is 'wetransfer.com'
      • coalesce(headers.auth_summary.dmarc.pass)
  5. 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
  6. any of:
    • all of:
      • any of:
        • not:
          • profile.by_sender().solicited
        • profile.by_sender().days_since.last_contact > 30
      • not:
        • profile.by_sender().any_messages_benign
    • all of:
      • length(recipients.to) is 1
      • sender.email.email in map(recipients.to, .email.email)

Inspects: attachments[].file_name, attachments[].file_type, body.current_thread.links, body.current_thread.links[].href_url.url, body.current_thread.text, body.html, body.links, body.links[].display_text, body.links[].href_url.domain, body.links[].href_url.domain.domain, headers.auth_summary.dmarc.pass, headers.reply_to, headers.reply_to[].email.domain.root_domain, headers.reply_to[].email.email, recipients.bcc, recipients.cc, recipients.cc[0].email.email, recipients.to, recipients.to[0].email.email, recipients.to[].display_name, recipients.to[].email.email, sender.email.domain.root_domain, sender.email.email, subject.subject, type.inbound. Sensors: file.explode, html.xpath, ml.nlu_classifier, network.whois, profile.by_sender, regex.icontains, regex.imatch, regex.match, strings.icontains. Reference lists: $file_types_images, $free_email_providers, $free_file_hosts, $free_subdomain_hosts, $high_trust_sender_root_domains, $org_domains, $recipient_emails.

Indicators matched (33)

FieldMatchValue
recipients.to[].display_namememberUndisclosed recipients
recipients.to[].display_namememberundisclosed-recipients
regex.icontainsregex(discuss.{0,15}purchas(e|ing))
regex.icontainsregex(sign(ed?)|view).{0,10}(purchase order)|Request for (a Quot(e|ation)|Proposal)
regex.icontainsregex(please|kindly).{0,30}(?:proposal|quot(e|ation))
regex.icontainsregex(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))
regex.icontainsregex(purchase.?order|Quot(e|ation))
ml.nlu_classifier(body.current_thread.text).tags[].nameequalspurchase_order
ml.nlu_classifier(body.current_thread.text).tags[].confidenceequalshigh
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsfinancial
regex.imatchregexrfp|rfq
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsrequest
21 more
strings.icontainssubstringsubmit bid
regex.icontainsregex(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)
regex.icontainsregex(?:invitation|intent) to bid
ml.nlu_classifier(body.current_thread.text).entities[].nameequalsurgency
ml.nlu_classifier(body.current_thread.text).topics[].nameequalsAdvertising and Promotions
ml.nlu_classifier(body.current_thread.text).topics[].confidenceequalshigh
regex.matchregex[A-Z ]+
ml.nlu_classifier(body.links[].display_text).entities[].namememberrequest
ml.nlu_classifier(body.links[].display_text).entities[].namememberurgency
ml.nlu_classifier(body.links[].display_text).intents[].namemembercred_theft
strings.icontainssubstringattached
regex.icontainsregexRFP
regex.icontainsregex(please|kindly).{0,30}quote
ml.nlu_classifier(file.explode(attachments[])[].scan.ocr.raw).entities[].nameequalsrequest
ml.nlu_classifier(file.explode(attachments[])[].scan.ocr.raw).entities[].nameequalsurgency
ml.nlu_classifier(file.explode(attachments[])[].scan.ocr.raw).tags[].nameequalspurchase_order
ml.nlu_classifier(file.explode(attachments[])[].scan.ocr.raw).tags[].confidenceequalshigh
ml.nlu_classifier(file.explode(attachments[])[].scan.ocr.raw).entities[].nameequalsfinancial
regex.icontainsregexproject\s+summary
regex.icontainsregex(put a bid|\bbid\s+(for|on)\b|submit.{1,20}(bid|quot(e|ation))|request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|project\s+summary)
sender.email.domain.root_domainequalswetransfer.com