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

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.

Sublime rule (View on GitHub)

  1name: "Request for Quote or Purchase (RFQ|RFP) with suspicious sender or recipient pattern"
  2description: |
  3  RFQ/RFP scams involve fraudulent emails posing as legitimate requests for quotations or purchases, often sent by scammers impersonating reputable organizations.
  4  These scams aim to deceive recipients into providing sensitive information or conducting unauthorized transactions, often leading to financial loss, or data leakage.  
  5type: "rule"
  6severity: "medium"
  7source: |
  8  type.inbound
  9  and (
 10    (
 11      (
 12        length(recipients.to) == 0
 13        or all(recipients.to,
 14               .display_name in (
 15                 "Undisclosed recipients",
 16                 "undisclosed-recipients"
 17               )
 18        )
 19      )
 20      and length(recipients.cc) == 0
 21    )
 22    or (
 23      sender.email.domain.root_domain in $free_email_providers
 24      and any(headers.reply_to, .email.email != sender.email.email)
 25      and any(headers.reply_to, .email.email not in $recipient_emails)
 26    )
 27    or (
 28      length(headers.reply_to) > 0
 29      and all(headers.reply_to,
 30              .email.domain.root_domain != sender.email.domain.root_domain
 31              and not .email.domain.root_domain in $org_domains
 32              // wetransfer includes user specific reply-to's & link display text which triggers NLU logic further within the rule
 33              and not sender.email.domain.root_domain == "wetransfer.com"
 34      )
 35    )
 36    or (
 37      length(recipients.to) == 1
 38      and all(recipients.to, .email.email == sender.email.email)
 39      and (length(recipients.cc) > 0 or length(recipients.bcc) > 0)
 40    )
 41  )
 42  and (
 43    // Group the keyword patterns that specifically indicate RFQ/RFP
 44    (
 45      1 of (
 46        // RFQ/RFP specific language patterns
 47        regex.icontains(body.current_thread.text,
 48                        '(discuss.{0,15}purchas(e|ing))'
 49        ),
 50        regex.icontains(body.current_thread.text,
 51                        '(sign(ed?)|view).{0,10}(purchase order)|Request for (a Quot(e|ation)|Proposal)'
 52        ),
 53        regex.icontains(body.current_thread.text,
 54                        '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
 55        ),
 56        regex.icontains(subject.subject,
 57                        '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
 58        ),
 59        any(attachments,
 60            regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
 61        ),
 62        any(ml.nlu_classifier(body.current_thread.text).tags,
 63            .name == "purchase_order" and .confidence == "high"
 64        ),
 65        any(ml.nlu_classifier(body.current_thread.text).entities,
 66            .name == "financial" and regex.imatch(.text, "rfp|rfq")
 67        )
 68      )
 69      // Required: at least one RFQ/RFP keyword pattern
 70  
 71      // Optional: at least one additional indicator (can be another keyword pattern or a non-keyword indicator)
 72      and (
 73        2 of (
 74          // RFQ/RFP keyword patterns (same as above)
 75          regex.icontains(body.current_thread.text,
 76                          '(discuss.{0,15}purchas(e|ing))'
 77          ),
 78          regex.icontains(body.current_thread.text,
 79                          '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
 80          ),
 81          regex.icontains(body.current_thread.text,
 82                          '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
 83          ),
 84          regex.icontains(subject.subject,
 85                          '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
 86          ),
 87          any(attachments,
 88              regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
 89          ),
 90          any(ml.nlu_classifier(body.current_thread.text).tags,
 91              .name == "purchase_order" and .confidence == "high"
 92          ),
 93          any(ml.nlu_classifier(body.current_thread.text).entities,
 94              .name == "financial" and regex.imatch(.text, "rfp|rfq")
 95          ),
 96  
 97          // Non-keyword indicators
 98          (
 99            any(ml.nlu_classifier(body.current_thread.text).entities,
100                .name == "request"
101            )
102            and any(ml.nlu_classifier(body.current_thread.text).entities,
103                    .name == "urgency"
104            )
105            and not any(beta.ml_topic(body.current_thread.text).topics,
106                        .name == "Advertising and Promotions"
107                        and .confidence == "high"
108            )
109          ),
110          (
111            0 < length(filter(body.links,
112                              (
113                                .href_url.domain.domain in $free_subdomain_hosts
114                                or .href_url.domain.domain in $free_file_hosts
115                                or network.whois(.href_url.domain).days_old < 30
116                              )
117                              and (
118                                regex.match(.display_text, '[A-Z ]+')
119                                or any(ml.nlu_classifier(.display_text).entities,
120                                       .name in ("request", "urgency")
121                                )
122                                or any(ml.nlu_classifier(.display_text).intents,
123                                       .name in ("cred_theft")
124                                )
125                              )
126                       )
127            ) < 3
128          ),
129          // mentions an attachment that does not exist
130          (
131            length(attachments) == 0
132            and strings.icontains(body.current_thread.text, "attached")
133          )
134        )
135      )
136    )
137    or (
138      length(attachments) == 1
139      and length(body.current_thread.text) < 100
140      and all(attachments,
141              .file_type in $file_types_images
142              and any(file.explode(.),
143                      2 of (
144                        regex.icontains(.scan.ocr.raw,
145                                        '(discuss.{0,15}purchas(e|ing))'
146                        ),
147                        regex.icontains(.scan.ocr.raw,
148                                        '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
149                        ),
150                        regex.icontains(.scan.ocr.raw,
151                                        '(please|kindly).{0,30}quote'
152                        ),
153                        (
154                          any(ml.nlu_classifier(.scan.ocr.raw).entities,
155                              .name == "request"
156                          )
157                          and any(ml.nlu_classifier(.scan.ocr.raw).entities,
158                                  .name == "urgency"
159                          )
160                        ),
161                        any(ml.nlu_classifier(.scan.ocr.raw).tags,
162                            .name == "purchase_order" and .confidence == "high"
163                        ),
164                        any(ml.nlu_classifier(.scan.ocr.raw).entities,
165                            .name == "financial"
166                            and regex.imatch(.text, "rfp|rfq")
167                        ),
168                      )
169              )
170      )
171    )
172  )
173  
174  // negate highly trusted sender domains unless they fail DMARC authentication
175  and (
176    (
177      sender.email.domain.root_domain in $high_trust_sender_root_domains
178      and not headers.auth_summary.dmarc.pass
179    )
180    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
181  )
182  and (
183    not profile.by_sender().solicited
184    or profile.by_sender().days_since.last_contact > 30
185  )
186  and not profile.by_sender().any_messages_benign  
187attack_types:
188  - "BEC/Fraud"
189tactics_and_techniques:
190  - "Evasion"
191  - "Free email provider"
192detection_methods:
193  - "Content analysis"
194  - "Natural Language Understanding"
195  - "URL analysis"
196id: "2ac0d329-c1fb-5c87-98dd-ea3e5b85377a"
to-top