1+ name : " PR# 4687 - Request for Quote or Purchase (RFQ|RFP) with suspicious sender or recipient pattern"
2+ description : |
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.
5+ type : " rule"
6+ severity : " medium"
7+ source : |
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 != "low"
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+ )
226+ attack_types :
227+ - " BEC/Fraud"
228+ tactics_and_techniques :
229+ - " Evasion"
230+ - " Free email provider"
231+ detection_methods :
232+ - " Content analysis"
233+ - " Natural Language Understanding"
234+ - " URL analysis"
235+ id : " 3e99fe16-b030-5b6f-a269-77d378795ae7"
236+ tags :
237+ - created_from_open_prs
238+ - rule_status_modified
239+ - pr_author_D-Bolton
240+ references :
241+ - https://github.com/sublime-security/sublime-rules/pull/4687
0 commit comments