Detection rules › Panther

Panther rules: snowflake

RuleSeverity
Query.Snowflake.AccountAdminGranted
Query.Snowflake.BruteForceByIp
Query.Snowflake.BruteForceByUsername
Query.Snowflake.ClientIp
Query.Snowflake.ConfigurationDrift
Query.Snowflake.CopyIntoStage
Query.Snowflake.External.Shares
Query.Snowflake.FailedLogins
Query.Snowflake.FileDownloaded
Query.Snowflake.KeyUserPasswordLogin
Query.Snowflake.MFALogin
Query.Snowflake.Multiple.Logins.Followed.By.Success
Query.Snowflake.SuspectedUserAccess
Query.Snowflake.TempStageCreated
Query.Snowflake.ThreatHunting.ClientIp
Query.Snowflake.ThreatHunting.ConfigurationDrift
Query.Snowflake.ThreatHunting.SuspectedUserAccess
Query.Snowflake.ThreatHunting.SuspectedUserActivity
Query.Snowflake.UserCreated
Query.Snowflake.UserEnabled
Snowflake Account Admin Grantedmedium
Snowflake Account Admin Grantedmedium
Snowflake Brute Force Attacks by IPmedium
Snowflake Brute Force Attacks by IPmedium
Snowflake Brute Force Attacks by Usermedium
Snowflake Brute Force Attacks by Usernamemedium
Snowflake Brute Force Login Successhigh
Snowflake Client IPhigh
Snowflake Configuration Driftmedium
Snowflake Data Exfiltrationcritical
Snowflake Data Exfiltrationcritical
Snowflake External Data Sharemedium
Snowflake External Sharemedium
Snowflake File Downloadedinformational
Snowflake File Downloadedinformational
Snowflake Grant to Public Rolemedium
Snowflake Login Without MFAmedium
Snowflake Login Without MFAmedium
Snowflake Multiple Failed Logins Followed By Successmedium
Snowflake Password Spraymedium
Snowflake Successful Logininformational
Snowflake Table Copied Into Stageinformational
Snowflake Table Copied Into Stageinformational
Snowflake Temporary Stage Createdinformational
Snowflake Temporary Stage Createdinformational
Snowflake User Accesshigh
Snowflake User Createdinformational
Snowflake User Createdinformational
Snowflake User Daily Query Volume Spikelow
Snowflake User Daily Query Volume Spike
Snowflake User Daily Query Volume Spike - Threat Hunting
Snowflake User Enabledinformational
Snowflake User Enabledinformational
Snowflake user with key-based auth logged in with password authmedium
Suspicious Snowflake Sessions - Unusual Applicationlow
Suspicious Snowflake Sessions - Unusual Application

Query.Snowflake.AccountAdminGranted

#
Source
github.com/panther-labs/panther-analysis

Monitor and detect granting account admin role.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.AccountAdminGranted"
Enabled: false
Description: >
  Monitor and detect granting account admin role.
Query: |
  --return instances where active (not deleted) roles are granted within the last 24 hours

  --this was adapted from a Security Feature Checklist query

  SELECT
    created_on as p_event_time,
    role,
    grantee_name as granted_to,
    granted_by
  FROM snowflake.account_usage.grants_to_users
  WHERE
    p_event_time is NOT NULL
    AND grantee_name is NOT NULL
    AND granted_to is NOT NULL
    AND role ILIKE '%admin%'
    AND deleted_on is NULL
    AND p_occurs_since('1 day')
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.grants_to_users

Stage 2: filter

  • p_event_time is present
  • grantee_name is present
  • granted_to is present
  • role matches the pattern *admin* (case-insensitive)
  • deleted_on is empty
Window
1d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
deleted_onis_null
  • (no value, null check)
field:"deleted_on" kind:is_null
granted_tois_not_null
  • (no value, null check)
field:"granted_to" kind:is_not_null
grantee_nameis_not_null
  • (no value, null check)
field:"grantee_name" kind:is_not_null
p_event_timeis_not_null
  • (no value, null check)
field:"p_event_time" kind:is_not_null
rolewildcard
  • *admin* transforms: nocase
field:"role" kind:wildcard value:"*admin*"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timecreated_on
role
granted_tograntee_name
granted_by

Query.Snowflake.BruteForceByIp

#
Source
github.com/panther-labs/panther-analysis

Detect brute force attempts by monitoring for failed logins to snowflake.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.BruteForceByIp"
Enabled: false
Description: >
  Detect brute force attempts by monitoring for failed logins to snowflake.
Query: |
  --return IPs with more than 5 failed logins in the previous 24 hours

  --this was adapted from a SnowAlert query

  SELECT
    client_ip,
    reported_client_type,
    ARRAY_AGG(DISTINCT error_code) as error_codes,
    ARRAY_AGG(DISTINCT error_message) as error_messages,
    COUNT(event_id) AS counts
  FROM snowflake.account_usage.login_history
  WHERE
    DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24
    AND event_type = 'LOGIN'
    AND error_code is NOT NULL
    AND error_code != 394304 -- Ignore JWT Fingerprint Mismatch
  GROUP BY client_ip, reported_client_type
  HAVING counts >= 5;
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • event_type is LOGIN
  • error_code is present
  • error_code is not 394304
Grouped by
client_ip, reported_client_type

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Stage 3: having

  • counts is at least 5

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
client_ip
reported_client_type
error_codesARRAY_AGG ( DISTINCT error_code )
error_messagesARRAY_AGG ( DISTINCT error_message )
countsCOUNT ( event_id )

Query.Snowflake.BruteForceByUsername

#
Source
github.com/panther-labs/panther-analysis

Detect brute force attempts by monitoring for failed logins to snowflake.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.BruteForceByUsername"
Enabled: false
Description: >
  Detect brute force attempts by monitoring for failed logins to snowflake.
Query: |
  --return users with more than 5 failed logins in the previous 24 hours

  --this was adapted from a SnowAlert query

  SELECT
    user_name,
    reported_client_type,
    ARRAY_AGG(DISTINCT error_code) as error_codes,
    ARRAY_AGG(DISTINCT error_message) as error_messages,
    COUNT(event_id) AS counts
  FROM snowflake.account_usage.login_history
  WHERE
    DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24
    AND event_type = 'LOGIN'
    AND error_code IS NOT NULL
    AND error_code != 394304 -- Ignore JWT Fingerprint Mismatch
  GROUP BY reported_client_type, user_name
  HAVING counts >=5;
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • event_type is LOGIN
  • error_code is present
  • error_code is not 394304
Grouped by
reported_client_type, user_name

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Stage 3: having

  • counts is at least 5

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_name
reported_client_type
error_codesARRAY_AGG ( DISTINCT error_code )
error_messagesARRAY_AGG ( DISTINCT error_message )
countsCOUNT ( event_id )

Query.Snowflake.ClientIp

#
Source
github.com/panther-labs/panther-analysis

Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: scheduled_query
Enabled: false
QueryName: "Query.Snowflake.ClientIp"
Description: >
  Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024
Query: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    event_timestamp as p_event_time,
    *
  FROM
      snowflake.account_usage.login_history
  WHERE
      client_ip IN (
  '104.223.91.28',
  '198.54.135.99',
  '184.147.100.29',
  '146.70.117.210',
  '198.54.130.153',
  '169.150.203.22',
  '185.156.46.163',
  '146.70.171.99',
  '206.217.206.108',
  '45.86.221.146',
  '193.32.126.233',
  '87.249.134.11',
  '66.115.189.247',
  '104.129.24.124',
  '146.70.171.112',
  '198.54.135.67',
  '146.70.124.216',
  '45.134.142.200',
  '206.217.205.49',
  '146.70.117.56',
  '169.150.201.25',
  '66.63.167.147',
  '194.230.144.126',
  '146.70.165.227',
  '154.47.30.137',
  '154.47.30.150',
  '96.44.191.140',
  '146.70.166.176',
  '198.44.136.56',
  '176.123.6.193',
  '192.252.212.60',
  '173.44.63.112',
  '37.19.210.34',
  '37.19.210.21',
  '185.213.155.241',
  '198.44.136.82',
  '93.115.0.49',
  '204.152.216.105',
  '198.44.129.82',
  '185.248.85.59',
  '198.54.131.152',
  '102.165.16.161',
  '185.156.46.144',
  '45.134.140.144',
  '198.54.135.35',
  '176.123.3.132',
  '185.248.85.14',
  '169.150.223.208',
  '162.33.177.32',
  '194.230.145.67',
  '5.47.87.202',
  '194.230.160.5',
  '194.230.147.127',
  '176.220.186.152',
  '194.230.160.237',
  '194.230.158.178',
  '194.230.145.76',
  '45.155.91.99',
  '194.230.158.107',
  '194.230.148.99',
  '194.230.144.50',
  '185.204.1.178',
  '79.127.217.44',
  '104.129.24.115',
  '146.70.119.24',
  '138.199.34.144'
      )
  AND p_occurs_since('1 day')
  ORDER BY p_event_time
  LIMIT 100;
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • client_ip is one of 104.223.91.28, 198.54.135.99, 184.147.100.29, 146.70.117.210, 198.54.130.153 (+61 more values, see Indicators below)
Window
1d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
client_ipin
  • 102.165.16.161
  • 104.129.24.115
  • 104.129.24.124
  • 104.223.91.28
  • 138.199.34.144
  • 146.70.117.210
  • 146.70.117.56
  • 146.70.119.24
  • 146.70.124.216
  • 146.70.165.227
  • 146.70.166.176
  • 146.70.171.112
  • 146.70.171.99
  • 154.47.30.137
  • 154.47.30.150
  • 162.33.177.32
  • 169.150.201.25
  • 169.150.203.22
  • 169.150.223.208
  • 173.44.63.112
  • 176.123.3.132
  • 176.123.6.193
  • 176.220.186.152
  • 184.147.100.29
  • 185.156.46.144
  • 185.156.46.163
  • 185.204.1.178
  • 185.213.155.241
  • 185.248.85.14
  • 185.248.85.59
  • 192.252.212.60
  • 193.32.126.233
  • 194.230.144.126
  • 194.230.144.50
  • 194.230.145.67
  • 194.230.145.76
  • 194.230.147.127
  • 194.230.148.99
  • 194.230.158.107
  • 194.230.158.178
  • +26 more values (see full rule source)
field:"client_ip" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timeevent_timestamp
*

Query.Snowflake.ConfigurationDrift

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Tags
Configuration Required
Source
github.com/panther-labs/panther-analysis

Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: scheduled_query
Enabled: false
QueryName: "Query.Snowflake.ConfigurationDrift"
Description: >
  Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024
Tags:
  - Configuration Required
SnowflakeQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- adjust query/limit to narrow as necessary

  SELECT
      query_text,
      user_name,
      role_name,
      start_time as p_event_time,
      end_time
  FROM snowflake.account_usage.query_history
    WHERE ((execution_status = 'SUCCESS'
      AND query_type NOT in ('SELECT', 'EXPLAIN')
      AND (
        user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
        AND NOT (
          role_name LIKE 'PANTHER_READONLY%'
          AND query_text LIKE 'COPY INTO @panther_lookups.public.panther_lut_export_stage%'
        )
      )
      AND user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
      AND (query_text ILIKE '%create role%'
          OR query_text ILIKE '%manage grants%'
          OR query_text ILIKE '%create integration%'
          OR query_text ILIKE '%alter integration%'
          OR query_text ILIKE '%create share%'
          OR query_text ILIKE '%create account%'
          OR query_text ILIKE '%monitor usage%'
          OR query_text ILIKE '%ownership%'
          OR query_text ILIKE '%drop table%'
          OR query_text ILIKE '%drop database%'
          OR query_text ILIKE '%create stage%'
          OR query_text ILIKE '%drop stage%'
          OR query_text ILIKE '%alter stage%'
          OR query_text ILIKE '%create user%'
          OR query_text ILIKE '%alter user%'
          OR query_text ILIKE '%drop user%'
          OR query_text ILIKE '%create_network_policy%'
          OR query_text ILIKE '%alter_network_policy%'
          OR query_text ILIKE '%drop_network_policy%'
          OR query_text ILIKE '%copy%'
          )
      ) OR (
        query_text ilike '%grant%accountadmin%to%'
        AND query_type = 'GRANT'
        AND execution_status = 'SUCCESS'
      ))
    AND p_occurs_since('1 day')
    ORDER BY end_time desc
    LIMIT 100;

DatabricksQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- adjust query/limit to narrow as necessary

  SELECT
      query_text,
      user_name,
      role_name,
      start_time as p_event_time,
      end_time
  FROM panther_logs.snowflake_queryhistory
    WHERE ((execution_status = 'SUCCESS'
      AND query_type NOT in ('SELECT', 'EXPLAIN')
      AND (
        user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
        AND NOT (
          role_name LIKE 'PANTHER_READONLY%'
          AND query_text LIKE 'COPY INTO @panther_lookups.panther_lut_export_stage%'
        )
      )
      AND user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
      AND (query_text ILIKE '%create role%'
          OR query_text ILIKE '%manage grants%'
          OR query_text ILIKE '%create integration%'
          OR query_text ILIKE '%alter integration%'
          OR query_text ILIKE '%create share%'
          OR query_text ILIKE '%create account%'
          OR query_text ILIKE '%monitor usage%'
          OR query_text ILIKE '%ownership%'
          OR query_text ILIKE '%drop table%'
          OR query_text ILIKE '%drop database%'
          OR query_text ILIKE '%create stage%'
          OR query_text ILIKE '%drop stage%'
          OR query_text ILIKE '%alter stage%'
          OR query_text ILIKE '%create user%'
          OR query_text ILIKE '%alter user%'
          OR query_text ILIKE '%drop user%'
          OR query_text ILIKE '%create_network_policy%'
          OR query_text ILIKE '%alter_network_policy%'
          OR query_text ILIKE '%drop_network_policy%'
          OR query_text ILIKE '%copy%'
          )
      ) OR (
        query_text ilike '%grant%accountadmin%to%'
        AND query_type = 'GRANT'
        AND execution_status = 'SUCCESS'
      ))
    AND p_occurs_since('1 day')
    ORDER BY end_time desc
    LIMIT 100
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.query_history

Stage 2: filter

  • any of:
    • all of:
      • execution_status is SUCCESS
      • query_type is not one of SELECT, EXPLAIN
      • user_name is not one of PANTHER_ADMIN, PANTHERACCOUNTADMIN
      • any of:
        • role_name does not match the pattern PANTHER?READONLY*
        • query_text does not match the pattern COPY INTO @panther?lookups.public.panther?lut?export?stage*
      • user_name is not one of PANTHER_ADMIN, PANTHERACCOUNTADMIN
      • any of:
        • query_text matches the pattern *create role* (case-insensitive)
        • query_text matches the pattern *manage grants* (case-insensitive)
        • query_text matches the pattern *create integration* (case-insensitive)
        • query_text matches the pattern *alter integration* (case-insensitive)
        • query_text matches the pattern *create share* (case-insensitive)
        • query_text matches the pattern *create account* (case-insensitive)
        • query_text matches the pattern *monitor usage* (case-insensitive)
        • query_text matches the pattern *ownership* (case-insensitive)
        • query_text matches the pattern *drop table* (case-insensitive)
        • query_text matches the pattern *drop database* (case-insensitive)
        • query_text matches the pattern *create stage* (case-insensitive)
        • query_text matches the pattern *drop stage* (case-insensitive)
        • query_text matches the pattern *alter stage* (case-insensitive)
        • query_text matches the pattern *create user* (case-insensitive)
        • query_text matches the pattern *alter user* (case-insensitive)
        • query_text matches the pattern *drop user* (case-insensitive)
        • query_text matches the pattern *create?network?policy* (case-insensitive)
        • query_text matches the pattern *alter?network?policy* (case-insensitive)
        • query_text matches the pattern *drop?network?policy* (case-insensitive)
        • query_text matches the pattern *copy* (case-insensitive)
    • all of:
      • query_text matches the pattern *grant*accountadmin*to* (case-insensitive)
      • query_type is GRANT
      • execution_status is SUCCESS
