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