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|preferred).{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|preferred).{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 any(ml.nlu_classifier(body.current_thread.text).entities,
111 .name == "request" and regex.imatch(.text, "bid invitation")
112 ),
113 any(attachments,
114 any(file.explode(.),
115 any(ml.nlu_classifier(.scan.ocr.raw).entities,
116 .name == "request"
117 and regex.imatch(.text, "view (?:rfp|rfq)")
118 )
119 )
120 ),
121
122 // Non-keyword indicators
123 (
124 any(ml.nlu_classifier(body.current_thread.text).entities,
125 .name == "request"
126 )
127 and any(ml.nlu_classifier(body.current_thread.text).entities,
128 .name == "urgency"
129 )
130 and not any(ml.nlu_classifier(body.current_thread.text).topics,
131 .name == "Advertising and Promotions"
132 and .confidence == "high"
133 )
134 ),
135 (
136 0 < length(filter(body.links,
137 (
138 .href_url.domain.domain in $free_subdomain_hosts
139 or .href_url.domain.domain in $free_file_hosts
140 or network.whois(.href_url.domain).days_old < 30
141 )
142 and (
143 regex.match(.display_text, '[A-Z ]+')
144 or any(ml.nlu_classifier(.display_text).entities,
145 .name in ("request", "urgency")
146 )
147 or any(ml.nlu_classifier(.display_text).intents,
148 .name in ("cred_theft")
149 )
150 )
151 )
152 ) < 3
153 ),
154 // mentions an attachment that does not exist
155 (
156 length(attachments) == 0
157 and strings.icontains(body.current_thread.text, "attached")
158 ),
159 any(body.current_thread.links, regex.icontains(.href_url.url, 'RFP'))
160 )
161 )
162 )
163 or (
164 length(attachments) == 1
165 and length(body.current_thread.text) < 100
166 and all(attachments,
167 .file_type in $file_types_images
168 and any(file.explode(.),
169 2 of (
170 regex.icontains(.scan.ocr.raw,
171 '(discuss.{0,15}purchas(e|ing))'
172 ),
173 regex.icontains(.scan.ocr.raw,
174 '(sign(ed?)|view).{0,10}(purchase order)|Request for a Quot(e|ation)'
175 ),
176 regex.icontains(.scan.ocr.raw,
177 '(please|kindly).{0,30}quote'
178 ),
179 (
180 any(ml.nlu_classifier(.scan.ocr.raw).entities,
181 .name == "request"
182 )
183 and any(ml.nlu_classifier(.scan.ocr.raw).entities,
184 .name == "urgency"
185 )
186 ),
187 any(ml.nlu_classifier(.scan.ocr.raw).tags,
188 .name == "purchase_order" and .confidence == "high"
189 ),
190 any(ml.nlu_classifier(.scan.ocr.raw).entities,
191 .name == "financial"
192 and regex.imatch(.text, "rfp|rfq")
193 ),
194 )
195 )
196 )
197 )
198 // fake PDF file icon used as a link lure with bid solicitation language
199 or (
200 regex.icontains(subject.subject, 'project\s+summary')
201 and any(html.xpath(body.html, '//div[.//img[contains(@src,"pdf")]]//a').nodes,
202 regex.icontains(.display_text, 'project\s+summary')
203 )
204 and regex.icontains(body.current_thread.text,
205 '(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)'
206 )
207 )
208 )
209 // wetransfer includes user specific reply-to's & link display text which triggers NLU logic further within the rule
210 and not (
211 sender.email.domain.root_domain == "wetransfer.com"
212 and coalesce(headers.auth_summary.dmarc.pass, false)
213 )
214
215 // negate highly trusted sender domains unless they fail DMARC authentication
216 and not (
217 sender.email.domain.root_domain in $high_trust_sender_root_domains
218 and coalesce(headers.auth_summary.dmarc.pass, false)
219 )
220 and (
221 (
222 (
223 not profile.by_sender().solicited
224 or profile.by_sender().days_since.last_contact > 30
225 )
226 and not profile.by_sender().any_messages_benign
227 )
228 // sender address listed as a recipient
229 or (
230 length(recipients.to) == 1
231 and sender.email.email in map(recipients.to, .email.email)
232 )
233 )
234attack_types:
235 - "BEC/Fraud"
236tactics_and_techniques:
237 - "Evasion"
238 - "Free email provider"
239detection_methods:
240 - "Content analysis"
241 - "Natural Language Understanding"
242 - "URL analysis"
243id: "2ac0d329-c1fb-5c87-98dd-ea3e5b85377a"