Window
1d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
execution_statuseq
  • SUCCESS
field:"execution_status" kind:eq value:"SUCCESS"
query_textwildcard
  • *alter integration* transforms: nocase
  • *alter stage* transforms: nocase
  • *alter user* transforms: nocase
  • *alter?network?policy* transforms: nocase
  • *copy* transforms: nocase
  • *create account* transforms: nocase
  • *create integration* transforms: nocase
  • *create role* transforms: nocase
  • *create share* transforms: nocase
  • *create stage* transforms: nocase
  • *create user* transforms: nocase
  • *create?network?policy* transforms: nocase
  • *drop database* transforms: nocase
  • *drop stage* transforms: nocase
  • *drop table* transforms: nocase
  • *drop user* transforms: nocase
  • *drop?network?policy* transforms: nocase
  • *grant*accountadmin*to* transforms: nocase
  • *manage grants* transforms: nocase
  • *monitor usage* transforms: nocase
  • *ownership* transforms: nocase
field:"query_text" kind:wildcard
query_typeeq
  • GRANT
field:"query_type" kind:eq value:"GRANT"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
query_text
user_name
role_name
p_event_timestart_time
end_time

Query.Snowflake.CopyIntoStage

#

MITRE ATT&CK coverage

TacticTechniques
ExfiltrationNo specific technique

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.CopyIntoStage"
Enabled: true
Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
SnowflakeQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_substr(query_text, 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 2) as stage,
    regexp_substr(query_text, 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s+FROM', 1, 1, 'i', 2) as path,
    query_text

    FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
    WHERE query_type = 'UNLOAD'
        AND stage IS NOT NULL
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100

DatabricksQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_extract(query_text, '(?i)COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 2) as stage,
    regexp_extract(query_text, '(?i)COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s+FROM', 2) as path,
    query_text

    FROM panther_logs.snowflake_queryhistory
    WHERE query_type = 'UNLOAD'
        AND regexp_extract(query_text, '(?i)COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 2) != ''
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100
Schedule:
    RateMinutes: 1440
    TimeoutMinutes: 5
Tags:
    - data exfil

Stages and Predicates

Stage 1: source

Table
SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY

Stage 2: filter

  • query_type is UNLOAD
  • stage is present
  • execution_status is SUCCESS
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_name
role_name
p_event_timestart_time
query_type
execution_status
stageregexp_substr ( query_text , 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)' , 1 , 1 , 'i' , 2 )
pathregexp_substr ( query_text , 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s+FROM' , 1 , 1 , 'i' , 2 )
query_text

Query.Snowflake.External.Shares

#
Source
github.com/panther-labs/panther-analysis

Monitor for external shares from one cloud source to another.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.External.Shares"
Enabled: false
Description: >
  Monitor for external shares from one cloud source to another.
Query: |
  --return external shares

  --this was adapted from a Security Feature Checklist query

  SELECT
    start_time as p_event_time,
    *
  FROM snowflake.account_usage.data_transfer_history
    WHERE
      p_occurs_since('1 day')
      AND p_event_time IS NOT NULL
      AND source_cloud IS NOT NULL
      AND target_cloud IS NOT NULL
      AND bytes_transferred > 0
  ORDER BY p_event_time desc
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.data_transfer_history

Stage 2: filter

  • p_event_time is present
  • source_cloud is present
  • target_cloud is present
  • bytes_transferred is greater than 0
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timestart_time
*

Query.Snowflake.FailedLogins

#

This is an enrichment or summary query that produces aggregate or lookup data for other rules to consume, not a standalone detection. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Detect brute force attempts by monitoring for failed logins to snowflake.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.FailedLogins"
Enabled: false
Description: >
  Detect brute force attempts by monitoring for failed logins to snowflake.
Query: |
  --return all failed failed logins in the previous 24 hours

  --this was adapted from a SnowAlert query

  SELECT
    client_ip,
    user_name,
    reported_client_type,
    error_code,
    error_message,
    count(distinct event_id) over (partition by client_ip) as count_by_ip,
    count(distinct event_id) over (partition by user_name) as count_by_username
  FROM snowflake.account_usage.login_history
  WHERE
    p_occurs_since(1d, , event_timestamp)
    AND event_type = 'LOGIN'
    AND error_code is NOT NULL
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • event_type is LOGIN
  • error_code is present
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
client_ip
user_name
reported_client_type
error_code
error_message
count_by_ipcount ( DISTINCT event_id ) OVER ( PARTITION BY client_ip )
count_by_usernamecount ( DISTINCT event_id ) OVER ( PARTITION BY user_name )

Query.Snowflake.FileDownloaded

#

MITRE ATT&CK coverage

TacticTechniques
ExfiltrationNo specific technique

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.FileDownloaded"
Enabled: true
Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
SnowflakeQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_substr(query_text, 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 2) as stage,
    regexp_substr(query_text, 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s', 1, 1, 'i', 2) as path,
    query_text

    FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
    WHERE query_type = 'GET_FILES'
        AND path IS NOT NULL
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100

DatabricksQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_extract(query_text, '(?i)GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)', 2) as stage,
    regexp_extract(query_text, '(?i)GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s', 2) as path,
    query_text

    FROM panther_logs.snowflake_queryhistory
    WHERE query_type = 'GET_FILES'
        AND regexp_extract(query_text, '(?i)GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s', 2) != ''
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100
Schedule:
    RateMinutes: 1440
    TimeoutMinutes: 5
Tags:
    - data exfil

Stages and Predicates

Stage 1: source

Table
SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY

Stage 2: filter

  • query_type is GET_FILES
  • path is present
  • execution_status is SUCCESS
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_name
role_name
p_event_timestart_time
query_type
execution_status
stageregexp_substr ( query_text , 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)' , 1 , 1 , 'i' , 2 )
pathregexp_substr ( query_text , 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s' , 1 , 1 , 'i' , 2 )
query_text

Query.Snowflake.KeyUserPasswordLogin

#
Source
github.com/panther-labs/panther-analysis

Detects when a user with a configured RSA key logs in with a password

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.KeyUserPasswordLogin"
Enabled: false
Description: >
  Detects when a user with a configured RSA key logs in with a password
Query: |
  --return instances where a user who has key-based login configured logs in with a password
  --this was adapted from a Security Feature Checklist query

   SELECT
     u.name,
     first_authentication_factor,
     second_authentication_factor,
     count(*) as counts
  FROM snowflake.account_usage.login_history as l
  JOIN snowflake.account_usage.users u on l.user_name = u.name and has_rsa_public_key = 'true'
   WHERE is_success = 'YES'
   AND first_authentication_factor != 'RSA_KEYPAIR'
   AND DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24
   GROUP BY name, first_authentication_factor, second_authentication_factor
   ORDER BY count(*) desc
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • is_success is YES
  • first_authentication_factor is not RSA_KEYPAIR
Grouped by
name, first_authentication_factor, second_authentication_factor

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
u.name
first_authentication_factor
second_authentication_factor
countscount ( * )

Query.Snowflake.MFALogin

#
Source
github.com/panther-labs/panther-analysis

Monitor logins that are not using MFA.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.MFALogin"
Enabled: false
Description: >
  Monitor logins that are not using MFA.
Query: |
  --return instances where a user logs in without MFA

  --this was adapted from a Security Feature Checklist query

  SELECT
    event_timestamp as p_event_time,
    user_name,
    client_ip,
    reported_client_type,
    reported_client_version,
    first_authentication_factor,
    second_authentication_factor
  FROM snowflake.account_usage.login_history
    WHERE
      p_occurs_since('1 day')
      AND event_type = 'LOGIN'
      AND first_authentication_factor = 'PASSWORD'
      AND second_authentication_factor IS null
  ORDER BY p_event_time desc
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • event_type is LOGIN
  • first_authentication_factor is PASSWORD
  • second_authentication_factor is empty
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timeevent_timestamp
user_name
client_ip
reported_client_type
reported_client_version
first_authentication_factor
second_authentication_factor

Query.Snowflake.Multiple.Logins.Followed.By.Success

#
Source
github.com/panther-labs/panther-analysis

Monitor for brute force user activity.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.Multiple.Logins.Followed.By.Success"
Enabled: false
Description: >
  Monitor for brute force user activity.
Query: |
  --return multiple failed logins followed by a success

  WITH login_attempts as (
  SELECT
      user_name,
      client_ip,
      reported_client_type,
      error_code,
      error_message,
      event_id,
      event_timestamp as p_event_time
    FROM snowflake.account_usage.login_history
    WHERE
      p_occurs_since('1 day')
      AND event_type = 'LOGIN'
      AND client_ip != '0.0.0.0' -- filtering out unnecessary 'elided' snowflake entries
  )
  SELECT
  *
  FROM login_attempts
  MATCH_RECOGNIZE(
      PARTITION BY client_ip, user_name
      ORDER BY p_event_time DESC -- backwards in time
      MEASURES
        match_number() as match_number,
        first(p_event_time) as successful_login_time,
        last(p_event_time) as start_of_unsuccessful_logins_time,
        count(*) as rows_in_sequence,
        count(row_with_success.*) as num_successes,
        count(row_with_fail.*) as num_fails,
        ARRAY_AGG(DISTINCT error_code) as error_codes,
        ARRAY_AGG(DISTINCT error_message) as error_messages
      ONE ROW PER MATCH
      AFTER MATCH SKIP TO LAST row_with_fail
      -- a success with fails following
      PATTERN(row_with_success row_with_fail+)
      DEFINE
        row_with_success AS error_message is null,
        row_with_fail AS error_message is not null
    )
  HAVING num_fails >= 5 -- changeable per environment
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
login_attempts

Stage 2: filter

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

Query.Snowflake.SuspectedUserAccess

#
Source
github.com/panther-labs/panther-analysis

Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: scheduled_query
Enabled: false
QueryName: "Query.Snowflake.SuspectedUserAccess"
Description: >
  Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024
SnowflakeQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    CREATED_ON as p_event_time,
    *
  FROM
      snowflake.account_usage.sessions
  WHERE
      PARSE_JSON(CLIENT_ENVIRONMENT):APPLICATION = 'rapeflake'
      OR
      (
          PARSE_JSON(CLIENT_ENVIRONMENT):APPLICATION = 'DBeaver_DBeaverUltimate'
          AND
          PARSE_JSON(CLIENT_ENVIRONMENT):OS = 'Windows Server 2022'
      )
  AND p_occurs_since('1 day')
  ORDER BY p_event_time
  LIMIT 100;

DatabricksQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    CREATED_ON as p_event_time,
    *
  FROM
      panther_logs.snowflake_sessions
  WHERE
      (
          CLIENT_ENVIRONMENT.APPLICATION = 'rapeflake'
          OR
          (
              CLIENT_ENVIRONMENT.APPLICATION = 'DBeaver_DBeaverUltimate'
              AND
              CLIENT_ENVIRONMENT.OS = 'Windows Server 2022'
          )
      )
  AND p_occurs_since('1 day')
  ORDER BY p_event_time
  LIMIT 100
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.sessions

Stage 2: filter

aggregate comparison (=); threshold not fully parsed

Window
1d

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timeCREATED_ON
*

Query.Snowflake.TempStageCreated

#

MITRE ATT&CK coverage

TacticTechniques
ExfiltrationNo specific technique

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.TempStageCreated"
Enabled: true
Description: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
SnowflakeQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_substr(query_text, 'CREATE\\s+(OR\\s+REPLACE\\s+)?(TEMPORARY\\s+|TEMP\\s+)STAGE\\s+(IF\\s+NOT\\s+EXISTS\\s+)?([a-zA-Z0-9_\\.]+)', 1, 1, 'i', 4) as stage,
    query_text

    FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
    WHERE query_type = 'CREATE'
        AND stage IS NOT NULL
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100

DatabricksQuery: |
    SELECT
    user_name,
    role_name,
    start_time AS p_event_time,
    query_type,
    execution_status,
    regexp_extract(query_text, '(?i)CREATE\\s+(OR\\s+REPLACE\\s+)?(TEMPORARY\\s+|TEMP\\s+)STAGE\\s+(IF\\s+NOT\\s+EXISTS\\s+)?([a-zA-Z0-9_\\.]+)', 4) as stage,
    query_text

    FROM panther_logs.snowflake_queryhistory
    WHERE query_type = 'CREATE'
        AND regexp_extract(query_text, '(?i)CREATE\\s+(OR\\s+REPLACE\\s+)?(TEMPORARY\\s+|TEMP\\s+)STAGE\\s+(IF\\s+NOT\\s+EXISTS\\s+)?([a-zA-Z0-9_\\.]+)', 4) != ''
        AND p_occurs_since('1 day')
        AND execution_status = 'SUCCESS'
    LIMIT 100
Schedule:
    RateMinutes: 1440
    TimeoutMinutes: 5
Tags:
    - data exfil

Stages and Predicates

Stage 1: source

Table
SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY

Stage 2: filter

  • query_type is CREATE
  • stage is present
  • execution_status is SUCCESS
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_name
role_name
p_event_timestart_time
query_type
execution_status
stageregexp_substr ( query_text , 'CREATE\\s+(OR\\s+REPLACE\\s+)?(TEMPORARY\\s+|TEMP\\s+)STAGE\\s+(IF\\s+NOT\\s+EXISTS\\s+)?([a-zA-Z0-9_\\.]+)' , 1 , 1 , 'i' , 4 )
query_text

Query.Snowflake.ThreatHunting.ClientIp

#
Source
github.com/panther-labs/panther-analysis

Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: saved_query
QueryName: "Query.Snowflake.ThreatHunting.ClientIp"
Description: >
  Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024
Query: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    event_timestamp as p_event_time,
    *
  FROM
      snowflake.account_usage.login_history
  WHERE
      client_ip IN (
  '104.223.91.28',
  '198.54.135.99',
  '184.147.100.29',
  '146.70.117.210',
  '198.54.130.153',
  '169.150.203.22',
  '185.156.46.163',
  '146.70.171.99',
  '206.217.206.108',
  '45.86.221.146',
  '193.32.126.233',
  '87.249.134.11',
  '66.115.189.247',
  '104.129.24.124',
  '146.70.171.112',
  '198.54.135.67',
  '146.70.124.216',
  '45.134.142.200',
  '206.217.205.49',
  '146.70.117.56',
  '169.150.201.25',
  '66.63.167.147',
  '194.230.144.126',
  '146.70.165.227',
  '154.47.30.137',
  '154.47.30.150',
  '96.44.191.140',
  '146.70.166.176',
  '198.44.136.56',
  '176.123.6.193',
  '192.252.212.60',
  '173.44.63.112',
  '37.19.210.34',
  '37.19.210.21',
  '185.213.155.241',
  '198.44.136.82',
  '93.115.0.49',
  '204.152.216.105',
  '198.44.129.82',
  '185.248.85.59',
  '198.54.131.152',
  '102.165.16.161',
  '185.156.46.144',
  '45.134.140.144',
  '198.54.135.35',
  '176.123.3.132',
  '185.248.85.14',
  '169.150.223.208',
  '162.33.177.32',
  '194.230.145.67',
  '5.47.87.202',
  '194.230.160.5',
  '194.230.147.127',
  '176.220.186.152',
  '194.230.160.237',
  '194.230.158.178',
  '194.230.145.76',
  '45.155.91.99',
  '194.230.158.107',
  '194.230.148.99',
  '194.230.144.50',
  '185.204.1.178',
  '79.127.217.44',
  '104.129.24.115',
  '146.70.119.24',
  '138.199.34.144'
      )
  ORDER BY p_event_time
  LIMIT 100;

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.login_history

Stage 2: filter

  • client_ip is one of 104.223.91.28, 198.54.135.99, 184.147.100.29, 146.70.117.210, 198.54.130.153 (+61 more values, see Indicators below)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
client_ipin
  • 102.165.16.161
  • 104.129.24.115
  • 104.129.24.124
  • 104.223.91.28
  • 138.199.34.144
  • 146.70.117.210
  • 146.70.117.56
  • 146.70.119.24
  • 146.70.124.216
  • 146.70.165.227
  • 146.70.166.176
  • 146.70.171.112
  • 146.70.171.99
  • 154.47.30.137
  • 154.47.30.150
  • 162.33.177.32
  • 169.150.201.25
  • 169.150.203.22
  • 169.150.223.208
  • 173.44.63.112
  • 176.123.3.132
  • 176.123.6.193
  • 176.220.186.152
  • 184.147.100.29
  • 185.156.46.144
  • 185.156.46.163
  • 185.204.1.178
  • 185.213.155.241
  • 185.248.85.14
  • 185.248.85.59
  • 192.252.212.60
  • 193.32.126.233
  • 194.230.144.126
  • 194.230.144.50
  • 194.230.145.67
  • 194.230.145.76
  • 194.230.147.127
  • 194.230.148.99
  • 194.230.158.107
  • 194.230.158.178
  • +26 more values (see full rule source)
field:"client_ip" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timeevent_timestamp
*

Query.Snowflake.ThreatHunting.ConfigurationDrift

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: saved_query
QueryName: "Query.Snowflake.ThreatHunting.ConfigurationDrift"
Description: >
  Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024
SnowflakeQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- adjust query/limit to narrow as necessary

  SELECT
      query_text,
      user_name,
      role_name,
      start_time as p_event_time,
      end_time
  FROM snowflake.account_usage.query_history
    WHERE ((execution_status = 'SUCCESS'
      AND query_type NOT in ('SELECT')
      AND user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
      AND (query_text ILIKE '%create role%'
          OR query_text ILIKE '%manage grants%'
          OR query_text ILIKE '%create integration%'
          OR query_text ILIKE '%alter integration%'
          OR query_text ILIKE '%create share%'
          OR query_text ILIKE '%create account%'
          OR query_text ILIKE '%monitor usage%'
          OR query_text ILIKE '%ownership%'
          OR query_text ILIKE '%drop table%'
          OR query_text ILIKE '%drop database%'
          OR query_text ILIKE '%create stage%'
          OR query_text ILIKE '%drop stage%'
          OR query_text ILIKE '%alter stage%'
          OR query_text ILIKE '%create user%'
          OR query_text ILIKE '%alter user%'
          OR query_text ILIKE '%drop user%'
          OR query_text ILIKE '%create_network_policy%'
          OR query_text ILIKE '%alter_network_policy%'
          OR query_text ILIKE '%drop_network_policy%'
          OR query_text ILIKE '%copy%'
          )
      ) OR (
        query_text ilike '%grant%accountadmin%to%'
        AND query_type = 'GRANT'
        AND execution_status = 'SUCCESS'
      ))
    ORDER BY end_time desc
    LIMIT 100;

DatabricksQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- adjust query/limit to narrow as necessary

  SELECT
      query_text,
      user_name,
      role_name,
      start_time as p_event_time,
      end_time
  FROM panther_logs.snowflake_queryhistory
    WHERE ((execution_status = 'SUCCESS'
      AND query_type NOT in ('SELECT')
      AND user_name NOT in ('PANTHER_ADMIN', 'PANTHERACCOUNTADMIN')
      AND (query_text ILIKE '%create role%'
          OR query_text ILIKE '%manage grants%'
          OR query_text ILIKE '%create integration%'
          OR query_text ILIKE '%alter integration%'
          OR query_text ILIKE '%create share%'
          OR query_text ILIKE '%create account%'
          OR query_text ILIKE '%monitor usage%'
          OR query_text ILIKE '%ownership%'
          OR query_text ILIKE '%drop table%'
          OR query_text ILIKE '%drop database%'
          OR query_text ILIKE '%create stage%'
          OR query_text ILIKE '%drop stage%'
          OR query_text ILIKE '%alter stage%'
          OR query_text ILIKE '%create user%'
          OR query_text ILIKE '%alter user%'
          OR query_text ILIKE '%drop user%'
          OR query_text ILIKE '%create_network_policy%'
          OR query_text ILIKE '%alter_network_policy%'
          OR query_text ILIKE '%drop_network_policy%'
          OR query_text ILIKE '%copy%'
          )
      ) OR (
        query_text ilike '%grant%accountadmin%to%'
        AND query_type = 'GRANT'
        AND execution_status = 'SUCCESS'
      ))
    ORDER BY end_time desc
    LIMIT 100

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.query_history

Stage 2: filter

  • any of:
    • all of:
      • execution_status is SUCCESS
      • query_type is not one of SELECT
      • user_name is not one of PANTHER_ADMIN, PANTHERACCOUNTADMIN
      • any of:
        • query_text matches the pattern *create role* (case-insensitive)
        • query_text matches the pattern *manage grants* (case-insensitive)
        • query_text matches the pattern *create integration* (case-insensitive)
        • query_text matches the pattern *alter integration* (case-insensitive)
        • query_text matches the pattern *create share* (case-insensitive)
        • query_text matches the pattern *create account* (case-insensitive)
        • query_text matches the pattern *monitor usage* (case-insensitive)
        • query_text matches the pattern *ownership* (case-insensitive)
        • query_text matches the pattern *drop table* (case-insensitive)
        • query_text matches the pattern *drop database* (case-insensitive)
        • query_text matches the pattern *create stage* (case-insensitive)
        • query_text matches the pattern *drop stage* (case-insensitive)
        • query_text matches the pattern *alter stage* (case-insensitive)
        • query_text matches the pattern *create user* (case-insensitive)
        • query_text matches the pattern *alter user* (case-insensitive)
        • query_text matches the pattern *drop user* (case-insensitive)
        • query_text matches the pattern *create?network?policy* (case-insensitive)
        • query_text matches the pattern *alter?network?policy* (case-insensitive)
        • query_text matches the pattern *drop?network?policy* (case-insensitive)
        • query_text matches the pattern *copy* (case-insensitive)
    • all of:
      • query_text matches the pattern *grant*accountadmin*to* (case-insensitive)
      • query_type is GRANT
      • execution_status is SUCCESS

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
execution_statuseq
  • SUCCESS
field:"execution_status" kind:eq value:"SUCCESS"
query_textwildcard
  • *alter integration* transforms: nocase
  • *alter stage* transforms: nocase
  • *alter user* transforms: nocase
  • *alter?network?policy* transforms: nocase
  • *copy* transforms: nocase
  • *create account* transforms: nocase
  • *create integration* transforms: nocase
  • *create role* transforms: nocase
  • *create share* transforms: nocase
  • *create stage* transforms: nocase
  • *create user* transforms: nocase
  • *create?network?policy* transforms: nocase
  • *drop database* transforms: nocase
  • *drop stage* transforms: nocase
  • *drop table* transforms: nocase
  • *drop user* transforms: nocase
  • *drop?network?policy* transforms: nocase
  • *grant*accountadmin*to* transforms: nocase
  • *manage grants* transforms: nocase
  • *monitor usage* transforms: nocase
  • *ownership* transforms: nocase
field:"query_text" kind:wildcard
query_typeeq
  • GRANT
field:"query_type" kind:eq value:"GRANT"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
query_text
user_name
role_name
p_event_timestart_time
end_time

Query.Snowflake.ThreatHunting.SuspectedUserAccess

#
Source
github.com/panther-labs/panther-analysis

Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: saved_query
QueryName: "Query.Snowflake.ThreatHunting.SuspectedUserAccess"
Description: >
  Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024
SnowflakeQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    CREATED_ON as p_event_time,
    *
  FROM
      snowflake.account_usage.sessions
  WHERE
      PARSE_JSON(CLIENT_ENVIRONMENT):APPLICATION = 'rapeflake'
      OR
      (
          PARSE_JSON(CLIENT_ENVIRONMENT):APPLICATION = 'DBeaver_DBeaverUltimate'
          AND
          PARSE_JSON(CLIENT_ENVIRONMENT):OS = 'Windows Server 2022'
      )
  ORDER BY p_event_time
  LIMIT 100;

DatabricksQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  SELECT
    CREATED_ON as p_event_time,
    *
  FROM
      panther_logs.snowflake_sessions
  WHERE
      CLIENT_ENVIRONMENT.APPLICATION = 'rapeflake'
      OR
      (
          CLIENT_ENVIRONMENT.APPLICATION = 'DBeaver_DBeaverUltimate'
          AND
          CLIENT_ENVIRONMENT.OS = 'Windows Server 2022'
      )
  ORDER BY p_event_time
  LIMIT 100

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.sessions

Stage 2: filter

aggregate comparison (=); threshold not fully parsed

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timeCREATED_ON
*

Query.Snowflake.ThreatHunting.SuspectedUserActivity

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Return actions/queries made by suspected users as part of ongoing cyber threat activity reported May 31st, 2024

Rule specification

AnalysisType: saved_query
QueryName: "Query.Snowflake.ThreatHunting.SuspectedUserActivity"
Description: >
  Return actions/queries made by suspected users as part of ongoing cyber threat activity reported May 31st, 2024
SnowflakeQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- replace <SUSPECTED_USER> with actual user name

  SELECT
    *
  FROM
      snowflake.account_usage.query_history
  WHERE
      user_name = '<SUSPECTED_USER>'
      AND start_time BETWEEN '2024-04-01'
      AND CURRENT_TIMESTAMP
  ORDER BY
  start_time
  LIMIT 100;

DatabricksQuery: |
  -- https://community.snowflake.com/s/article/Communication-ID-0108977-Additional-Information

  -- replace <SUSPECTED_USER> with actual user name

  SELECT
    *
  FROM
      panther_logs.snowflake_queryhistory
  WHERE
      user_name = '<SUSPECTED_USER>'
      AND start_time BETWEEN '2024-04-01'
      AND CURRENT_TIMESTAMP
  ORDER BY
  start_time
  LIMIT 100

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.query_history

Stage 2: filter

  • user_name is <SUSPECTED_USER>
  • start_time is at least 2024-04-01

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

Query.Snowflake.UserCreated

#
Source
github.com/panther-labs/panther-analysis

Monitor for new users.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.UserCreated"
Enabled: false
Description: >
  Monitor for new users.
SnowflakeQuery: |
  --return create user events

  --this was adapted from a Security Feature Checklist query

  SELECT
    start_time as p_event_time,
    end_time,
    query_type,
    query_text,
    user_name,
    role_name
  FROM snowflake.account_usage.query_history
    WHERE
      p_occurs_since('1 day')
      AND execution_status = 'SUCCESS'
      AND query_type = 'CREATE_USER'
      AND query_text ILIKE '%create%user%'
  ORDER BY end_time desc

DatabricksQuery: |
  --return create user events

  --this was adapted from a Security Feature Checklist query

  SELECT
    start_time as p_event_time,
    end_time,
    query_type,
    query_text,
    user_name,
    role_name
  FROM panther_logs.snowflake_queryhistory
    WHERE
      p_occurs_since('1 day')
      AND execution_status = 'SUCCESS'
      AND query_type = 'CREATE_USER'
      AND query_text ILIKE '%create%user%'
  ORDER BY end_time desc
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.query_history

Stage 2: filter

  • execution_status is SUCCESS
  • query_type is CREATE_USER
  • query_text matches the pattern *create*user* (case-insensitive)
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timestart_time
end_time
query_type
query_text
user_name
role_name

Query.Snowflake.UserEnabled

#
Source
github.com/panther-labs/panther-analysis

Monitor for users that are being re-enabled.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Snowflake.UserEnabled"
Enabled: false
Description: >
  Monitor for users that are being re-enabled.
SnowflakeQuery: |
  --return enable user events

  --this was adapted from a Security Feature Checklist query

  SELECT
    start_time as p_event_time,
    end_time,
    query_type,
    query_text,
    user_name,
    role_name
  FROM snowflake.account_usage.query_history
    WHERE
      p_occurs_since('1 day')
      AND execution_status = 'SUCCESS'
      AND query_type = 'ALTER_USER'
      AND (query_text ILIKE '%alter user%set disabled = false%'
          OR query_text ILIKE '%alter user%set disabled= false%'
          OR query_text ILIKE '%alter user%set disabled =false%'
          OR query_text ILIKE '%alter user%set disabled=false%')
  ORDER BY end_time desc

DatabricksQuery: |
  --return enable user events

  --this was adapted from a Security Feature Checklist query

  SELECT
    start_time as p_event_time,
    end_time,
    query_type,
    query_text,
    user_name,
    role_name
  FROM panther_logs.snowflake_queryhistory
    WHERE
      p_occurs_since('1 day')
      AND execution_status = 'SUCCESS'
      AND query_type = 'ALTER_USER'
      AND (query_text ILIKE '%alter user%set disabled = false%'
          OR query_text ILIKE '%alter user%set disabled= false%'
          OR query_text ILIKE '%alter user%set disabled =false%'
          OR query_text ILIKE '%alter user%set disabled=false%')
  ORDER BY end_time desc
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
snowflake.account_usage.query_history

Stage 2: filter

  • execution_status is SUCCESS
  • query_type is ALTER_USER
  • any of:
    • query_text matches the pattern *alter user*set disabled = false* (case-insensitive)
    • query_text matches the pattern *alter user*set disabled= false* (case-insensitive)
    • query_text matches the pattern *alter user*set disabled =false* (case-insensitive)
    • query_text matches the pattern *alter user*set disabled=false* (case-insensitive)
Window
1d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
execution_statuseq
  • SUCCESS
field:"execution_status" kind:eq value:"SUCCESS"
query_textwildcard
  • *alter user*set disabled = false* transforms: nocase
  • *alter user*set disabled =false* transforms: nocase
  • *alter user*set disabled= false* transforms: nocase
  • *alter user*set disabled=false* transforms: nocase
field:"query_text" kind:wildcard
query_typeeq
  • ALTER_USER
field:"query_type" kind:eq value:"ALTER_USER"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_timestart_time
end_time
query_type
query_text
user_name
role_name

Snowflake Account Admin Granted

#
Severity
medium
Tags
Snowflake, Privilege Escalation:Valid Accounts
Source
github.com/panther-labs/panther-analysis

Detect when account admin is granted.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

def rule(_):
    return True


def title(event):
    target = " ".join(event.get("query_text", "").split(" ")[-2:])
    return f"Snowflake AccountAdmin granted to {target}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_account_admin_assigned.py
RuleID: "Snowflake.AccountAdminGranted"
Description: >
  Detect when account admin is granted.
DisplayName: "Snowflake Account Admin Granted"
Enabled: false
Tags:
  - Snowflake
  - Privilege Escalation:Valid Accounts
Reports:
  MITRE ATT&CK:
    - TA0004:T1078
ScheduledQueries:
  - Query.Snowflake.AccountAdminGranted
Severity: Medium

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.AccountAdminGranted; its Python module (Detection logic above) shapes the alert rather than filtering.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "query_text": "grant role accountadmin to user testuser;"
}

Snowflake Account Admin Granted

#
Severity
medium
Log types
Snowflake.GrantsToUsers
Tags
Snowflake, [MITRE] Privilege Escalation, [MITRE] Valid Accounts
Source
github.com/panther-labs/panther-analysis

Detect when account admin is granted.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

def rule(event):
    if event.get("DELETED_ON"):
        return False
    return "admin" in event.get("GRANTEE_NAME", "").lower()


def title(event):
    source_name = event.get("p_source_label", "<UNKNOWN SNOWFLAKE SOURCE>")
    target = event.get("GRANTED_TO", "<UNKNOWN TARGET>")
    actor = event.get("GRANTED_BY", "<UNKNOWN ACTOR>")
    role = event.get("GRANTEE_NAME", "<UNKNOWN ROLE>")
    return f"{source_name}: {actor} granted role {role} to {target}"

Rule specification

AnalysisType: rule
Filename: snowflake_stream_account_admin_assigned.py
RuleID: "Snowflake.Stream.AccountAdminGranted"
DisplayName: Snowflake Account Admin Granted
Enabled: true
LogTypes:
  - Snowflake.GrantsToUsers
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0004:T1078
Description: Detect when account admin is granted.
Tags:
  - Snowflake
  - '[MITRE] Privilege Escalation'
  - '[MITRE] Valid Accounts'

Stages and Predicates

Fires on Snowflake.GrantsToUsers events when all of the conditions below hold.

Condition

  • DELETED_ON is empty
  • GRANTEE_NAME contains admin (case-insensitive)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DELETED_ONis_null
  • (no value, null check)
field:"DELETED_ON" kind:is_null
GRANTEE_NAMEcontains
  • admin transforms: tolower
field:"GRANTEE_NAME" kind:contains value:"admin"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_source_label
GRANTED_BY
GRANTEE_NAME
GRANTED_TO

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CREATED_ON": "2024-10-08 11:24:50.682000000",
  "GRANTED_BY": "SNOWFLAKE",
  "GRANTED_TO": "APPLICATION_ROLE",
  "GRANTEE_NAME": "TRUST_CENTER_ADMIN",
  "p_event_time": "2024-10-08 11:24:50.682000000",
  "p_log_type": "Snowflake.GrantsToUsers",
  "p_source_label": "Snowflake Prod"
}

Snowflake Brute Force Attacks by IP

#
Severity
medium
Tags
Snowflake, Credential Access:Brute Force
Source
github.com/panther-labs/panther-analysis

Detect brute force attacks by monitoring for failed logins from the same IP address

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def rule(_):
    return True


def title(event):
    return (
        f"Snowflake: {event.get('count_by_ip', 'many')} failed login attempts from IP "
        f"[{event.get('client_ip','<UNKNOWN_USER>')}]"
    )


def severity(event):
    # If the error appears to be caused by an automation issue, downgrade to INFO
    common_errors = {"JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH"}
    if event.get("ERROR_MESSAGE") in common_errors:
        return "INFO"
    return "DEFAULT"


def dedup(event):
    # Dedup on title and severity
    return f"[{severity(event)}] {title(event)}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_brute_force_ip.py
RuleID: "Snowflake.BruteForceByIp"
DisplayName: "Snowflake Brute Force Attacks by IP"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.FailedLogins
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0006:T1110
Description: >
  Detect brute force attacks by monitoring for failed logins from the same IP address
Threshold: 5
SummaryAttributes:
  - error_message
  - error_code
  - reported_client_type
  - user_name
Tags:
  - Snowflake
  - Credential Access:Brute Force

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.FailedLogins; its Python module (Detection logic above) shapes the alert rather than filtering.

Alert cadence
alerts after 5 matches within 1h

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
count_by_ip
client_ip

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "client_ip": "1.2.3.4",
  "count_by_ip": 100
}

Snowflake Brute Force Attacks by IP

#
Severity
medium
Group by
CLIENT_IP, REPORTED_CLIENT_TYPE
Log types
Snowflake.LoginHistory
Tags
Snowflake, [MITRE] Credential Access, [MITRE] Brute Force
Source
github.com/panther-labs/panther-analysis

Detect brute force attacks by monitorign failed logins from the same IP address

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def rule(event):
    # Return true for any login attempt; Let Panther's dedup and threshold handle the brute force
    #   detection.
    return (
        event.get("EVENT_TYPE") == "LOGIN"
        and event.get("IS_SUCCESS") == "NO"
        and event.get("ERROR_MESSAGE") != "OVERFLOW_FAILURE_EVENTS_ELIDED"
        # ^^ OVERFLOW_FAILURE_EVENTS_ELIDED are placeholder logs -> no point in alerting
    )


def title(event):
    return (
        "Login attempts from IP "
        f"{event.get('CLIENT_IP', '<UNKNOWN IP>')} "
        "have exceeded the failed logins threshold"
    )


def severity(event):
    # If the error appears to be caused by an automation issue, downgrade to INFO
    common_errors = {"JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH"}
    if event.get("ERROR_MESSAGE") in common_errors:
        return "INFO"
    return "DEFAULT"


def dedup(event):
    return event.get("CLIENT_IP", "<UNKNOWN IP>") + event.get(
        "REPORTED_CLIENT_TYPE", "<UNKNOWN CLIENT TYPE>"
    )

Rule specification

AnalysisType: rule
Filename: snowflake_stream_brute_force_by_ip.py
RuleID: "Snowflake.Stream.BruteForceByIp"
DisplayName: Snowflake Brute Force Attacks by IP
Enabled: true
LogTypes:
  - Snowflake.LoginHistory
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0006:T1110
Description: Detect brute force attacks by monitorign failed logins from the same
  IP address
DedupPeriodMinutes: 60
Threshold: 5
Tags:
  - Snowflake
  - '[MITRE] Credential Access'
  - '[MITRE] Brute Force'

Stages and Predicates

Fires on Snowflake.LoginHistory events when all of the conditions below hold.

Condition

  • EVENT_TYPE is LOGIN
  • IS_SUCCESS is NO
  • ERROR_MESSAGE is not OVERFLOW_FAILURE_EVENTS_ELIDED
Alert cadence
alerts after 5 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
CLIENT_IP

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CLIENT_IP": "1.2.3.4",
  "EVENT_ID": "393754014361778",
  "EVENT_TIMESTAMP": "2024-10-08 14:38:46.061000000",
  "EVENT_TYPE": "LOGIN",
  "FIRST_AUTHENTICATION_FACTOR": "PASSWORD",
  "IS_SUCCESS": "NO",
  "RELATED_EVENT_ID": "0",
  "REPORTED_CLIENT_TYPE": "OTHER",
  "REPORTED_CLIENT_VERSION": "1.11.1",
  "USER_NAME": "luthor@lexcorp.com",
  "p_event_time": "2024-10-08 14:38:46.061000000",
  "p_log_type": "Snowflake.LoginHistory",
  "p_source_label": "Snowflake Prod"
}

Snowflake Brute Force Attacks by User

#
Severity
medium
Group by
REPORTED_CLIENT_TYPE, USER_NAME
Log types
Snowflake.LoginHistory
Tags
Snowflake, [MITRE] Credential Access, [MITRE] Brute Force
Source
github.com/panther-labs/panther-analysis

Detect brute force attacks by monitorign failed logins from the same IP address

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def rule(event):
    # Return true for any login attempt; Let Panther's dedup and threshold handle the brute force
    #   detection.
    return (
        event.get("EVENT_TYPE") == "LOGIN"
        and event.get("IS_SUCCESS") == "NO"
        and event.get("ERROR_MESSAGE") != "OVERFLOW_FAILURE_EVENTS_ELIDED"
        # ^^ OVERFLOW_FAILURE_EVENTS_ELIDED are placeholder logs -> no point in alerting
    )


def title(event):
    return (
        f"User {event.get('USER_NAME', '<UNKNOWN USER>')} has exceeded the failed logins threshold"
    )


def severity(event):
    # If the error appears to be caused by an automation issue, downgrade to INFO
    common_errors = {"JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH"}
    if event.get("ERROR_MESSAGE") in common_errors:
        return "INFO"
    return "DEFAULT"


def dedup(event):
    return event.get("USER_NAME", "<UNKNOWN USER>") + event.get(
        "REPORTED_CLIENT_TYPE", "<UNKNOWN CLIENT TYPE>"
    )

Rule specification

AnalysisType: rule
Filename: snowflake_stream_brute_force_by_username.py
RuleID: "Snowflake.Stream.BruteForceByUsername"
DisplayName: Snowflake Brute Force Attacks by User
Enabled: true
LogTypes:
  - Snowflake.LoginHistory
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0006:T1110
Description: Detect brute force attacks by monitorign failed logins from the same
  IP address
DedupPeriodMinutes: 60
Threshold: 5
Tags:
  - Snowflake
  - '[MITRE] Credential Access'
  - '[MITRE] Brute Force'

Stages and Predicates

Fires on Snowflake.LoginHistory events when all of the conditions below hold.

Condition

  • EVENT_TYPE is LOGIN
  • IS_SUCCESS is NO
  • ERROR_MESSAGE is not OVERFLOW_FAILURE_EVENTS_ELIDED
Alert cadence
alerts after 5 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
USER_NAME

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CLIENT_IP": "1.2.3.4",
  "EVENT_ID": "393754014361778",
  "EVENT_TIMESTAMP": "2024-10-08 14:38:46.061000000",
  "EVENT_TYPE": "LOGIN",
  "FIRST_AUTHENTICATION_FACTOR": "PASSWORD",
  "IS_SUCCESS": "NO",
  "RELATED_EVENT_ID": "0",
  "REPORTED_CLIENT_TYPE": "OTHER",
  "REPORTED_CLIENT_VERSION": "1.11.1",
  "USER_NAME": "luthor@lexcorp.com",
  "p_event_time": "2024-10-08 14:38:46.061000000",
  "p_log_type": "Snowflake.LoginHistory",
  "p_source_label": "Snowflake Prod"
}

Snowflake Brute Force Attacks by Username

#
Severity
medium
Tags
Snowflake, Credential Access:Brute Force
Source
github.com/panther-labs/panther-analysis

Detect brute force attacks by monitoring for failed logins by the same username

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def rule(_):
    return True


def title(event):
    return (
        f"Snowflake: {event.get('counts_by_user', 'many')} failed login attempts by user "
        f"[{event.get('user_name','<UNKNOWN_USER>')}]"
    )


def severity(event):
    # If the error appears to be caused by an automation issue, downgrade to INFO
    common_errors = {"JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH"}
    if event.get("ERROR_MESSAGE") in common_errors:
        return "INFO"
    return "DEFAULT"


def dedup(event):
    # Dedup on title and severity
    return f"[{severity(event)}] {title(event)}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_brute_force_username.py
RuleID: "Snowflake.BruteForceByUsername"
Description: >
  Detect brute force attacks by monitoring for failed logins by the same username
DisplayName: "Snowflake Brute Force Attacks by Username"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.FailedLogins
Tags:
  - Snowflake
  - Credential Access:Brute Force
Reports:
  MITRE ATT&CK:
    - TA0006:T1110
Severity: Medium
Threshold: 5
SummaryAttributes:
  - client_ip
  - error_message
  - error_code
  - reported_client_type

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.FailedLogins; its Python module (Detection logic above) shapes the alert rather than filtering.

Alert cadence
alerts after 5 matches within 1h

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
counts_by_user
user_name

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "count_by_username": 100,
  "error_message": "JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH",
  "user_name": "testuser"
}

