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    or (
 42      length(recipients.to) == 0
 43      and length(recipients.cc) == 1
 44      and sender.email.email == recipients.cc[0].email.email
 45    )
 46    or (
 47      length(recipients.to) == 1
 48      and length(recipients.cc) == 0
 49      and sender.email.email == recipients.to[0].email.email
 50    )
 51  )
 52  and (
 53    // Group the keyword patterns that specifically indicate RFQ/RFP
 54    (
 55      1 of (
 56        // RFQ/RFP specific language patterns
 57        regex.icontains(body.current_thread.text,
 58                        '(discuss.{0,15}purchas(e|ing))'
 59        ),
 60        regex.icontains(body.current_thread.text,
 61                        '(sign(ed?)|view).{0,10}(purchase order)|Request for (a Quot(e|ation)|Proposal)'
 62        ),
 63        regex.icontains(body.current_thread.text,
 64                        '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
 65        ),
 66        regex.icontains(subject.subject,
 67                        '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
 68        ),
 69        any(attachments,
 70            regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
 71        ),
 72        any(ml.nlu_classifier(body.current_thread.text).tags,
 73            .name == "purchase_order" and .confidence == "high"
 74        ),
 75        any(ml.nlu_classifier(body.current_thread.text).entities,
 76            .name == "financial" and regex.imatch(.text, "rfp|rfq")
 77        ),
 78        any(ml.nlu_classifier(body.current_thread.text).entities,
 79            .name == "request" and strings.icontains(.text, 'submit bid')
 80        )
 81      )
 82      // Required: at least one RFQ/RFP keyword pattern
 83  
 84      // Optional: at least one additional indicator (can be another keyword pattern or a non-keyword indicator)
 85      and (
 86        2 of (
 87          // RFQ/RFP keyword patterns (same as above)
 88          regex.icontains(body.current_thread.text,
 89                          '(discuss.{0,15}purchas(e|ing))'
 90          ),
 91          regex.icontains(body.current_thread.text,
 92                          '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
 93          ),
 94          regex.icontains(body.current_thread.text,
 95                          '(please|kindly).{0,30}(?:proposal|quot(e|ation))'
 96          ),
 97          regex.icontains(body.current_thread.text,
 98                          '(?:invitation|intent) to bid'
 99          ),
100          regex.icontains(subject.subject,
101                          '(request for (purchase|quot(e|ation))|\bRFQ\b|\bRFP\b|bid invit(e|ation))'
102          ),
103          any(attachments,
104              regex.icontains(.file_name, "(purchase.?order|Quot(e|ation))")
105          ),
106          any(ml.nlu_classifier(body.current_thread.text).tags,
107              .name == "purchase_order" and .confidence == "high"
108          ),
109          any(ml.nlu_classifier(body.current_thread.text).entities,
110              .name == "financial" and regex.imatch(.text, "rfp|rfq")
111          ),
112  
113          // Non-keyword indicators
114          (
115            any(ml.nlu_classifier(body.current_thread.text).entities,
116                .name == "request"
117            )
118            and any(ml.nlu_classifier(body.current_thread.text).entities,
119                    .name == "urgency"
120            )
121            and not any(beta.ml_topic(body.current_thread.text).topics,
122                        .name == "Advertising and Promotions"
123                        and .confidence == "high"
124            )
125          ),
126          (
127            0 < length(filter(body.links,
128                              (
129                                .href_url.domain.domain in $free_subdomain_hosts
130                                or .href_url.domain.domain in $free_file_hosts
131                                or network.whois(.href_url.domain).days_old < 30
132                              )
133                              and (
134                                regex.match(.display_text, '[A-Z ]+')
135                                or any(ml.nlu_classifier(.display_text).entities,
136                                       .name in ("request", "urgency")
137                                )
138                                or any(ml.nlu_classifier(.display_text).intents,
139                                       .name in ("cred_theft")
140                                )
141                              )
142                       )
143            ) < 3
144          ),
145          // mentions an attachment that does not exist
146          (
147            length(attachments) == 0
148            and strings.icontains(body.current_thread.text, "attached")
149          ),
150          any(body.current_thread.links, regex.icontains(.href_url.url, 'RFP'))
151        )
152      )
153    )
154    or (
155      length(attachments) == 1
156      and length(body.current_thread.text) < 100
157      and all(attachments,
158              .file_type in $file_types_images
159              and any(file.explode(.),
160                      2 of (
161                        regex.icontains(.scan.ocr.raw,
162                                        '(discuss.{0,15}purchas(e|ing))'
163                        ),
164                        regex.icontains(.scan.ocr.raw,
165                                        '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
166                        ),
167                        regex.icontains(.scan.ocr.raw,
168                                        '(please|kindly).{0,30}quote'
169                        ),
170                        (
171                          any(ml.nlu_classifier(.scan.ocr.raw).entities,
172                              .name == "request"
173                          )
174                          and any(ml.nlu_classifier(.scan.ocr.raw).entities,
175                                  .name == "urgency"
176                          )
177                        ),
178                        any(ml.nlu_classifier(.scan.ocr.raw).tags,
179                            .name == "purchase_order" and .confidence == "high"
180                        ),
181                        any(ml.nlu_classifier(.scan.ocr.raw).entities,
182                            .name == "financial"
183                            and regex.imatch(.text, "rfp|rfq")
184                        ),
185                      )
186              )
187      )
188    )
189  )
190  
191  // negate highly trusted sender domains unless they fail DMARC authentication
192  and (
193    (
194      sender.email.domain.root_domain in $high_trust_sender_root_domains
195      and not headers.auth_summary.dmarc.pass
196    )
197    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
198  )
199  and (
200    (
201      (
202        not profile.by_sender().solicited
203        or profile.by_sender().days_since.last_contact > 30
204      )
205      and not profile.by_sender().any_messages_benign
206    )
207    // sender address listed as a recipient
208    or (
209      length(recipients.to) == 1
210      and sender.email.email in map(recipients.to, .email.email)
211    )
212  )  
213attack_types:
214  - "BEC/Fraud"
215tactics_and_techniques:
216  - "Evasion"
217  - "Free email provider"
218detection_methods:
219  - "Content analysis"
220  - "Natural Language Understanding"
221  - "URL analysis"
222id: "2ac0d329-c1fb-5c87-98dd-ea3e5b85377a"
to-top