Description

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.

References

No references.

Sublime Security
Created Feb 11th, 2025 • Last updated Feb 20th, 2025
Source
type.inbound
and (
  any([
        strings.replace_confusables(sender.display_name),
        strings.replace_confusables(subject.subject),
        // domain parts of sender
        sender.email.local_part,
        sender.email.domain.sld
      ],
      // quick checks first
      strings.icontains(., 'Capital One')
      or strings.icontains(., 'CapitalOne')

      // slower checks next
      or regex.icontains(., 'Capital.?One')
      // levenshtein distince similar to captial one
      or strings.ilevenshtein(., 'Capital One') <= 2
  )
  or any(ml.logo_detect(beta.message_screenshot()).brands,
         .name == "Capital One Bank" and .confidence != "low"
  )
)
and not (
  sender.email.domain.root_domain in (
    "capitalone.co.uk",
    "capitalone.com",
    "capitaloneshopping.com",
    "capitalonesoftware.com",
    "capitalonebooking.com",
    "capitalonetravel.com",
    "olbanking.com", // a fiserv.one domain
    "bynder.com", // Digital Assest Mgmt
    "gcs-web.com", // investor relations run by capital one
    "capitalonearena.com", // the arena
    "monumentalsports.com", // the company that owns a bunch of teams that play at the arena?
    "ticketmaster.com", // sell and advertises tickets at Capital One Arena
  )
  and headers.auth_summary.dmarc.pass
)
// and the sender is not from high trust sender root domains
and (
  (
    sender.email.domain.root_domain in $high_trust_sender_root_domains
    and not headers.auth_summary.dmarc.pass
  )
  or sender.email.domain.root_domain not in $high_trust_sender_root_domains
)
and // suspicious indicators here
 (
  // // password theme
  (
    strings.icontains(body.current_thread.text, "new password")
    or regex.icontains(body.current_thread.text,
                       '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)',
                       '(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
                       '(?:short|weak|chang(?:e|ing)|reset)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
                       '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:short|weak|chang(?:e|ing)|reset)',
    )
  )
  // // login failures
  or (
    strings.icontains(body.current_thread.text, "unusual number of")
    or strings.icontains(body.current_thread.text, "security breach")
    or (
      strings.icontains(body.current_thread.text, "security alert")
      // some capital one notiifcaitons include directions to 
      // change notificaiton preferences to only security alerts
      and (
        strings.icount(body.current_thread.text, "security alert") > strings.icount(body.current_thread.text,
                                                                                    "sign in to your account and select Security Alerts."
        )
      )
    )
    or strings.icontains(body.current_thread.text, "account remains secure")
    or strings.icontains(body.current_thread.text, "please verify your account")
    or strings.icontains(body.current_thread.text,
                         "suspicious activity detected"
    )
    or strings.icontains(body.current_thread.text, "temporarily locked out")
    or regex.icontains(body.current_thread.text,
                       '(?: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)',
                       '(?:log(?:.?in)?|sign(?:.?in)?|account|access|activity)\s*(?:\w+\s+){0,3}\s*(?:invalid|unrecognized|fail(?:ed|ure)?|suspicious|unusual|attempt(?:ed)?\b)'
    )
  )
  // // account locked
  or (
    strings.icontains(body.current_thread.text, "been suspend")
    or strings.icontains(body.current_thread.text, "will be restored")
    or strings.icontains(body.current_thread.text, "security reasons")
    or strings.icontains(body.current_thread.text,
                         "temporarily restricted access"
    )
    or regex.icontains(body.current_thread.text,
                       '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))',
                       '(?: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'
    )
  )
  // // secure messages
  or (
    regex.icontains(body.current_thread.text,
                    '(?:encrypt(?:ion|ed)?|secur(?:ed?|ity)) (?:\w+\s+){0,3}\s*message'
    )
    or strings.icontains(body.current_thread.text, "document portal")
    or regex.icontains(body.current_thread.text,
                       "has been (?:encrypt|sent secure)"
    )
    or regex.icontains(body.current_thread.text,
                       'encryption (?:\w+\s+){0,3}\s*tech'
    )
  )
  // // documents to view
  or (
    // we can skip the regex if the diplay_text doesn't contain document
    // this might need to be removed if the regex is expanded
    strings.icontains(body.current_thread.text, 'document')
    and regex.icontains(body.current_thread.text,
                        'document\s*(?:\w+\s+){0,3}\s*(?:ready|posted|review|available|online)',
                        '(?:ready|posted|review|available|online)\s*(?:\w+\s+){0,3}\s*document'
    )
  )
  // // account/profile details 
  or (
    strings.icontains(body.current_thread.text, "about your account")
    or strings.icontains(body.current_thread.text, "action required")
    or regex.icontains(body.current_thread.text,
                       '(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)?)',
                       '(?: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)?)'
    )
  )
  // // other calls to action that are unexpected
  or (strings.icontains(body.current_thread.text, "download the attachment"))

  // the links contain suspect wording
  or (
    0 < length(body.links) <= 50
    and any(body.links,
            (
              regex.icontains(.display_text, '(?:log|sign).?in')
              or strings.icontains(.display_text, 'confirm')
              or strings.icontains(.display_text, 'i recongize it')
              or strings.icontains(.display_text, "something\'s wrong")
              or regex.icontains(.display_text,
                                 '(?:(?:re)?view|see|read)\s*(?:\w+\s*){0,3}\s*(?:document|message|now|account)'
              )
              or regex.icontains(.display_text,
                                 'restore\s*(?:\w+\s*){0,3}\s*(?:account|access)'
              )
              or regex.icontains(.display_text,
                                 'review\s*(?:\w+\s*){0,3}\s*(?:payment)'
              )
            )
            and not regex.icontains(.display_text,
                                    'confirm\s*(?:\w+\s*){0,3}\s*this message'
            )
            and .href_url.domain.root_domain != "capitalone.com"
    )
  )
  // the message contains a disclaimer but isn't from capitalone
  or (
    regex.icontains(body.current_thread.text,
                    'To ensure delivery, add [^\@]+@[^\s]*capitalone.com to your address book.'
    )
    and sender.email.domain.root_domain != "capitalone.com"
  )
)
// negation of inbound org domains which path eamil auth
and not (
  type.inbound
  and sender.email.domain.domain in $org_domains
  and headers.auth_summary.spf.pass
  and headers.auth_summary.dmarc.pass
  and not 'fail' in~ distinct(map(headers.hops, .authentication_results.dkim))
)
and not any(beta.ml_topic(body.html.display_text).topics,
            (
              .name in (
                // lots of newsletters talk about capital one 
                "Newsletters and Digests",
                // lots of recruiting mention oppurtunties at capital one, often including the logo
                "Professional and Career Development",
              )
              and .confidence == "high"
            )
            or (
              .name in (
                // Outage events are often news worthy
                "News and Current Events"
              )
              and .confidence != "low"
            )
)
// negating legit replies/forwards
// https://github.com/sublime-security/sublime-rules/blob/main/insights/authentication/org_inbound_auth_pass.yml
and not (
  (
    strings.istarts_with(subject.subject, "RE:")
    or strings.istarts_with(subject.subject, "FW:")
    or strings.istarts_with(subject.subject, "FWD:")
    or regex.imatch(subject.subject,
                    '(\[[^\]]+\]\s?){0,3}(re|fwd?|automat.*)\s?:.*'
    )
    or strings.istarts_with(subject.subject, "Réponse automatique")
  )
  and (
    length(headers.references) > 0
    and any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
  )
)
// negate bounce backs
and not (
  strings.like(sender.email.local_part,
               "*postmaster*",
               "*mailer-daemon*",
               "*administrator*"
  )
  and any(attachments,
          .content_type in (
            "message/rfc822",
            "message/delivery-status",
            "text/calendar"
          )
  )
)
MQL Rule Console
•Docs•Learning Labs

Playground

Test against your own EMLs or sample data.

Share

Post about this on your socials.

Get Started. Today.

Managed or self-managed. No MX changes.