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