Brand Impersonation: Capital One

This detection rule identifies inbound messages containing Capital One branding indicators in display names, sender addresses, message content, or embedded logos, while excluding legitimate Capital One domains and authenticated communications from known trusted senders.

Sublime rule (View on GitHub)

  1name: "Brand Impersonation: Capital One"
  2description: "This detection rule identifies inbound messages containing Capital One branding indicators in display names, sender addresses, message content, or embedded logos, while excluding legitimate Capital One domains and authenticated communications from known trusted senders."
  3type: "rule"
  4severity: "high"
  5source: |
  6  type.inbound
  7  and (
  8    any([
  9          strings.replace_confusables(sender.display_name),
 10          strings.replace_confusables(subject.subject),
 11          // domain parts of sender
 12          sender.email.local_part,
 13          sender.email.domain.sld
 14        ],
 15        // quick checks first
 16        strings.icontains(., 'Capital One')
 17        or strings.icontains(., 'CapitalOne')
 18  
 19        // slower checks next
 20        or regex.icontains(., 'Capital.?One')
 21        // levenshtein distince similar to captial one
 22        or strings.ilevenshtein(., 'Capital One') <= 2
 23    )
 24    or any(ml.logo_detect(beta.message_screenshot()).brands,
 25           .name == "Capital One Bank" and .confidence != "low"
 26    )
 27  )
 28  and not (
 29    sender.email.domain.root_domain in (
 30      "capitalone.co.uk",
 31      "capitalone.com",
 32      "capitaloneshopping.com",
 33      "capitalonesoftware.com",
 34      "capitalonebooking.com",
 35      "capitalonetravel.com",
 36      "olbanking.com", // a fiserv.one domain
 37      "bynder.com", // Digital Assest Mgmt
 38      "gcs-web.com", // investor relations run by capital one
 39      "capitalonearena.com", // the arena
 40      "monumentalsports.com", // the company that owns a bunch of teams that play at the arena?
 41      "ticketmaster.com", // sell and advertises tickets at Capital One Arena
 42    )
 43    and headers.auth_summary.dmarc.pass
 44  )
 45  // and the sender is not from high trust sender root domains
 46  and (
 47    (
 48      sender.email.domain.root_domain in $high_trust_sender_root_domains
 49      and not headers.auth_summary.dmarc.pass
 50    )
 51    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
 52  )
 53  and // suspicious indicators here
 54   (
 55    // // password theme
 56    (
 57      strings.icontains(body.current_thread.text, "new password")
 58      or regex.icontains(body.current_thread.text,
 59                         '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)',
 60                         '(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
 61                         '(?:short|weak|chang(?:e|ing)|reset)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
 62                         '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:short|weak|chang(?:e|ing)|reset)',
 63      )
 64    )
 65    // // login failures
 66    or (
 67      strings.icontains(body.current_thread.text, "unusual number of")
 68      or strings.icontains(body.current_thread.text, "security breach")
 69      or (
 70        strings.icontains(body.current_thread.text, "security alert")
 71        // some capital one notiifcaitons include directions to 
 72        // change notificaiton preferences to only security alerts
 73        and (
 74          strings.icount(body.current_thread.text, "security alert") > strings.icount(body.current_thread.text,
 75                                                                                      "sign in to your account and select Security Alerts."
 76          )
 77        )
 78      )
 79      or strings.icontains(body.current_thread.text, "account remains secure")
 80      or strings.icontains(body.current_thread.text, "please verify your account")
 81      or strings.icontains(body.current_thread.text,
 82                           "suspicious activity detected"
 83      )
 84      or strings.icontains(body.current_thread.text, "temporarily locked out")
 85      or regex.icontains(body.current_thread.text,
 86                         '(?:invalid|unrecognized|unauthorized|fail(?:ed|ure)?|suspicious|unusual|attempt(?:ed)?\b|tried to)\s*(?:\w+\s+){0,3}\s*(?:log(?:.?in)?|sign(?:.?in)?|account|access|activity)',
 87                         '(?:log(?:.?in)?|sign(?:.?in)?|account|access|activity)\s*(?:\w+\s+){0,3}\s*(?:invalid|unrecognized|fail(?:ed|ure)?|suspicious|unusual|attempt(?:ed)?\b)'
 88      )
 89    )
 90    // // account locked
 91    or (
 92      strings.icontains(body.current_thread.text, "been suspend")
 93      or strings.icontains(body.current_thread.text, "will be restored")
 94      or strings.icontains(body.current_thread.text, "security reasons")
 95      or strings.icontains(body.current_thread.text,
 96                           "temporarily restricted access"
 97      )
 98      or regex.icontains(body.current_thread.text,
 99                         'acc(?:ou)?n?t\s*(?:\w+\s+){0,3}\s*(?:authenticat(?:e|ion)|activity|\bho[li]d\b|terminat|[il1]{2}m[il1]t(?:s|ed|ation)|b?locked|de-?activat|suspen(?:ed|sion)|restrict(?:ed|ion)?|expir(?:ed?|ing)|v[il]o[li]at|verif(?:y|ication))',
100                         '(?:authenticat(?:e|ion)|activity|\bho[li]d\b|terminat|[il1]{2}m[il1]t(?:s|ed|ation)|b?locked|de-?activat|suspen(?:ed|sion)|restrict(?:ed|ion)?|expir(?:ed?|ing)|v[il]o[li]at|verif(?:y|ication))\s*(?:\w+\s+){0,3}\s*acc(?:ou)?n?t\b'
101      )
102    )
103    // // secure messages
104    or (
105      regex.icontains(body.current_thread.text,
106                      '(?:encrypt(?:ion|ed)?|secur(?:ed?|ity)) (?:\w+\s+){0,3}\s*message'
107      )
108      or strings.icontains(body.current_thread.text, "document portal")
109      or regex.icontains(body.current_thread.text,
110                         "has been (?:encrypt|sent secure)"
111      )
112      or regex.icontains(body.current_thread.text,
113                         'encryption (?:\w+\s+){0,3}\s*tech'
114      )
115    )
116    // // documents to view
117    or (
118      // we can skip the regex if the diplay_text doesn't contain document
119      // this might need to be removed if the regex is expanded
120      strings.icontains(body.current_thread.text, 'document')
121      and regex.icontains(body.current_thread.text,
122                          'document\s*(?:\w+\s+){0,3}\s*(?:ready|posted|review|available|online)',
123                          '(?:ready|posted|review|available|online)\s*(?:\w+\s+){0,3}\s*document'
124      )
125    )
126    // // account/profile details 
127    or (
128      strings.icontains(body.current_thread.text, "about your account")
129      or strings.icontains(body.current_thread.text, "action required")
130      or regex.icontains(body.current_thread.text,
131                         '(update|\bedit\b|modify|revise|verif(?:y|ication)|discrepanc(?:y|ies)|mismatch(?:es)?|inconsistenc(?:y|ies)?|difference(?:s)?|anomal(?:y|ies)?|irregularit(?:y|ies)?)\s*(?:\w+\s+){0,4}\s*(?:account|ownership|detail|record|data|info(?:rmation)?)',
132                         '(?:account|ownership|detail|record|data|info(?:rmation)?)\s*(?:\w+\s+){0,4}\s*(update|\bedit\b|modify|revise|verif(?:y|ication)|discrepanc(?:y|ies)|mismatch(?:es)?|inconsistenc(?:y|ies)?|difference(?:s)?|anomal(?:y|ies)?|irregularit(?:y|ies)?)'
133      )
134    )
135    // // other calls to action that are unexpected
136    or (strings.icontains(body.current_thread.text, "download the attachment"))
137  
138    // the links contain suspect wording
139    or (
140      0 < length(body.links) <= 50
141      and any(body.links,
142              (
143                regex.icontains(.display_text, '(?:log|sign).?in')
144                or strings.icontains(.display_text, 'confirm')
145                or strings.icontains(.display_text, 'i recongize it')
146                or strings.icontains(.display_text, "something\'s wrong")
147                or regex.icontains(.display_text,
148                                   '(?:(?:re)?view|see|read)\s*(?:\w+\s*){0,3}\s*(?:document|message|now|account)'
149                )
150                or regex.icontains(.display_text,
151                                   'restore\s*(?:\w+\s*){0,3}\s*(?:account|access)'
152                )
153                or regex.icontains(.display_text,
154                                   'review\s*(?:\w+\s*){0,3}\s*(?:payment)'
155                )
156              )
157              and not regex.icontains(.display_text,
158                                      'confirm\s*(?:\w+\s*){0,3}\s*this message'
159              )
160              and .href_url.domain.root_domain != "capitalone.com"
161      )
162    )
163    // the message contains a disclaimer but isn't from capitalone
164    or (
165      regex.icontains(body.current_thread.text,
166                      'To ensure delivery, add [^\@]+@[^\s]*capitalone.com to your address book.'
167      )
168      and sender.email.domain.root_domain != "capitalone.com"
169    )
170  )
171  // negation of inbound org domains which path eamil auth
172  and not (
173    type.inbound
174    and sender.email.domain.domain in $org_domains
175    and headers.auth_summary.spf.pass
176    and headers.auth_summary.dmarc.pass
177    and not 'fail' in~ distinct(map(headers.hops, .authentication_results.dkim))
178  )
179  and not any(beta.ml_topic(body.html.display_text).topics,
180              (
181                .name in (
182                  // lots of newsletters talk about capital one 
183                  "Newsletters and Digests",
184                  // lots of recruiting mention oppurtunties at capital one, often including the logo
185                  "Professional and Career Development",
186                )
187                and .confidence == "high"
188              )
189              or (
190                .name in (
191                  // Outage events are often news worthy
192                  "News and Current Events"
193                )
194                and .confidence != "low"
195              )
196  )
197  // negating legit replies/forwards
198  // https://github.com/sublime-security/sublime-rules/blob/main/insights/authentication/org_inbound_auth_pass.yml
199  and not (
200    (
201      strings.istarts_with(subject.subject, "RE:")
202      or strings.istarts_with(subject.subject, "FW:")
203      or strings.istarts_with(subject.subject, "FWD:")
204      or regex.imatch(subject.subject,
205                      '(\[[^\]]+\]\s?){0,3}(re|fwd?|automat.*)\s?:.*'
206      )
207      or strings.istarts_with(subject.subject, "Réponse automatique")
208    )
209    and (
210      length(headers.references) > 0
211      and any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
212    )
213  )
214  // negate bounce backs
215  and not (
216    strings.like(sender.email.local_part,
217                 "*postmaster*",
218                 "*mailer-daemon*",
219                 "*administrator*"
220    )
221    and any(attachments,
222            .content_type in (
223              "message/rfc822",
224              "message/delivery-status",
225              "text/calendar"
226            )
227    )
228  )  
229attack_types:
230  - "Credential Phishing"
231tactics_and_techniques:
232  - "Impersonation: Brand"
233  - "Lookalike domain"
234  - "Social engineering"
235detection_methods:
236  - "Computer Vision"
237  - "Sender analysis"
238  - "Header analysis"
239id: "d53848e4-fc40-5bd1-ad5e-c9c4e85a669f"
to-top