Snowflake Brute Force Login Success

#
Severity
high
Time window
30h
Match by
CLIENT_IP
Source
github.com/panther-labs/panther-analysis

Detecting brute force activity and reporting when a user has incorrectly logged in multiple times and then had a successful login.

Rule specification

AnalysisType: correlation_rule
RuleID: "Snowflake.PotentialBruteForceSuccess.Group"
DisplayName: "Snowflake Brute Force Login Success"
Enabled: false
Severity: High
Description: Detecting brute force activity and reporting when a user has incorrectly logged in multiple times and then had a successful login.
Detection:
  - Group:
      - ID: Multiple Failed Logins
        RuleID: Snowflake.Stream.BruteForceByIp
        MinMatchCount: 5
      - ID: Successful Login
        RuleID: Snowflake.Stream.LoginSuccess
    MatchCriteria:
      field_name:
        - GroupID: Multiple Failed Logins
          Match: CLIENT_IP
        - GroupID: Successful Login
          Match: CLIENT_IP
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 15
    LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by CLIENT_IP. Each step needs one match unless a higher minimum is shown.

Stage 1: step Multiple Failed Logins

References detection Snowflake Brute Force Attacks by IP (min 5 matches).

Stage 2: step Successful Login

References detection Snowflake Successful Login.

