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"