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"