Snowflake Client IP

#
Severity
high
Source
github.com/panther-labs/panther-analysis

Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024

Detection logic

def rule(_):
    return True


def title(event):
    client_ip = event.get("client_ip", "<NO_IP_FOUND>")
    user_name = event.get("user_name", "<NO USERNAME FOUND>")
    return f"{user_name} accessed Snowflake from {client_ip}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_0108977_ip.py
RuleID: "Snowflake.Client.IP"
Description: >
  Monitor for malicious IPs interacting with Snowflake as part of ongoing cyber threat activity reported May 31st, 2024
DisplayName: "Snowflake Client IP"
Enabled: false
Runbook: Determine if this occurred as a result of a valid business request.
ScheduledQueries:
  - Query.Snowflake.ClientIp
Severity: High

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.ClientIp; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name
client_ip

Response runbook

Determine if this occurred as a result of a valid business request.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "client_ip": "1.2.3.4",
  "event_id": 342391,
  "event_timestamp": "2023-11-08 23:40:08.524Z",
  "event_type": "LOGIN",
  "first_authentication_factor": "PASSWORD",
  "is_success": "YES",
  "related_event_id": 0,
  "reported_client_type": "OTHER",
  "reported_client_version": "1.6.24",
  "user_name": "USER_NAME"
}

Snowflake Configuration Drift

#
Severity
medium
Tags
Configuration Required
Source
github.com/panther-labs/panther-analysis

Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024

Detection logic

def rule(_):
    return True


def title(event):
    user_name = event.get("user_name", "<NO USERNAME FOUND>")
    action = event.get("query_text", "<NO QUERY FOUND>").split(" ")[:2]
    target = " ".join(event.get("query_text", "").split(" ")[-2:])
    return f"{user_name} performed {action} on {target}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_0108977_configuration_drift.py
RuleID: "Snowflake.Configuration.Drift"
Description: >
  Monitor for configuration drift made by malicious actors as part of ongoing cyber threat activity reported May 31st, 2024
DisplayName: "Snowflake Configuration Drift"
Enabled: false
Runbook: Determine if this occurred as a result of a valid business request.
ScheduledQueries:
  - Query.Snowflake.ConfigurationDrift
Tags:
  - Configuration Required
Severity: Medium

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.ConfigurationDrift; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name

Response runbook

Determine if this occurred as a result of a valid business request.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "end_time": "2024-05-31 19:20:08.604Z",
  "query_text": "COPY INTO table.name FROM here as test file_format = (type = JSON);",
  "role_name": "ADMIN",
  "start_time": "2024-05-31 19:20:07.088Z",
  "user_name": "USER_NAME"
}

Snowflake Data Exfiltration

#
Severity
critical
Time window
30h
Match by
stage
Tags
Snowflake, Data Exfiltration, Database, Cloud Security
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects multi-step Snowflake data exfiltration by identifying temporary stage creation, table data copied to stage, and file downloads. This technique was used in the April 2024 Snowflake breach (UNC5537) targeting accounts without MFA. The correlation of all three steps provides high-confidence evidence of active data theft beyond legitimate ETL operations.

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "Snowflake.Data.Exfiltration.Group"
DisplayName: "Snowflake Data Exfiltration"
Enabled: false
Tags:
  - Snowflake
  - Data Exfiltration
  - Database
  - Cloud Security
