Detection rules › Panther
Panther rules: snowflake
Query.Snowflake.AccountAdminGranted
#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
Stage 2: filter
p_event_timeis presentgrantee_nameis presentgranted_tois presentrolematches the pattern*admin*deleted_onis empty
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
deleted_on | is_null | field:"deleted_on" kind:is_null | |
granted_to | is_not_null | field:"granted_to" kind:is_not_null | |
grantee_name | is_not_null | field:"grantee_name" kind:is_not_null | |
p_event_time | is_not_null | field:"p_event_time" kind:is_not_null | |
role | wildcard |
| field:"role" kind:wildcard value:"*admin*" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | created_on |
role | |
granted_to | grantee_name |
granted_by |
Query.Snowflake.BruteForceByIp
#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
Stage 2: filter
event_typeisLOGINerror_codeis presenterror_codeis not394304
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
countsis at least5
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
counts | ge |
| field:"counts" kind:ge value:"5" |
error_code | is_not_null | field:"error_code" kind:is_not_null | |
error_code | ne |
| field:"error_code" kind:ne value:"394304" |
event_type | eq |
| field:"event_type" kind:eq value:"LOGIN" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
client_ip | |
reported_client_type | |
error_codes | ARRAY_AGG ( DISTINCT error_code ) |
error_messages | ARRAY_AGG ( DISTINCT error_message ) |
counts | COUNT ( event_id ) |
Query.Snowflake.BruteForceByUsername
#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
Stage 2: filter
event_typeisLOGINerror_codeis presenterror_codeis not394304
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
countsis at least5
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
counts | ge |
| field:"counts" kind:ge value:"5" |
error_code | is_not_null | field:"error_code" kind:is_not_null | |
error_code | ne |
| field:"error_code" kind:ne value:"394304" |
event_type | eq |
| field:"event_type" kind:eq value:"LOGIN" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_name | |
reported_client_type | |
error_codes | ARRAY_AGG ( DISTINCT error_code ) |
error_messages | ARRAY_AGG ( DISTINCT error_message ) |
counts | COUNT ( event_id ) |
Query.Snowflake.ClientIp
#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
Stage 2: filter
client_ipis one of104.223.91.28,198.54.135.99,184.147.100.29,146.70.117.210,198.54.130.153
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
client_ip | in |
| field:"client_ip" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | event_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.
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
Stage 2: filter
any of:
all of:
execution_statusisSUCCESSquery_typeis not one ofSELECT,EXPLAINuser_nameis not one ofPANTHER_ADMIN,PANTHERACCOUNTADMINany of:
role_namedoes not match the patternPANTHER?READONLY*query_textdoes not match the patternCOPY INTO @panther?lookups.public.panther?lut?export?stage*
user_nameis not one ofPANTHER_ADMIN,PANTHERACCOUNTADMINany of:
query_textmatches the pattern*create role*query_textmatches the pattern*manage grants*query_textmatches the pattern*create integration*query_textmatches the pattern*alter integration*query_textmatches the pattern*create share*query_textmatches the pattern*create account*query_textmatches the pattern*monitor usage*query_textmatches the pattern*ownership*query_textmatches the pattern*drop table*query_textmatches the pattern*drop database*query_textmatches the pattern*create stage*query_textmatches the pattern*drop stage*query_textmatches the pattern*alter stage*query_textmatches the pattern*create user*query_textmatches the pattern*alter user*query_textmatches the pattern*drop user*query_textmatches the pattern*create?network?policy*query_textmatches the pattern*alter?network?policy*query_textmatches the pattern*drop?network?policy*query_textmatches the pattern*copy*
all of:
query_textmatches the pattern*grant*accountadmin*to*query_typeisGRANTexecution_statusisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_text | wildcard |
| field:"query_text" kind:wildcard |
query_type | eq |
| field:"query_type" kind:eq value:"GRANT" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
query_text | |
user_name | |
role_name | |
p_event_time | start_time |
end_time |
Query.Snowflake.CopyIntoStage
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | No 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
Stage 2: filter
query_typeisUNLOADstageis presentexecution_statusisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_type | eq |
| field:"query_type" kind:eq value:"UNLOAD" |
stage | is_not_null | field:"stage" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_name | |
role_name | |
p_event_time | start_time |
query_type | |
execution_status | |
stage | regexp_substr ( query_text , 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)' , 1 , 1 , 'i' , 2 ) |
path | regexp_substr ( query_text , 'COPY\\s+INTO\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s+FROM' , 1 , 1 , 'i' , 2 ) |
query_text |
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.
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
Stage 2: filter
event_typeisLOGINerror_codeis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
error_code | is_not_null | field:"error_code" kind:is_not_null | |
event_type | eq |
| field:"event_type" kind:eq value:"LOGIN" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
client_ip | |
user_name | |
reported_client_type | |
error_code | |
error_message | |
count_by_ip | count ( DISTINCT event_id ) OVER ( PARTITION BY client_ip ) |
count_by_username | count ( DISTINCT event_id ) OVER ( PARTITION BY user_name ) |
Query.Snowflake.FileDownloaded
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | No 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
Stage 2: filter
query_typeisGET_FILESpathis presentexecution_statusisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
path | is_not_null | field:"path" kind:is_not_null | |
query_type | eq |
| field:"query_type" kind:eq value:"GET_FILES" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_name | |
role_name | |
p_event_time | start_time |
query_type | |
execution_status | |
stage | regexp_substr ( query_text , 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\.]+)' , 1 , 1 , 'i' , 2 ) |
path | regexp_substr ( query_text , 'GET\\s+(\\$\\$|\\\')?@([a-zA-Z0-9_\\./]+)(\\$\\$|\\\')?\\s' , 1 , 1 , 'i' , 2 ) |
query_text |
Query.Snowflake.KeyUserPasswordLogin
#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
Stage 2: filter
is_successisYESfirst_authentication_factoris notRSA_KEYPAIR
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.
| Field | Kind | Values | Search |
|---|---|---|---|
first_authentication_factor | ne |
| field:"first_authentication_factor" kind:ne value:"RSA_KEYPAIR" |
is_success | eq |
| field:"is_success" kind:eq value:"YES" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
u.name | |
first_authentication_factor | |
second_authentication_factor | |
counts | count ( * ) |
Query.Snowflake.MFALogin
#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
Stage 2: filter
event_typeisLOGINfirst_authentication_factorisPASSWORDsecond_authentication_factoris empty
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 | Source |
|---|---|
p_event_time | event_timestamp |
user_name | |
client_ip | |
reported_client_type | |
reported_client_version | |
first_authentication_factor | |
second_authentication_factor |
Query.Snowflake.Multiple.Logins.Followed.By.Success
#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
Stage 2: filter
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
* |
Query.Snowflake.SuspectedUserAccess
#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
Stage 2: filter
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | CREATED_ON |
* |
Query.Snowflake.TempStageCreated
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | No 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
Stage 2: filter
query_typeisCREATEstageis presentexecution_statusisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_type | eq |
| field:"query_type" kind:eq value:"CREATE" |
stage | is_not_null | field:"stage" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_name | |
role_name | |
p_event_time | start_time |
query_type | |
execution_status | |
stage | 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 ) |
query_text |
Query.Snowflake.ThreatHunting.ClientIp
#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
Stage 2: filter
client_ipis one of104.223.91.28,198.54.135.99,184.147.100.29,146.70.117.210,198.54.130.153
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
client_ip | in |
| field:"client_ip" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | event_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.
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
Stage 2: filter
any of:
all of:
execution_statusisSUCCESSquery_typeis not one ofSELECTuser_nameis not one ofPANTHER_ADMIN,PANTHERACCOUNTADMINany of:
query_textmatches the pattern*create role*query_textmatches the pattern*manage grants*query_textmatches the pattern*create integration*query_textmatches the pattern*alter integration*query_textmatches the pattern*create share*query_textmatches the pattern*create account*query_textmatches the pattern*monitor usage*query_textmatches the pattern*ownership*query_textmatches the pattern*drop table*query_textmatches the pattern*drop database*query_textmatches the pattern*create stage*query_textmatches the pattern*drop stage*query_textmatches the pattern*alter stage*query_textmatches the pattern*create user*query_textmatches the pattern*alter user*query_textmatches the pattern*drop user*query_textmatches the pattern*create?network?policy*query_textmatches the pattern*alter?network?policy*query_textmatches the pattern*drop?network?policy*query_textmatches the pattern*copy*
all of:
query_textmatches the pattern*grant*accountadmin*to*query_typeisGRANTexecution_statusisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_text | wildcard |
| field:"query_text" kind:wildcard |
query_type | eq |
| field:"query_type" kind:eq value:"GRANT" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
query_text | |
user_name | |
role_name | |
p_event_time | start_time |
end_time |
Query.Snowflake.ThreatHunting.SuspectedUserAccess
#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
Stage 2: filter
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | CREATED_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.
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
Stage 2: filter
user_nameis<SUSPECTED_USER>start_timeis at least2024-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.
| Field | Kind | Values | Search |
|---|---|---|---|
start_time | ge |
| field:"start_time" kind:ge value:"2024-04-01" |
user_name | eq |
| field:"user" kind:eq value:"<SUSPECTED_USER>" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
* |
Query.Snowflake.UserCreated
#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
Stage 2: filter
execution_statusisSUCCESSquery_typeisCREATE_USERquery_textmatches the pattern*create*user*
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_text | wildcard |
| field:"query_text" kind:wildcard value:"*create*user*" |
query_type | eq |
| field:"query_type" kind:eq value:"CREATE_USER" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | start_time |
end_time | |
query_type | |
query_text | |
user_name | |
role_name |
Query.Snowflake.UserEnabled
#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
Stage 2: filter
execution_statusisSUCCESSquery_typeisALTER_USERany of:
query_textmatches the pattern*alter user*set disabled = false*query_textmatches the pattern*alter user*set disabled= false*query_textmatches the pattern*alter user*set disabled =false*query_textmatches the pattern*alter user*set disabled=false*
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
execution_status | eq |
| field:"execution_status" kind:eq value:"SUCCESS" |
query_text | wildcard |
| field:"query_text" kind:wildcard |
query_type | eq |
| field:"query_type" kind:eq value:"ALTER_USER" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | start_time |
end_time | |
query_type | |
query_text | |
user_name | |
role_name |
Snowflake Account Admin Granted
#Detect when account admin is granted.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#Detect when account admin is granted.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_ONis emptyGRANTEE_NAMEcontainsadmin
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DELETED_ON | is_null | field:"DELETED_ON" kind:is_null | |
GRANTEE_NAME | contains |
| 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
#Detect brute force attacks by monitoring for failed logins from the same IP address
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.
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
#Detect brute force attacks by monitorign failed logins from the same IP address
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisLOGINIS_SUCCESSisNOERROR_MESSAGEis notOVERFLOW_FAILURE_EVENTS_ELIDED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ERROR_MESSAGE | ne |
| field:"ERROR_MESSAGE" kind:ne value:"OVERFLOW_FAILURE_EVENTS_ELIDED" |
EVENT_TYPE | eq |
| field:"EVENT_TYPE" kind:eq value:"LOGIN" |
IS_SUCCESS | eq |
| field:"IS_SUCCESS" kind:eq value:"NO" |
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
#Detect brute force attacks by monitorign failed logins from the same IP address
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisLOGINIS_SUCCESSisNOERROR_MESSAGEis notOVERFLOW_FAILURE_EVENTS_ELIDED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ERROR_MESSAGE | ne |
| field:"ERROR_MESSAGE" kind:ne value:"OVERFLOW_FAILURE_EVENTS_ELIDED" |
EVENT_TYPE | eq |
| field:"EVENT_TYPE" kind:eq value:"LOGIN" |
IS_SUCCESS | eq |
| field:"IS_SUCCESS" kind:eq value:"NO" |
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
#Detect brute force attacks by monitoring for failed logins by the same username
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.
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
#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
#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
#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
#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
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
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
#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
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
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 File Downloaded
#A file was downloaded from a stage
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#A file was downloaded from a stage.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisGET_FILESEXECUTION_STATUSisSUCCESSQUERY_TEXTis notGET '@~/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.
| Field | Kind | Values | Search |
|---|---|---|---|
EXECUTION_STATUS | eq |
| field:"EXECUTION_STATUS" kind:eq value:"SUCCESS" |
QUERY_TEXT | ne |
| field:"QUERY_TEXT" kind:ne value:"GET '@~/worksheet_data/metadata' 'file:///'" |
QUERY_TYPE | eq |
| field:"QUERY_TYPE" kind:eq value:"GET_FILES" |
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
#Detect additional grants to the public role.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_NAMEispublic
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
GRANTEE_NAME | eq |
| 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
#Detect snowflake logins without multifactor authentication
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
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_nameis not one ofPANTHER_READONLY,PANTHER_ADMIN,PANTHERACCOUNTADMIN
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
user_name | in | PANTHERACCOUNTADMIN, PANTHER_ADMIN, PANTHER_READONLY | excludes: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
#Detect Snowflake logins without multifactor authentication
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
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_TYPEisLOGINIS_SUCCESSisYESFIRST_AUTHENTICATION_FACTORisPASSWORDSECOND_AUTHENTICATION_FACTORis emptyUSER_NAMEis not one ofPANTHER_READONLY,PANTHER_ADMIN,PANTHERACCOUNTADMIN
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
USER_NAME | in | PANTHERACCOUNTADMIN, PANTHER_ADMIN, PANTHER_READONLY | excludes: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
#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
#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
| Tactic | Techniques |
|---|---|
| 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_TYPEisLOGINIS_SUCCESSisNOERROR_MESSAGEis notOVERFLOW_FAILURE_EVENTS_ELIDED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ERROR_MESSAGE | ne |
| field:"ERROR_MESSAGE" kind:ne value:"OVERFLOW_FAILURE_EVENTS_ELIDED" |
EVENT_TYPE | eq |
| field:"EVENT_TYPE" kind:eq value:"LOGIN" |
IS_SUCCESS | eq |
| field:"IS_SUCCESS" kind:eq value:"NO" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
client_ip | CLIENT_IP |
user_name | USER_NAME |
client_type | REPORTED_CLIENT_TYPE |
error_code | ERROR_CODE |
error_message | ERROR_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
#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_TYPEisLOGINIS_SUCCESSisYES
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EVENT_TYPE | eq |
| field:"EVENT_TYPE" kind:eq value:"LOGIN" |
IS_SUCCESS | eq |
| field:"IS_SUCCESS" kind:eq value:"YES" |
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
#A table was copied into a stage.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisUNLOADEXECUTION_STATUSisSUCCESS
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EXECUTION_STATUS | eq |
| field:"EXECUTION_STATUS" kind:eq value:"SUCCESS" |
QUERY_TYPE | eq |
| field:"QUERY_TYPE" kind:eq value:"UNLOAD" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | 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": "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
#A table was copied into a stage
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#A temporary stage was created.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisCREATEEXECUTION_STATUSisSUCCESS
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EXECUTION_STATUS | eq |
| field:"EXECUTION_STATUS" kind:eq value:"SUCCESS" |
QUERY_TYPE | eq |
| field:"QUERY_TYPE" kind:eq value:"CREATE" |
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
#A temporary stage was created
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#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
#Detect new users created in Snowflake.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_STATUSisSUCCESSQUERY_TYPEisCREATE_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.
| Field | Kind | Values | Search |
|---|---|---|---|
EXECUTION_STATUS | eq |
| field:"EXECUTION_STATUS" kind:eq value:"SUCCESS" |
QUERY_TYPE | eq |
| field:"QUERY_TYPE" kind:eq value:"CREATE_USER" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | user_name |
role | role_name |
source | p_source_label |
warehouse | WAREHOUSE_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
#Detect new users created in snowflake
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#Returns instances where a user's cumulative daily query volume is much larger than normal. Could indicate exfiltration attempts.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#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
Stage 2: filter
zscoreis greater than3histogram.daily_bytesis greater than1000000
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
histogram.daily_bytes | gt |
| field:"histogram.daily_bytes" kind:gt value:"1000000" |
zscore | gt |
| field:"zscore" kind:gt value:"3" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
zscore | abs ( 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.
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
Stage 2: filter
USER_NAMEis" "p_source_labelis" "
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
USER_NAME | eq | field:"USER_NAME" kind:eq value:" " | |
p_source_label | eq | field:"p_source_label" kind:eq value:" " |
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
#Detects users being re-enabled in your environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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_TYPEisALTER_USEREXECUTION_STATUSisSUCCESS
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EXECUTION_STATUS | eq |
| field:"EXECUTION_STATUS" kind:eq value:"SUCCESS" |
QUERY_TYPE | eq |
| field:"QUERY_TYPE" kind:eq value:"ALTER_USER" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | user_name |
role | role_name |
source | p_source_label |
warehouse | WAREHOUSE_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
#Detect users being re-enabled in your environment
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#Detect when a user that has key-based authentication configured logs in with a password
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
#Detects unusual (non-common) applications and client characteristics that have been used to connect to a Snowflake account
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.
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
#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
Stage 2: filter
Stage 3: having
n_sessionsis greater than50
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.
| Field | Kind | Values | Search |
|---|---|---|---|
n_sessions | gt |
| field:"n_sessions" kind:gt value:"50" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
client_application | CLIENT_ENVIRONMENT:APPLICATION |
client_os | CLIENT_ENVIRONMENT:OS |
client_os_version | CLIENT_ENVIRONMENT:OS_VERSION |
first_seen | min ( CREATED_ON ) |
last_seen | max ( CREATED_ON ) |
n_sessions | count ( * ) |
p_source_id | |
p_source_label |