Severity: Critical
Description: >
  Detects multi-step Snowflake data exfiltration by identifying temporary stage creation, table data copied to stage, and file downloads. This technique was used in the April 2024 Snowflake breach (UNC5537) targeting accounts without MFA. The correlation of all three steps provides high-confidence evidence of active data theft beyond legitimate ETL operations.
Runbook: |
  1. Query Snowflake's QUERY_HISTORY and ACCESS_HISTORY views for the stage name to identify the user account, session ID, source IP addresses, client application, and all tables that were copied into the stage
  2. Review the specific tables copied into the stage to determine data sensitivity (PII, financial records, intellectual property) and estimate the volume of data exfiltrated, then check Snowflake's LOGIN_HISTORY to verify if the user account had MFA enabled
  3. Analyze the source IP addresses used during the exfiltration sequence against threat intelligence feeds to determine if they are corporate IPs, suspicious cloud providers, or known malicious infrastructure, and check for impossible travel patterns
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Reports:
  MITRE ATT&CK:
    - TA0010:T1041 # Exfiltration: Exfiltration Over C2 Channel
    - TA0010:T1530 # Exfiltration: Data from Cloud Storage
    - TA0009:T1213 # Collection: Data from Information Repositories
Detection:
  - Group:
      - ID: SnowflakeTempStageCreated
        RuleID: Snowflake.TempStageCreated
      - ID: SnowflakeCopyIntoStage
        RuleID: Snowflake.CopyIntoStage
      - ID: SnowflakeFileDownloaded
        RuleID: Snowflake.FileDownloaded
    MatchCriteria:
      field_name:
        - GroupID: SnowflakeTempStageCreated
          Match: stage
        - GroupID: SnowflakeCopyIntoStage
          Match: stage
        - GroupID: SnowflakeFileDownloaded
          Match: stage
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 15
    LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by stage. Each step needs one match unless a higher minimum is shown.

Stage 1: step SnowflakeTempStageCreated

References detection Snowflake Temporary Stage Created.

Stage 2: step SnowflakeCopyIntoStage

References detection Snowflake Table Copied Into Stage.

Stage 3: step SnowflakeFileDownloaded

References detection Snowflake File Downloaded.

Response runbook

1. Query Snowflake's QUERY_HISTORY and ACCESS_HISTORY views for the stage name to identify the user account, session ID, source IP addresses, client application, and all tables that were copied into the stage

2. Review the specific tables copied into the stage to determine data sensitivity (PII, financial records, intellectual property) and estimate the volume of data exfiltrated, then check Snowflake's LOGIN_HISTORY to verify if the user account had MFA enabled

3. Analyze the source IP addresses used during the exfiltration sequence against threat intelligence feeds to determine if they are corporate IPs, suspicious cloud providers, or known malicious infrastructure, and check for impossible travel patterns

Snowflake Data Exfiltration

#
Severity
critical
Time window
30h
Match by
p_alert_context.stage
Tags
Snowflake, Data Exfiltration, Database, Cloud Security
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects multi-step Snowflake data exfiltration by identifying temporary stage creation, table data copied to stage, and file downloads. This technique was used in the April 2024 Snowflake breach (UNC5537) targeting accounts without MFA. The correlation of all three steps provides high-confidence evidence of active data theft beyond legitimate ETL operations.

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "Snowflake.Stream.DataExfiltration.Group"
DisplayName: "Snowflake Data Exfiltration"
Enabled: false
Tags:
  - Snowflake
  - Data Exfiltration
  - Database
  - Cloud Security
Severity: Critical
Description: >
  Detects multi-step Snowflake data exfiltration by identifying temporary stage creation, table data copied to stage, and file downloads. This technique was used in the April 2024 Snowflake breach (UNC5537) targeting accounts without MFA. The correlation of all three steps provides high-confidence evidence of active data theft beyond legitimate ETL operations.
Runbook: |
  1. Query Snowflake's QUERY_HISTORY and ACCESS_HISTORY views for the stage name in p_alert_context.stage to identify the user account, session ID, source IP addresses, client application, and all tables that were copied into the stage
  2. Review the specific tables copied into the stage to determine data sensitivity (PII, financial records, intellectual property) and estimate the volume of data exfiltrated, then check Snowflake's LOGIN_HISTORY to verify if the user account had MFA enabled
  3. Analyze the source IP addresses used during the exfiltration sequence against threat intelligence feeds to determine if they are corporate IPs, suspicious cloud providers, or known malicious infrastructure, and check for impossible travel patterns
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Reports:
  MITRE ATT&CK:
    - TA0010:T1041 # Exfiltration: Exfiltration Over C2 Channel
    - TA0010:T1530 # Exfiltration: Data from Cloud Storage
    - TA0009:T1213 # Collection: Data from Information Repositories
Detection:
  - Group:
      - ID: SnowflakeTempStageCreated
        RuleID: Snowflake.Stream.TempStageCreated
      - ID: SnowflakeCopyIntoStage
        RuleID: Snowflake.Stream.TableCopiedIntoStage
      - ID: SnowflakeFileDownloaded
        RuleID: Snowflake.Stream.FileDownloaded
    MatchCriteria:
      field_name:
        - GroupID: SnowflakeTempStageCreated
          Match: p_alert_context.stage
        - GroupID: SnowflakeCopyIntoStage
          Match: p_alert_context.stage
        - GroupID: SnowflakeFileDownloaded
          Match: p_alert_context.stage
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 15
    LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.stage. Each step needs one match unless a higher minimum is shown.

Stage 1: step SnowflakeTempStageCreated

References detection Snowflake Temporary Stage Created.

Stage 2: step SnowflakeCopyIntoStage

References detection Snowflake Table Copied Into Stage.

Stage 3: step SnowflakeFileDownloaded

References detection Snowflake File Downloaded.

Response runbook

1. Query Snowflake's QUERY_HISTORY and ACCESS_HISTORY views for the stage name in p_alert_context.stage to identify the user account, session ID, source IP addresses, client application, and all tables that were copied into the stage

2. Review the specific tables copied into the stage to determine data sensitivity (PII, financial records, intellectual property) and estimate the volume of data exfiltrated, then check Snowflake's LOGIN_HISTORY to verify if the user account had MFA enabled

3. Analyze the source IP addresses used during the exfiltration sequence against threat intelligence feeds to determine if they are corporate IPs, suspicious cloud providers, or known malicious infrastructure, and check for impossible travel patterns

Snowflake External Data Share

#
Severity
medium
Log types
Snowflake.DataTransferHistory
Tags
Snowflake, [MITRE] Exfiltration, [MITRE] Transfer Data to Cloud Account
Source
github.com/panther-labs/panther-analysis

Detect when an external share has been initiated from one source cloud to another target cloud.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def rule(event):
    return all(
        [
            event.get("SOURCE_CLOUD"),
            event.get("TARGET_CLOUD"),
            event.get("BYTES_TRANSFERRED", 0) > 0,
        ]
    )


def title(event):
    return (
        f"A data export has been initiated from source cloud "
        f"{event.get('SOURCE_CLOUD', '<UNKNOWN SOURCE CLOUD>')} "
        f"in source region {event.get('SOURCE_REGION', '<UNKNOWN SOURCE REGION>')} "
        f"to target cloud {event.get('TARGET_CLOUD', '<UNKNOWN TARGET CLOUD>')} "
        f"in target region {event.get('TARGET_REGION', '<UNKNOWN TARGET REGION>')} "
        f"with transfer type {event.get('TRANSFER_TYPE', '<UNKNOWN TRANSFER TYPE>')} "
        f"for {event.get('BYTES_TRANSFERRED', '<UNKNOWN VOLUME>')} bytes"
    )

Rule specification

AnalysisType: rule
Filename: snowflake_stream_external_shares.py
RuleID: Snowflake.Stream.ExternalShares
DisplayName: Snowflake External Data Share
Enabled: true
LogTypes:
  - Snowflake.DataTransferHistory
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Description: Detect when an external share has been initiated from one source cloud
  to another target cloud.
Runbook: Determine if this occurred as a result of a valid business request.
Tags:
  - Snowflake
  - '[MITRE] Exfiltration'
  - '[MITRE] Transfer Data to Cloud Account'

Stages and Predicates

Fires on Snowflake.DataTransferHistory events when all of the conditions below hold.

Condition

  • SOURCE_CLOUD is present
  • TARGET_CLOUD is present
  • BYTES_TRANSFERRED is greater than 0

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
BYTES_TRANSFERREDgt
  • 0 transforms: number
field:"BYTES_TRANSFERRED" kind:gt value:"0"
SOURCE_CLOUDis_not_null
  • (no value, null check)
field:"SOURCE_CLOUD" kind:is_not_null
TARGET_CLOUDis_not_null
  • (no value, null check)
field:"TARGET_CLOUD" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
SOURCE_CLOUD
SOURCE_REGION
TARGET_CLOUD
TARGET_REGION
TRANSFER_TYPE
BYTES_TRANSFERRED

Response runbook

Determine if this occurred as a result of a valid business request.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "BYTES_TRANSFERRED": 61235879,
  "REGION": "US-EAST-2",
  "SOURCE_CLOUD": "AWS",
  "SOURCE_REGION": "US-EAST-2",
  "TARGET_CLOUD": "AWS",
  "TARGET_REGION": "EU-WEST-1",
  "TRANSFER_TYPE": "COPY"
}

Snowflake External Share

#
Severity
medium
Source
github.com/panther-labs/panther-analysis

Detect when an external share has been initiated from one source cloud to another target cloud.

Detection logic

def rule(_):
    return True


def title(event):
    return (
        "A data export has been initiated from source cloud "
        f"[{event.get('source_cloud','<SOURCE_CLOUD_NOT_FOUND>')}] "
        f"in source region [{event.get('source_region','<SOURCE_REGION_NOT_FOUND>')}] "
        f"to target cloud [{event.get('target_cloud','<TARGET_CLOUD_NOT_FOUND>')}] "
        f"in target region [{event.get('target_region','<TARGET_REGION_NOT_FOUND>')}] "
        f"with transfer type [{event.get('transfer_type','<TRANSFER_TYPE_NOT_FOUND>')}] "
        f"for [{event.get('bytes_transferred','<BYTES_NOT_FOUND>')}] bytes."
    )

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_external_shares.py
RuleID: "Snowflake.External.Shares"
Description: >
  Detect when an external share has been initiated from one source cloud to another target cloud.
DisplayName: "Snowflake External Share"
Enabled: false
Runbook: Determine if this occurred as a result of a valid business request.
ScheduledQueries:
  - Query.Snowflake.External.Shares
Severity: Medium

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.External.Shares; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
source_cloud
source_region
target_cloud
target_region
transfer_type
bytes_transferred

Response runbook

Determine if this occurred as a result of a valid business request.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "bytes_transferred": 555,
  "end_time": "2023-04-12 18:13:35Z",
  "source_cloud": "Amazon Web Services (AWS)",
  "source_region": "us-west-2",
  "start_time": "2023-04-12 18:12:49Z",
  "target_cloud": "Microsoft Azure",
  "target_region": "East US 2",
  "transfer_type": "COPY"
}

Snowflake File Downloaded

#
Severity
informational
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A file was downloaded from a stage

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def rule(_):
    return True

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_file_downloaded_signal.py
RuleID: "Snowflake.FileDownloaded"
Description: >
  A file was downloaded from a stage
DisplayName: "Snowflake File Downloaded"
Enabled: true
CreateAlert: false
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Reports:
    MITRE ATT&CK:
        - TA0010:T1041  # Exfiltration Over C2 Channel
ScheduledQueries:
  - Query.Snowflake.FileDownloaded
Severity: Info

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.FileDownloaded; its Python module (Detection logic above) shapes the alert rather than filtering.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "execution_status": "SUCCESS",
  "path": "LOGS.PUBLIC.data_exfil/DATA.csv",
  "query_text": "GET '@LOGS.PUBLIC.data_exfil/DATA.csv' 'file:///Users/evil.genius/Documents'",
  "query_type": "GET_FILES",
  "role_name": "SYSADMIN",
  "stage": "LOGS.PUBLIC.data_exfil",
  "start_time": "2024-06-10 19:37:15.698Z",
  "user_name": "ADMIN"
}

Snowflake File Downloaded

#
Severity
informational
Log types
Snowflake.QueryHistory
Tags
Snowflake, [MITRE] Exfiltration, [MITRE] Exfiltration Over C2 Channel
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A file was downloaded from a stage.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

import re

from panther_snowflake_helpers import query_history_alert_context

PATH_EXPR = re.compile(r"GET\s+(?:\$\$|')?@([a-zA-Z0-9_\./]+)(?:\$\$|')?\s", flags=re.I)
STAGE_EXPR = re.compile(r"GET\s+(?:\$\$|')?@([a-zA-Z0-9_\.]+)", flags=re.I)

PATH = ""
STAGE = ""


def rule(event):
    # pylint: disable=global-statement
    # Check these conditions first to avoid running an expensive regex on every log
    if not all(
        (
            event.get("QUERY_TYPE") == "GET_FILES",
            event.get("EXECUTION_STATUS") == "SUCCESS",
            # Avoid alerting for fetching worksheets:
            event.get("QUERY_TEXT") != "GET '@~/worksheet_data/metadata' 'file:///'",
        )
    ):
        return False

    global PATH
    PATH = PATH_EXPR.search(event.get("QUERY_TEXT", ""))

    return PATH is not None


def alert_context(event):
    # pylint: disable=global-statement
    global PATH
    global STAGE
    STAGE = STAGE_EXPR.match(event.get("QUERY_TEXT", ""))
    return query_history_alert_context(event) | {
        "path": PATH.group(1),
        "stage": None if not STAGE else STAGE.group(1).lower(),
    }

Rule specification

AnalysisType: rule
Filename: snowflake_stream_file_downloaded.py
RuleID: Snowflake.Stream.FileDownloaded
DisplayName: Snowflake File Downloaded
Enabled: true
LogTypes:
  - Snowflake.QueryHistory
Severity: Info
CreateAlert: false
Reports:
  MITRE ATT&CK:
    - TA0010:T1041 # Exfiltration Over C2 Channel
Description: A file was downloaded from a stage.
Reference: 
  https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Tags:
  - Snowflake
  - '[MITRE] Exfiltration'
  - '[MITRE] Exfiltration Over C2 Channel'

Stages and Predicates

Fires on Snowflake.QueryHistory events when all of the conditions below hold.

Condition

  • QUERY_TYPE is GET_FILES
  • EXECUTION_STATUS is SUCCESS
  • QUERY_TEXT is not GET '@~/worksheet_data/metadata' 'file:///'

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "EXECUTION_STATUS": "SUCCESS",
  "QUERY_TEXT": "GET @PANTHER_LOGS.PUBLIC.data_exfil/DATA.csv 'file:///Users/lex.luthor/Documents'",
  "QUERY_TYPE": "GET_FILES",
  "ROLE_NAME": "PUBLIC",
  "USER_NAME": "LEX_LUTHOR",
  "p_event_time": "2024-10-09 19:38:06.158000000",
  "p_log_type": "Snowflake.QueryHistory",
  "p_source_label": "SF-Ben"
}

Snowflake Grant to Public Role

#
Severity
medium
Log types
Snowflake.GrantsToRoles
Tags
Snowflake, [MITRE] Privilege Escalation, [MITRE] Valid Accounts, [MITRE] Valid Accounts: Default Accounts
Source
github.com/panther-labs/panther-analysis

Detect additional grants to the public role.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

def rule(event):
    return event.get("GRANTEE_NAME").lower() == "public"


def title(event):
    return (
        f"{event.get('p_source_label', '<UNKNOWN LOG SOURCE>')}: "
        f"{event.get('GRANTED_BY', '<UNKNOWN ACTOR>')} made changes to the PUBLIC role"
    )

Rule specification

AnalysisType: rule
Filename: snowflake_stream_public_role_grant.py
RuleID: Snowflake.Stream.PublicRoleGrant
DisplayName: Snowflake Grant to Public Role
Enabled: true
LogTypes:
  - Snowflake.GrantsToRoles
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0004:T1078.001
Description: Detect additional grants to the public role.
Runbook: Determine if this is a necessary grant for the public role, which should
  be kept to the fewest possible.
Tags:
  - Snowflake
  - '[MITRE] Privilege Escalation'
  - '[MITRE] Valid Accounts'
  - '[MITRE] Valid Accounts: Default Accounts'

Stages and Predicates

Fires on Snowflake.GrantsToRoles events when the condition below holds.

Condition

  • GRANTEE_NAME is public (case-insensitive)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
GRANTEE_NAMEeq
  • public transforms: tolower
field:"GRANTEE_NAME" kind:eq value:"public"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_source_label
GRANTED_BY

Response runbook

Determine if this is a necessary grant for the public role, which should be kept to the fewest possible.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CREATED_ON": "2024-10-10 12:56:35.822 -0700",
  "DELETED_ON": "",
  "GRANTED_BY": "ACCOUNTADMIN",
  "GRANTED_BY_ROLE_TYPE": "ROLE",
  "GRANTED_ON": "TABLE",
  "GRANTED_TO": "ROLE",
  "GRANTEE_NAME": "PUBLIC",
  "GRANT_OPTION": false,
  "MODIFIED_ON": "2024-10-10 12:56:35.822 -0700",
  "NAME": "MYTABLE",
  "OBJECT_INSTANCE": "",
  "PRIVILEGE": "SELECT",
  "TABLE_CATALOG": "TEST_DB",
  "TABLE_SCHEMA": "PUBLIC",
  "p_source_label": "DailyPlanet-Snowflake"
}

Snowflake Login Without MFA

#
Severity
medium
Tags
Snowflake, Defense Evasion:Modify Authentication Process
Source
github.com/panther-labs/panther-analysis

Detect snowflake logins without multifactor authentication

MITRE ATT&CK coverage

Detection logic

MFA_EXCEPTIONS = {"PANTHER_READONLY", "PANTHER_ADMIN", "PANTHERACCOUNTADMIN"}


def rule(event):
    return event.get("user_name", "") not in MFA_EXCEPTIONS

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_login_without_mfa.py
RuleID: "Snowflake.LoginWithoutMFA"
Description: >
  Detect snowflake logins without multifactor authentication
DisplayName: "Snowflake Login Without MFA"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.MFALogin
Tags:
  - Snowflake
  - Defense Evasion:Modify Authentication Process
Reports:
  MITRE ATT&CK:
    - TA0005:T1556
Severity: Medium

Stages and Predicates

Fires when the condition below holds.

Condition

  • user_name is not one of PANTHER_READONLY, PANTHER_ADMIN, PANTHERACCOUNTADMIN

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
user_nameinPANTHERACCOUNTADMIN, PANTHER_ADMIN, PANTHER_READONLYexcludes:user_name field:"user_name" value:"PANTHERACCOUNTADMIN" field:"user_name" value:"PANTHER_ADMIN" field:"user_name" value:"PANTHER_READONLY"

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "Anything": "any value"
}

Snowflake Login Without MFA

#
Severity
medium
Log types
Snowflake.LoginHistory
Tags
Snowflake, [MITRE] Defense Evasion, [MITRE] Modify Authentication Process
Source
github.com/panther-labs/panther-analysis

Detect Snowflake logins without multifactor authentication

MITRE ATT&CK coverage

Detection logic

MFA_EXCEPTIONS = {"PANTHER_READONLY", "PANTHER_ADMIN", "PANTHERACCOUNTADMIN"}


def rule(event):
    return all(
        (
            event.get("EVENT_TYPE") == "LOGIN",
            event.get("IS_SUCCESS") == "YES",
            event.get("FIRST_AUTHENTICATION_FACTOR") == "PASSWORD",
            not event.get("SECOND_AUTHENTICATION_FACTOR"),
            event.get("USER_NAME") not in MFA_EXCEPTIONS,
        )
    )


def title(event):
    source = event.get("p_source_label", "<UNKNOWN SOURCE>")
    user = event.get("USER_NAME", "<UNKNOWN USER>")
    return f"{source}: User {user} logged in without MFA"

Rule specification

AnalysisType: rule
Filename: snowflake_stream_login_without_mfa.py
RuleID: Snowflake.Stream.LoginWithoutMFA
DisplayName: Snowflake Login Without MFA
Enabled: false
LogTypes:
  - Snowflake.LoginHistory
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0005:T1556
Description: Detect Snowflake logins without multifactor authentication
DedupPeriodMinutes: 1440
Tags:
  - Snowflake
  - '[MITRE] Defense Evasion'
  - '[MITRE] Modify Authentication Process'

Stages and Predicates

Fires on Snowflake.LoginHistory events when all of the conditions below hold.

Condition

  • EVENT_TYPE is LOGIN
  • IS_SUCCESS is YES
  • FIRST_AUTHENTICATION_FACTOR is PASSWORD
  • SECOND_AUTHENTICATION_FACTOR is empty
  • USER_NAME is not one of PANTHER_READONLY, PANTHER_ADMIN, PANTHERACCOUNTADMIN
Alert deduplication
repeat matches within 1d group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
USER_NAMEinPANTHERACCOUNTADMIN, PANTHER_ADMIN, PANTHER_READONLYexcludes:USER_NAME field:"USER_NAME" value:"PANTHERACCOUNTADMIN" field:"USER_NAME" value:"PANTHER_ADMIN" field:"USER_NAME" value:"PANTHER_READONLY"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_source_label
USER_NAME

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CLIENT_IP": "1.2.3.4",
  "EVENT_ID": "393754014361778",
  "EVENT_TIMESTAMP": "2024-10-08 14:38:46.061000000",
  "EVENT_TYPE": "LOGIN",
  "FIRST_AUTHENTICATION_FACTOR": "PASSWORD",
  "IS_SUCCESS": "YES",
  "RELATED_EVENT_ID": "0",
  "REPORTED_CLIENT_TYPE": "OTHER",
  "REPORTED_CLIENT_VERSION": "1.11.1",
  "USER_NAME": "luthor@lexcorp.com",
  "p_event_time": "2024-10-08 14:38:46.061000000",
  "p_log_type": "Snowflake.LoginHistory",
  "p_source_label": "Snowflake Prod"
}

Snowflake Multiple Failed Logins Followed By Success

#
Severity
medium
Source
github.com/panther-labs/panther-analysis

Detecting brute force activity and reporting when a user has incorrectly logged in multiple times and then had a successful login.

Detection logic

def rule(_):
    return True


def title(event):
    # pylint: disable=line-too-long
    return (
        f"Username [{event.get('user_name','<USER_NOT_FOUND>')}] from clientIP "
        f"[{event.get('client_ip','<CLIENT_IP_NOT_FOUND>')}] "
        f"registered [{event.get('num_fails','<NUM_FAILS_NOT_FOUND>')}] failed logins "
        f" which began at [{event.get('start_of_unsuccessful_logins_time','<UNSUCCESSFUL_LOGINS_START_TIME_NOT_FOUND>')}] "
        f"followed by a successful login which occurred at "
        f"[{event.get('successful_login_time','<SUCCESS_LOGIN_TIME_NOT_FOUND>')}]."
    )

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_multiple_failed_logins_followed_by_success.py
RuleID: "Snowflake.Multiple.Failed.Logins.Followed.By.Success"
Description: >
  Detecting brute force activity and reporting when a user has incorrectly logged in multiple times and then had a successful login.
DisplayName: "Snowflake Multiple Failed Logins Followed By Success"
Enabled: false
Runbook: Determine if this was a simple mistake or if this is an active brute force attempt.
ScheduledQueries:
  - Query.Snowflake.Multiple.Logins.Followed.By.Success
Severity: Medium

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.Multiple.Logins.Followed.By.Success; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name
client_ip
num_fails
start_of_unsuccessful_logins_time
successful_login_time

Response runbook

Determine if this was a simple mistake or if this is an active brute force attempt.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "client_ip": "12.12.12.12",
  "error_codes": [
    390100
  ],
  "error_messages": [
    "INCORRECT_USERNAME_PASSWORD"
  ],
  "match_number": 1,
  "num_fails": 6,
  "num_successes": 1,
  "rows_in_sequence": 7,
  "start_of_unsuccessful_logins_time": "2023-04-12 18:12:49Z",
  "successful_login_time": "2023-04-12 18:13:35Z",
  "user_name": "USERNAME"
}

Snowflake Password Spray

#
Status
Experimental
Severity
medium
Group by
CLIENT_IP
Log types
Snowflake.LoginHistory
Tags
Snowflake, Credential Access, Brute Force
Reference
docs.snowflake.com
Source
github.com/panther-labs/panther-analysis

Detects password spraying attacks against Snowflake by tracking the number of distinct user accounts targeted by failed login attempts from the same source IP address within a short timeframe. Unlike brute force against a single account, password spraying distributes attempts across many accounts to evade lockout policies. This rule complements Snowflake.Stream.BruteForceByIp (same IP, any accounts) and Snowflake.PotentialBruteForceSuccess.Group (brute force followed by confirmed login).

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def rule(event):
    return (
        event.get("EVENT_TYPE") == "LOGIN"
        and event.get("IS_SUCCESS") == "NO"
        and event.get("ERROR_MESSAGE") != "OVERFLOW_FAILURE_EVENTS_ELIDED"
    )


def unique(event):
    return event.get("USER_NAME", "UNKNOWN_USER")


def dedup(event):
    return event.get("CLIENT_IP", "UNKNOWN_IP")


def title(event):
    client_ip = event.get("CLIENT_IP", "UNKNOWN_IP")
    return f"[Snowflake] Password spray detected from IP [{client_ip}] targeting multiple accounts"


def severity(event):
    # Downgrade JWT key mismatches to INFO as these are typically automation misconfiguration
    if event.get("ERROR_MESSAGE") == "JWT_TOKEN_INVALID_PUBLIC_KEY_FINGERPRINT_MISMATCH":
        return "INFO"
    return "DEFAULT"


def alert_context(event):
    return {
        "client_ip": event.get("CLIENT_IP", "UNKNOWN_IP"),
        "user_name": event.get("USER_NAME", "UNKNOWN_USER"),
        "client_type": event.get("REPORTED_CLIENT_TYPE"),
        "error_code": event.get("ERROR_CODE"),
        "error_message": event.get("ERROR_MESSAGE"),
    }

Rule specification

AnalysisType: rule
RuleID: "Snowflake.Stream.PasswordSpray"
DisplayName: "Snowflake Password Spray"
Filename: snowflake_stream_password_spray.py
Enabled: true
Status: Experimental
LogTypes:
  - Snowflake.LoginHistory
Severity: Medium
Threshold: 5
DedupPeriodMinutes: 60
Description: >
  Detects password spraying attacks against Snowflake by tracking the number of distinct
  user accounts targeted by failed login attempts from the same source IP address within
  a short timeframe. Unlike brute force against a single account, password spraying
  distributes attempts across many accounts to evade lockout policies. This rule
  complements Snowflake.Stream.BruteForceByIp (same IP, any accounts) and
  Snowflake.PotentialBruteForceSuccess.Group (brute force followed by confirmed login).
Runbook: |
  1. Review the distinct usernames targeted from the source IP and determine if any are privileged accounts (ACCOUNTADMIN, SYSADMIN, SECURITYADMIN) which would elevate the risk significantly
  2. Query Snowflake LoginHistory for all events with IS_SUCCESS = 'YES' from the same CLIENT_IP within the past 60 minutes to determine if any spray attempt succeeded — the alert context shows only the last triggering username, not all targeted accounts
  3. Check if the source IP belongs to known anonymization infrastructure (VPNs, Tor exit nodes, cloud provider ranges) and cross-reference against other Snowflake tenants if applicable
Reference: https://docs.snowflake.com/en/sql-reference/account-usage/login_history
Tags:
  - Snowflake
  - Credential Access
  - Brute Force
Reports:
  MITRE ATT&CK:
    - TA0006:T1110.003 # Credential Access: Password Spraying

Stages and Predicates

Fires on Snowflake.LoginHistory events when all of the conditions below hold.

Condition

  • EVENT_TYPE is LOGIN
  • IS_SUCCESS is NO
  • ERROR_MESSAGE is not OVERFLOW_FAILURE_EVENTS_ELIDED
Alert cadence
alerts after 5 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
client_ipCLIENT_IP
user_nameUSER_NAME
client_typeREPORTED_CLIENT_TYPE
error_codeERROR_CODE
error_messageERROR_MESSAGE

Response runbook

1. Review the distinct usernames targeted from the source IP and determine if any are privileged accounts (ACCOUNTADMIN, SYSADMIN, SECURITYADMIN) which would elevate the risk significantly

2. Query Snowflake LoginHistory for all events with IS_SUCCESS = 'YES' from the same CLIENT_IP within the past 60 minutes to determine if any spray attempt succeeded — the alert context shows only the last triggering username, not all targeted accounts

3. Check if the source IP belongs to known anonymization infrastructure (VPNs, Tor exit nodes, cloud provider ranges) and cross-reference against other Snowflake tenants if applicable

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CLIENT_IP": "1.2.3.4",
  "EVENT_ID": "393754014361778",
  "EVENT_TIMESTAMP": "2024-10-08 14:38:46.061000000",
  "EVENT_TYPE": "LOGIN",
  "FIRST_AUTHENTICATION_FACTOR": "PASSWORD",
  "IS_SUCCESS": "NO",
  "RELATED_EVENT_ID": "0",
  "REPORTED_CLIENT_TYPE": "OTHER",
  "REPORTED_CLIENT_VERSION": "1.11.1",
  "USER_NAME": "ckent@dailyplanet.org",
  "p_event_time": "2024-10-08 14:38:46.061000000",
  "p_log_type": "Snowflake.LoginHistory"
}

Snowflake Successful Login

#
Severity
informational
Log types
Snowflake.LoginHistory
Tags
Snowflake
Source
github.com/panther-labs/panther-analysis

Track successful login signals for correlation.

Detection logic

def rule(event):
    return all((event.get("EVENT_TYPE") == "LOGIN", event.get("IS_SUCCESS") == "YES"))

Rule specification

AnalysisType: rule
Filename: snowflake_stream_login_success.py
RuleID: Snowflake.Stream.LoginSuccess
DisplayName: Snowflake Successful Login
Enabled: true
LogTypes:
  - Snowflake.LoginHistory
Severity: Info
CreateAlert: false
Description: Track successful login signals for correlation.
Tags:
  - Snowflake

Stages and Predicates

Fires on Snowflake.LoginHistory events when all of the conditions below hold.

Condition

  • EVENT_TYPE is LOGIN
  • IS_SUCCESS is YES

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "CLIENT_IP": "1.1.1.1",
  "EVENT_ID": "393754014361778",
  "EVENT_TIMESTAMP": "2024-10-08 14:38:46.061000000",
  "EVENT_TYPE": "LOGIN",
  "FIRST_AUTHENTICATION_FACTOR": "PASSWORD",
  "IS_SUCCESS": "YES",
  "RELATED_EVENT_ID": "0",
  "REPORTED_CLIENT_TYPE": "OTHER",
  "REPORTED_CLIENT_VERSION": "1.11.1",
  "USER_NAME": "ckent@dailyplanet.org",
  "p_event_time": "2024-10-08 14:38:46.061000000",
  "p_log_type": "Snowflake.LoginHistory",
  "p_source_label": "Snowflake Prod"
}

Snowflake Table Copied Into Stage

#
Severity
informational
Log types
Snowflake.QueryHistory
Tags
Snowflake, [MITRE] Exfiltration, [MITRE] Exfiltration Over C2 Channel
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A table was copied into a stage.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

import re

STAGE_EXPR = re.compile(r"COPY\s+INTO\s+(?:\$\$|')?@([\w\.]+)", flags=re.I)
PATH_EXPR = re.compile(r"COPY\s+INTO\s+(?:\$\$|')?@([\w\./]+)(?:\$\$|')?\s+FROM", flags=re.I)

STAGE = ""


def rule(event):
    # pylint: disable=global-statement
    global STAGE
    STAGE = STAGE_EXPR.match(event.get("QUERY_TEXT", ""))
    return all(
        (
            event.get("QUERY_TYPE") == "UNLOAD",
            STAGE is not None,
            event.get("EXECUTION_STATUS") == "SUCCESS",
        )
    )


def alert_context(event):
    # pylint: disable=global-statement
    global STAGE
    path = PATH_EXPR.match(event.get("QUERY_TEXT", ""))
    return {"actor": event.get("USER_NAME"), "path": path.group(1), "stage": STAGE.group(1).lower()}

Rule specification

AnalysisType: rule
Filename: snowflake_stream_table_copied_into_stage.py
RuleID: Snowflake.Stream.TableCopiedIntoStage
DisplayName: Snowflake Table Copied Into Stage
Enabled: true
LogTypes:
  - Snowflake.QueryHistory
Severity: Info
CreateAlert: false
Reports:
  MITRE ATT&CK:
    - TA0010:T1041      # Exfiltration Over C2 Channel
Description: A table was copied into a stage.
Reference: 
  https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Tags:
  - Snowflake
  - '[MITRE] Exfiltration'
  - '[MITRE] Exfiltration Over C2 Channel'

Stages and Predicates

Fires on Snowflake.QueryHistory events when all of the conditions below hold.

Condition

  • QUERY_TYPE is UNLOAD
  • EXECUTION_STATUS is SUCCESS

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actorUSER_NAME

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "EXECUTION_STATUS": "SUCCESS",
  "QUERY_TEXT": "COPY INTO '@PANTHER_LOGS.PUBLIC.data_exfil/DATA.csv' FROM (SELECT * FROM PANTHER_LOGS.PUBLIC.amazon_eks_audit LIMIT 100) FILE_FORMAT = ( TYPE='CSV' COMPRESSION=GZIP FIELD_DELIMITER=',' ESCAPE=NONE ESCAPE_UNENCLOSED_FIELD=NONE date_format='AUTO' time_format='AUTO' timestamp_format='AUTO' binary_format='UTF-8' null_if='' EMPTY_FIELD_AS_NULL = FALSE ) overwrite=TRUE single=FALSE max_file_size=5368709120 header=TRUE",
  "QUERY_TYPE": "UNLOAD",
  "USER_NAME": "LEX_LUTHOR"
}

Snowflake Table Copied Into Stage

#
Severity
informational
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A table was copied into a stage

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def rule(_):
    return True

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_table_copied_into_stage_signal.py
RuleID: "Snowflake.CopyIntoStage"
Description: >
  A table was copied into a stage
DisplayName: "Snowflake Table Copied Into Stage"
Enabled: true
CreateAlert: false
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Reports:
    MITRE ATT&CK:
        - TA0010:T1041  # Exfiltration Over C2 Channel
ScheduledQueries:
  - Query.Snowflake.CopyIntoStage
Severity: Info

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.CopyIntoStage; its Python module (Detection logic above) shapes the alert rather than filtering.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "execution_status": "SUCCESS",
  "path": "LOGS.PUBLIC.data_exfil/DATA.csv",
  "query_text": "COPY INTO @LOGS.PUBLIC.data_exfil/DATA.csv\nFROM (SELECT * FROM PANTHER_LOGS.PUBLIC.GITLAB_API_VARIANT LIMIT 100)\nFILE_FORMAT = ( \n TYPE='CSV' \n COMPRESSION=GZIP\n FIELD_DELIMITER=',' \n ESCAPE=NONE \n ESCAPE_UNENCLOSED_FIELD=NONE \n date_format='AUTO' \n time_format='AUTO' \n timestamp_format='AUTO'\n binary_format='UTF-8' \n field_optionally_enclosed_by='\"' \n null_if='' \n EMPTY_FIELD_AS_NULL = FALSE \n)  \noverwrite=TRUE \nsingle=FALSE \nmax_file_size=5368709120 \nheader=TRUE",
  "query_type": "UNLOAD",
  "role_name": "SYSADMIN",
  "stage": "LOGS.PUBLIC.data_exfil",
  "start_time": "2024-06-10 19:15:37.445Z",
  "user_name": "ADMIN"
}

Snowflake Temporary Stage Created

#
Severity
informational
Log types
Snowflake.QueryHistory
Tags
Snowflake, [MITRE] Exfiltration, [MITRE] Exfiltration Over C2 Channel
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A temporary stage was created.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

import re

from panther_snowflake_helpers import query_history_alert_context

STAGE_EXPR = re.compile(
    (
        r"CREATE\s+(?:OR\s+REPLACE\s+)?(?:TEMPORARY\s+|TEMP\s+)STAGE\s+"
        r"(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z0-9_\.]+)"
    ),
    flags=re.I,
)

STAGE = ""


def rule(event):
    # pylint: disable=global-statement
    global STAGE
    STAGE = STAGE_EXPR.match(event.get("QUERY_TEXT", ""))

    return all(
        (
            event.get("QUERY_TYPE") == "CREATE",
            event.get("EXECUTION_STATUS") == "SUCCESS",
            STAGE is not None,
        )
    )


def alert_context(event):
    # pylint: disable=global-statement
    global STAGE
    return query_history_alert_context(event) | {"stage": STAGE.group(1).lower()}

Rule specification

AnalysisType: rule
Filename: snowflake_stream_temp_stage_created.py
RuleID: Snowflake.Stream.TempStageCreated
DisplayName: Snowflake Temporary Stage Created
Enabled: true
LogTypes:
  - Snowflake.QueryHistory
Severity: Info
CreateAlert: false
Reports:
  MITRE ATT&CK:
    - TA0010:T1041      # Exfiltration Over C2 Channel
Description: A temporary stage was created.
Reference: 
  https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Tags:
  - Snowflake
  - '[MITRE] Exfiltration'
  - '[MITRE] Exfiltration Over C2 Channel'

Stages and Predicates

Fires on Snowflake.QueryHistory events when all of the conditions below hold.

Condition

  • QUERY_TYPE is CREATE
  • EXECUTION_STATUS is SUCCESS

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "EXECUTION_STATUS": "SUCCESS",
  "QUERY_TEXT": "CREATE OR REPLACE TEMP STAGE panther_logs.PUBLIC.data_exfil;",
  "QUERY_TYPE": "CREATE",
  "USER_NAME": "LEX_LUTHOR",
  "WAREHOUSE_NAME": "ADMIN_WH",
  "p_event_time": "2024-10-09 21:06:03.631000000",
  "p_log_type": "Snowflake.QueryHistory",
  "p_source_id": "132d65cd-d6e4-4981-a209-a1d5902afd59",
  "p_source_label": "SF-Ben"
}

Snowflake Temporary Stage Created

#
Severity
informational
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A temporary stage was created

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def rule(_):
    return True

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_temp_stage_created_signal.py
RuleID: "Snowflake.TempStageCreated"
Description: >
  A temporary stage was created
DisplayName: "Snowflake Temporary Stage Created"
Enabled: true
CreateAlert: false
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
Reports:
    MITRE ATT&CK:
        - TA0010:T1041  # Exfiltration Over C2 Channel
ScheduledQueries:
  - Query.Snowflake.TempStageCreated
Severity: Info

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.TempStageCreated; its Python module (Detection logic above) shapes the alert rather than filtering.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "execution_status": "SUCCESS",
  "query_text": "CREATE OR REPLACE TEMP STAGE logs.PUBLIC.data_exfil",
  "query_type": "CREATE",
  "role_name": "SYSADMIN",
  "stage": "logs.PUBLIC.data_exfil",
  "start_time": "2024-06-10 19:48:52.068Z",
  "user_name": "ADMIN"
}

Snowflake User Access

#
Severity
high
Source
github.com/panther-labs/panther-analysis

Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024

Detection logic

def rule(_):
    return True


def title(event):
    auth_method = event.get("authentication_method", "<NO AUTHENTICATION METHOD FOUND>")
    login_id = event.get("login_event_id", "<NO LOGIN EVENT ID FOUND>")
    session_id = event.get("session_id", "<NO SESSION ID FOUND>")
    user_name = event.get("user_name", "<NO USER NAME FOUND>")
    return f"{user_name} accessed Snowflake with login event ID \
        {login_id} and session ID {session_id} via {auth_method}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_0109877_suspected_user_access.py
RuleID: "Snowflake.User.Access"
Description: >
  Return sessions of suspected clients as part of ongoing cyber threat activity reported May 31st, 2024
DisplayName: "Snowflake User Access"
Enabled: false
Runbook: Determine if this occurred as a result of a valid business request.
ScheduledQueries:
  - Query.Snowflake.SuspectedUserAccess
Severity: High

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.SuspectedUserAccess; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name
login_event_id
session_id
authentication_method

Response runbook

Determine if this occurred as a result of a valid business request.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "authentication_method": "Password",
  "client_application_id": "Go 1.6.24",
  "client_application_version": "1.6.24",
  "client_build_id": "",
  "client_environment": {
    "APPLICATION": "An Application",
    "OCSP_MODE": "FAIL_OPEN",
    "OS": "linux",
    "OS_VERSION": "gc-amd64"
  },
  "client_version": "0",
  "closed_reason": "ABANDONED",
  "created_on": "2023-11-08 23:40:08.602Z",
  "login_event_id": 33614596,
  "session_id": 8605339653,
  "user_name": "USER NAME"
}

Snowflake User Created

#
Severity
informational
Log types
Snowflake.QueryHistory
Tags
Snowflake, [MITRE] Persistence, [MITRE] Create Account
Source
github.com/panther-labs/panther-analysis

Detect new users created in Snowflake.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

import re

from panther_snowflake_helpers import query_history_alert_context

CREATE_USER_EXPR = re.compile(r"create user (\w+).*", flags=re.I)

CREATE_USER = ""


def rule(event):
    # pylint: disable=global-statement
    global CREATE_USER
    CREATE_USER = CREATE_USER_EXPR.match(event.get("QUERY_TEXT", ""))
    return all(
        (
            event.get("EXECUTION_STATUS") == "SUCCESS",
            event.get("QUERY_TYPE") == "CREATE_USER",
            CREATE_USER is not None,
        )
    )


def title(event):
    # pylint: disable=global-statement
    global CREATE_USER
    new_user = CREATE_USER.group(1)
    actor = event.get("user_name", "<UNKNOWN ACTOR>")
    source = event.get("p_source_label", "<UNKNOWN SOURCE>")
    return f"{source}: Snowflake user {new_user} created by {actor}"


def alert_context(event):
    return query_history_alert_context(event)

Rule specification

AnalysisType: rule
Filename: snowflake_stream_user_created.py
RuleID: Snowflake.Stream.UserCreated
DisplayName: Snowflake User Created
Enabled: false
LogTypes:
  - Snowflake.QueryHistory
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0003:T1136
Description: Detect new users created in Snowflake.
Tags:
  - Snowflake
  - '[MITRE] Persistence'
  - '[MITRE] Create Account'

Stages and Predicates

Fires on Snowflake.QueryHistory events when all of the conditions below hold.

Condition

  • EXECUTION_STATUS is SUCCESS
  • QUERY_TYPE is CREATE_USER

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
useruser_name
rolerole_name
sourcep_source_label
warehouseWAREHOUSE_NAME

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "BYTES_DELETED": 0,
  "EXECUTION_STATUS": "SUCCESS",
  "QUERY_TEXT": "CREATE USER MERCY\nPASSWORD = '☺☺☺☺☺'\nDEFAULT_ROLE = PUBLIC;",
  "QUERY_TYPE": "CREATE_USER",
  "ROLE_NAME": "ACCOUNTADMIN",
  "USER_NAME": "LEX_LUTHOR",
  "WAREHOUSE_NAME": "ADMIN_WH",
  "p_event_time": "2024-10-09 19:43:05.007000000",
  "p_log_type": "Snowflake.QueryHistory"
}

Snowflake User Created

#
Severity
informational
Tags
Snowflake, Persistence:Create Account
Source
github.com/panther-labs/panther-analysis

Detect new users created in snowflake

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

import re

_CREATE_USER_RE = re.compile(r"CREATE\s+USER\s+(?:IF\s+NOT\s+EXISTS\s+)?(\S+)", re.IGNORECASE)


def rule(_):
    return True


def title(event):
    match = _CREATE_USER_RE.search(event.get("query_text", ""))
    username = match.group(1) if match else "<UNKNOWN_USER>"
    return (
        f"Snowflake user [{username}] created by " f"[{event.get('user_name', '<UNKNOWN_ADMIN>')}]"
    )

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_user_created.py
RuleID: "Snowflake.UserCreated"
Description: >
  Detect new users created in snowflake
DisplayName: "Snowflake User Created"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.UserCreated
Severity: Info
Tags:
  - Snowflake
  - Persistence:Create Account
Reports:
  MITRE ATT&CK:
    - TA0003:T1136

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.UserCreated; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "query_text": "create USER testuser password='☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺' default_role = 'READONLY' must_change_password = true;",
  "user_name": "admin"
}

Snowflake User Daily Query Volume Spike

#
Severity
low
Tags
Snowflake, Behavior Analysis, Exfiltration:Exfiltration Over Web Service
Source
github.com/panther-labs/panther-analysis

Returns instances where a user's cumulative daily query volume is much larger than normal. Could indicate exfiltration attempts.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def rule(_):
    return True


def title(event):
    username = event.get("user_name", "<UNKNOWN USER>")
    source = event.get("p_source_label", "<UNKNOWN SOURCE>")
    return f"{source}: Abnormally large query volume from user {username}"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_user_query_volume_spike.py
RuleID: "Snowflake.Stream.UserDailyQueryVolumeSpike"
DisplayName: "Snowflake User Daily Query Volume Spike"
Enabled: true
ScheduledQueries:
  - "Snowflake User Daily Query Volume Spike"
Severity: Low
Reports:
  MITRE ATT&CK:
    - TA0010:T1567
Description: >
  Returns instances where a user's cumulative daily query volume is much larger than
  normal. Could indicate exfiltration attempts.
Runbook: >
  Review the user's query history for the past day. Identify any large queries
  and determine if any data was accessed that shouldn't be.
Tags:
  - Snowflake
  - Behavior Analysis
  - Exfiltration:Exfiltration Over Web Service

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Snowflake User Daily Query Volume Spike; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_source_label
user_name

Response runbook

Review the user's query history for the past day. Identify any large queries and determine if any data was accessed that shouldn't be.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "USER_NAME": "ZEEBLE_BROMBUS",
  "daily_bytes": 1376806.35210866,
  "mean": 729289.7142857143,
  "p_source_id": "26c3f2be-005e-443a-90cb-f623522f37a2",
  "p_source_label": "SF Prod",
  "std_dev": 206110.99016770572,
  "tend": "2024-10-21 22:09:13.47Z",
  "tstart": "2024-10-20 22:09:13.47Z",
  "zscore": 3.141592
}

Snowflake User Daily Query Volume Spike

#
Source
github.com/panther-labs/panther-analysis

Returns instances where a user's cumulative daily query volume is much larger than normal. Could indicate exfiltration attempts.

Rule specification

AnalysisType: scheduled_query
QueryName: "Snowflake User Daily Query Volume Spike"
Enabled: false
Description: >
  Returns instances where a user's cumulative daily query volume is much larger than
  normal. Could indicate exfiltration attempts.
SnowflakeQuery: |
  with t as (
    select
      dateadd('day', -1-seq4(), p_current_timestamp()) as tstart,
      dateadd('day', -seq4(), p_current_timestamp()) as tend
    from table(generator(rowcount => 90))
    ),
  data as (
    select
      user_name,
      end_time as t,
      bytes_written_to_result as n_bytes,
      p_source_id,
      p_source_label
    from panther_logs.public.snowflake_queryhistory
    where p_occurs_since('90d', , end_time)
  ),
  dimensions as (
    select distinct user_name, p_source_id from data
  ),
  axes as (
    select * from t cross join dimensions
  ),
  histogram as (
    select
      sum(data.n_bytes) as daily_bytes,
      data.p_source_label,
      axes.tstart,
      axes.tend,
      axes.user_name,
      axes.p_source_id
    from data join axes
    on
      p_occurs_between(axes.tstart, axes.tend, data, t)
      and data.user_name = axes.user_name
      and data.p_source_id = axes.p_source_id
    group by (
      axes.p_source_id,
      data.p_source_label,
      axes.tstart,
      axes.tend,
      axes.user_name
    )
  ),
  stats as (
    select
      avg(daily_bytes) as mean,
      stddev(daily_bytes) as std_dev,
      user_name,
      p_source_id,
      p_source_label
    from histogram
    group by
      p_source_id,
      p_source_label,
      user_name
  )
  select
    abs(histogram.daily_bytes - stats.mean) / COALESCE(NULLIF(stats.std_dev, 0), 1) as zscore,
    histogram.*,
    stats.mean,
    stats.std_dev
  from histogram join stats on
    histogram.user_name = stats.user_name and
    histogram.p_source_id = stats.p_source_id,
  where p_occurs_since('12h', histogram, tend)
    and zscore > 3
    and histogram.daily_bytes > 1000000 -- Minimum 1MB threshold

DatabricksQuery: |
  with t as (
    select
      p_current_timestamp() - INTERVAL (seq4 + 1) DAYS as tstart,
      p_current_timestamp() - INTERVAL seq4 DAYS as tend
    from (SELECT EXPLODE(SEQUENCE(0, 89)) AS seq4)
    ),
  data as (
    select
      user_name,
      end_time as t,
      bytes_written_to_result as n_bytes,
      p_source_id,
      p_source_label
    from panther_logs.snowflake_queryhistory
    where p_occurs_since('90d', , end_time)
  ),
  dimensions as (
    select distinct user_name, p_source_id from data
  ),
  axes as (
    select * from t cross join dimensions
  ),
  histogram as (
    select
      sum(data.n_bytes) as daily_bytes,
      data.p_source_label,
      axes.tstart,
      axes.tend,
      axes.user_name,
      axes.p_source_id
    from data join axes
    on
      p_occurs_between(axes.tstart, axes.tend, data, t)
      and data.user_name = axes.user_name
      and data.p_source_id = axes.p_source_id
    group by
      axes.p_source_id,
      data.p_source_label,
      axes.tstart,
      axes.tend,
      axes.user_name
  ),
  stats as (
    select
      avg(daily_bytes) as mean,
      stddev(daily_bytes) as std_dev,
      user_name,
      p_source_id,
      p_source_label
    from histogram
    group by
      p_source_id,
      p_source_label,
      user_name
  )
  select
    abs(histogram.daily_bytes - stats.mean) / COALESCE(NULLIF(stats.std_dev, 0), 1) as zscore,
    histogram.*,
    stats.mean,
    stats.std_dev
  from histogram join stats on
    histogram.user_name = stats.user_name and
    histogram.p_source_id = stats.p_source_id
  where p_occurs_since('12h', histogram, tend)
    and zscore > 3
    and histogram.daily_bytes > 1000000 -- Minimum 1MB threshold
Schedule:
  CronExpression: "0 0 * * *"
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
histogram

Stage 2: filter

  • zscore is greater than 3
  • histogram.daily_bytes is greater than 1000000
Window
12h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
zscoreabs ( histogram.daily_bytes - stats.mean ) / COALESCE ( NULLIF ( stats.std_dev , 0 ) , 1 )
histogram . *
stats.mean
stats.std_dev

Snowflake User Daily Query Volume Spike - Threat Hunting

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Tags
Snowflake, Threat Hunting
Source
github.com/panther-labs/panther-analysis

This query returns the most voluminous queries executed by a specific user over the past 48 hours.

Rule specification

AnalysisType: saved_query
QueryName: "Snowflake User Daily Query Volume Spike - Threat Hunting"
Description: This query returns the most voluminous queries executed by a specific
  user over the past 48 hours.
Tags:
  - Snowflake
  - Threat Hunting
Query: |-
  -- pragma: template
  -- Adjust 'username' and 'source_label' values as needed
  {% set username = 'PANTHER_AUDIT_VIEW_USER' %}
  {% set source_label = 'SF-Ben' %}

  select
      p_event_time,
      BYTES_WRITTEN_TO_RESULT,
      QUERY_TEXT,
      QUERY_TAG,
      EXECUTION_STATUS,
      QUERY_ID,
  from panther_logs.public.snowflake_queryhistory
  where p_occurs_since('48h')
  and USER_NAME = '{{username}}'
  and p_source_label = '{{source_label}}'
  order by BYTES_WRITTEN_TO_RESULT desc

Stages and Predicates

Stage 1: source

Table
panther_logs.public.snowflake_queryhistory

Stage 2: filter

  • USER_NAME is " "
  • p_source_label is " "
Window
2d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_event_time
BYTES_WRITTEN_TO_RESULT
QUERY_TEXT
QUERY_TAG
EXECUTION_STATUS
QUERY_ID

Snowflake User Enabled

#
Severity
informational
Log types
Snowflake.QueryHistory
Tags
Snowflake, [MITRE] Persistence, [MITRE] Create Account
Source
github.com/panther-labs/panther-analysis

Detects users being re-enabled in your environment.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

import re

from panther_snowflake_helpers import query_history_alert_context

USER_ENABLED_EXPR = re.compile(r"alter\s+user\s+(.+?)\s+.*?set\s+disabled\s*=\s*false", flags=re.I)

USER_ENABLED = ""


def rule(event):
    # pylint: disable=global-statement
    global USER_ENABLED
    USER_ENABLED = USER_ENABLED_EXPR.match(event.get("QUERY_TEXT", ""))

    # Exit out early to avoid needless regex
    return all(
        (
            event.get("QUERY_TYPE") == "ALTER_USER",
            event.get("EXECUTION_STATUS") == "SUCCESS",
            USER_ENABLED is not None,
        )
    )


def title(event):
    # pylint: disable=global-statement
    global USER_ENABLED
    enabled_user = USER_ENABLED.group(1)
    actor = event.get("USER_NAME", "<UNKNOWN ACTOR>")
    source = event.get("p_source_label", "<UNKNOWN SOURCE>")
    return f"{source}: Snowflake user {enabled_user} enabled by {actor}"


def alert_context(event):
    return query_history_alert_context(event)

Rule specification

AnalysisType: rule
Filename: snowflake_stream_user_enabled.py
RuleID: Snowflake.Stream.UserEnabled
DisplayName: Snowflake User Enabled
Enabled: true
LogTypes:
  - Snowflake.QueryHistory
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0003:T1136
Description: Detects users being re-enabled in your environment.
Tags:
  - Snowflake
  - '[MITRE] Persistence'
  - '[MITRE] Create Account'

Stages and Predicates

Fires on Snowflake.QueryHistory events when all of the conditions below hold.

Condition

  • QUERY_TYPE is ALTER_USER
  • EXECUTION_STATUS is SUCCESS

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
useruser_name
rolerole_name
sourcep_source_label
warehouseWAREHOUSE_NAME
USER_NAME

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "EXECUTION_STATUS": "SUCCESS",
  "QUERY_TEXT": "ALTER USER CLARK_KENT SET DISABLED=FALSE;",
  "QUERY_TYPE": "ALTER_USER",
  "ROLE_NAME": "ACCOUNTADMIN",
  "USER_NAME": "LEX_LUTHOR",
  "WAREHOUSE_NAME": "DATAOPS_WH",
  "p_event_time": "2024-10-09 21:03:25.750000000",
  "p_log_type": "Snowflake.QueryHistory",
  "p_row_id": "6283439ab35193e891ac9ea1227b",
  "p_source_label": "SF-Ben"
}

Snowflake User Enabled

#
Severity
informational
Tags
Snowflake, Persistence:Create Account
Source
github.com/panther-labs/panther-analysis

Detect users being re-enabled in your environment

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

def rule(_):
    return True


def title(event):
    query_text = event.get("query_text", "").split(" ")
    if len(query_text) > 2:
        return (
            f"Snowflake user [{query_text[2]}] "
            f"enabled by [{event.get('user_name','<UNKNOWN_ADMIN>')}]"
        )
    return (
        f"Snowflake user [<UNKNOWN_USER>] enabled by [{event.get('user_name','<UNKNOWN_ADMIN>')}]"
    )

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_user_enabled.py
RuleID: "Snowflake.UserEnabled"
Description: >
  Detect users being re-enabled in your environment
DisplayName: "Snowflake User Enabled"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.UserEnabled
Severity: Info
Tags:
  - Snowflake
  - Persistence:Create Account
Reports:
  MITRE ATT&CK:
    - TA0003:T1136

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.UserEnabled; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_name

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "query_text": "alter user TESTUSER set disabled=false;"
}

Snowflake user with key-based auth logged in with password auth

#
Severity
medium
Tags
Snowflake, Persistence:Account Manipulation
Source
github.com/panther-labs/panther-analysis

Detect when a user that has key-based authentication configured logs in with a password

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

def rule(_):
    return True


def title(event):
    return f"User {event.get('name')} logged in with Password instead of RSA key"

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_key_user_password_login.py
RuleID: "Snowflake.KeyUserPasswordLogin"
Description: >
  Detect when a user that has key-based authentication configured logs in with a password
DisplayName: "Snowflake user with key-based auth logged in with password auth"
Enabled: false
ScheduledQueries:
  - Query.Snowflake.KeyUserPasswordLogin
Tags:
  - Snowflake
  - Persistence:Account Manipulation
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
Severity: Medium

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Query.Snowflake.KeyUserPasswordLogin; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
name

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "first_authentication_factor": "RSA_KEYPAIR",
  "name": "testuser"
}

Suspicious Snowflake Sessions - Unusual Application

#
Severity
low
Group by
client_application, client_os, client_os_version
Tags
Snowflake, Behavior Analysis, Initial Access:Valid Accounts:Cloud Accounts
Source
github.com/panther-labs/panther-analysis

Detects unusual (non-common) applications and client characteristics that have been used to connect to a Snowflake account

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

def rule(_):
    return True


def title(event):
    return f"{event.get('p_source_label', '<UNKNOWN SOURCE>')}: Suspicious Application Session"


def dedup(event):
    return "-".join(
        (
            event.get("client_application", "<UNKNOWN APP>"),
            event.get("client_os", "<UNKNOWN OS>"),
            event.get("client_os_version", "<UNKNOWN VERSION>"),
        )
    )

Rule specification

AnalysisType: scheduled_rule
Filename: snowflake_suspicious_session.py
RuleID: "Snowflake.Stream.SuspiciousSession.UnusualApp"
DisplayName: Suspicious Snowflake Sessions - Unusual Application
Enabled: true
ScheduledQueries:
  - "Suspicious Snowflake Sessions - Unusual Application"
Severity: Low
Reports:
  MITRE ATT&CK:
    - TA0001:T1078.004
Description: Detects unusual (non-common) applications and client characteristics
  that have been used to connect to a Snowflake account
DedupPeriodMinutes: 1440
Tags:
  - Snowflake
  - Behavior Analysis
  - Initial Access:Valid Accounts:Cloud Accounts

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Suspicious Snowflake Sessions - Unusual Application; its Python module (Detection logic above) shapes the alert rather than filtering.

Alert deduplication
repeat matches within 1d group into one alert

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
p_source_label

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "client_application": "Snowflake Web App",
  "first_seen": "2024-10-09 14:48:33.284",
  "last_seen": "2024-10-09 15:01:13.492",
  "n_sessions": 83,
  "p_source_id": "26c3f2be-005e-443a-90cb-f623522f37a2",
  "p_source_label": "SF Prod"
}

Suspicious Snowflake Sessions - Unusual Application

#
Tags
Snowflake, Configuration Required
Source
github.com/panther-labs/panther-analysis

This query can be used for the detection of unusual, non-common applications and client characteristics that had been used to connect to the Snowflake account, using a comparison to the previous usage baseline.

Rule specification

AnalysisType: scheduled_query
QueryName: Suspicious Snowflake Sessions - Unusual Application
Enabled: false
Description: This query can be used for the detection of unusual, non-common applications
  and client characteristics that had been used to connect to the Snowflake account,
  using a comparison to the previous usage baseline.
Schedule:
  RateMinutes: 1320
  TimeoutMinutes: 2
Tags:
  - Snowflake
  - Configuration Required
SnowflakeQuery: |
  -- Adjustments as follows:
  --   adjust n_sessions threshold on line 18 as needed
  --   adjust baseline lookback period on line 16 as desired
  --   adust recent lookpack period on line 19 as desired
  --   adjust scheduled query period to be 2 hrs shorter than the lookback window on line 19
  select
      CLIENT_ENVIRONMENT:APPLICATION as client_application,
      CLIENT_ENVIRONMENT:OS as client_os,
      CLIENT_ENVIRONMENT:OS_VERSION as client_os_version,
      min(CREATED_ON) as first_seen,
      max(CREATED_ON) as last_seen,
      count(*) as n_sessions,
      p_source_id,
      p_source_label
  from panther_logs.public.snowflake_sessions
  where p_occurs_since(90d)
  group by client_application, client_os, client_os_version, p_source_id, p_source_label
  having n_sessions > 50
      and first_seen > timeadd('day', -10, p_current_timestamp())
  order by n_sessions desc

DatabricksQuery: |
  -- Adjustments as follows:
  --   adjust n_sessions threshold on line 18 as needed
  --   adjust baseline lookback period on line 16 as desired
  --   adust recent lookpack period on line 19 as desired
  --   adjust scheduled query period to be 2 hrs shorter than the lookback window on line 19
  select
      CLIENT_ENVIRONMENT:APPLICATION as client_application,
      CLIENT_ENVIRONMENT:OS as client_os,
      CLIENT_ENVIRONMENT:OS_VERSION as client_os_version,
      min(CREATED_ON) as first_seen,
      max(CREATED_ON) as last_seen,
      count(*) as n_sessions,
      p_source_id,
      p_source_label
  from panther_logs.snowflake_sessions
  where p_occurs_since(90d)
  group by client_application, client_os, client_os_version, p_source_id, p_source_label
  having n_sessions > 50
      and first_seen > DATEADD(DAY, -10, p_current_timestamp())
  order by n_sessions desc

Stages and Predicates

Stage 1: source

Table
panther_logs.public.snowflake_sessions

Stage 2: filter

Grouped by
client_application, client_os, client_os_version, p_source_id, p_source_label
Window
90d

Stage 3: having

  • n_sessions is greater than 50

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
client_applicationCLIENT_ENVIRONMENT:APPLICATION
client_osCLIENT_ENVIRONMENT:OS
client_os_versionCLIENT_ENVIRONMENT:OS_VERSION
first_seenmin ( CREATED_ON )
last_seenmax ( CREATED_ON )
n_sessionscount ( * )
p_source_id
p_source_label