-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert Snowflake Scheduled Rules into Streaming Rules (#1387)
Co-authored-by: Ariel Ropek <[email protected]>
- Loading branch information
1 parent
18e11fd
commit 9c91c75
Showing
28 changed files
with
960 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
correlation_rules/snowflake_potential_brute_force_success.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
AnalysisType: correlation_rule | ||
RuleID: "Snowflake.PotentialBruteForceSuccess" | ||
DisplayName: "Snowflake Brute Force Login Success" | ||
Enabled: true | ||
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: | ||
- Sequence: | ||
- ID: Multiple Failed Logins | ||
RuleID: Snowflake.Stream.BruteForceByIp | ||
MinMatchCount: 5 | ||
- ID: Successful Login | ||
RuleID: Snowflake.Stream.LoginSuccess | ||
Transitions: | ||
- ID: Multiple Failed Logins FOLLOWED BY Successful Login | ||
From: Multiple Failed Logins | ||
To: Successful Login | ||
WithinTimeFrameMinutes: 30 | ||
Match: | ||
- On: CLIENT_IP | ||
Schedule: | ||
RateMinutes: 720 | ||
TimeoutMinutes: 15 | ||
LookbackWindowMinutes: 1440 | ||
Tests: | ||
- Name: Successful Bulk Login | ||
ExpectedResult: true | ||
RuleOutputs: | ||
- ID: Multiple Failed Logins | ||
Matches: | ||
CLIENT_IP: | ||
"1.1.1.1": [0, 2, 3, 6, 9, 10, 11, 15] | ||
- ID: Successful Login | ||
Matches: | ||
CLIENT_IP: | ||
"1.1.1.1": [16] | ||
- Name: Successful Login With Single Failure | ||
ExpectedResult: false | ||
RuleOutputs: | ||
- ID: Multiple Failed Logins | ||
Matches: | ||
CLIENT_IP: | ||
"1.1.1.1": [0] | ||
- ID: Successful Login | ||
Matches: | ||
CLIENT_IP: | ||
"1.1.1.1": [1] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
""" Global helpers for Snowflake streaming detections. """ | ||
|
||
|
||
def query_history_alert_context(event): | ||
return { | ||
"user": event.get("user_name", "<UNKNOWN USER>"), | ||
"role": event.get("role_name", "<UNKNOWN ROLE>"), | ||
"source": event.get("p_source_label", "<UNKNOWN SOURCE>"), | ||
# Not all queries are run in a warehouse; e.g.: getting worksheet files | ||
"warehouse": event.get("WAREHOUSE_NAME", "<NO WAREHOUSE>"), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
AnalysisType: global | ||
Filename: panther_snowflake_helpers.py | ||
GlobalID: "panther_snowflake_helpers" | ||
Description: > | ||
Global helpers for Snowflake streaming detections |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
AnalysisType: pack | ||
PackID: PantherManaged.SnowflakeStreaming | ||
DisplayName: "Panther Snowflake Real-Time Rules" | ||
Description: Group of all streaming (real-time) Snowflake detections | ||
PackDefinition: | ||
IDs: | ||
# Correlation Rules | ||
- Snowflake.PotentialBruteForceSuccess | ||
# Helpers | ||
- panther_snowflake_helpers | ||
# Rules | ||
- Snowflake.Stream.AccountAdminGranted | ||
- Snowflake.Stream.BruteForceByIp | ||
- Snowflake.Stream.BruteForceByUsername | ||
- Snowflake.Stream.ExternalShares | ||
- Snowflake.Stream.FileDownloaded | ||
- Snowflake.Stream.LoginSuccess | ||
- Snowflake.Stream.LoginWithoutMFA | ||
- Snowflake.Stream.PublicRoleGrant | ||
- Snowflake.Stream.TableCopiedIntoStage | ||
- Snowflake.Stream.TempStageCreated | ||
- Snowflake.Stream.UserCreated | ||
- Snowflake.Stream.UserEnabled |
12 changes: 12 additions & 0 deletions
12
rules/snowflake_rules/snowflake_stream_account_admin_assigned.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
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", "<UNKNWON SNOWFLAKE SOURCE>") | ||
target = event.get("GRANTED_TO", "<UNKNWON 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}" |
41 changes: 41 additions & 0 deletions
41
rules/snowflake_rules/snowflake_stream_account_admin_assigned.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
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' | ||
Tests: | ||
- Name: Admin Role Assigned | ||
ExpectedResult: true | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 11:24:50.682000000", | ||
"p_log_type": "Snowflake.GrantsToUsers", | ||
"p_source_label": "Snowflake Prod", | ||
"CREATED_ON": "2024-10-08 11:24:50.682000000", | ||
"GRANTED_BY": "SNOWFLAKE", | ||
"GRANTED_TO": "APPLICATION_ROLE", | ||
"GRANTEE_NAME": "TRUST_CENTER_ADMIN" | ||
} | ||
- Name: Non-Admin Role Assigned | ||
ExpectedResult: false | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 11:24:50.682000000", | ||
"p_log_type": "Snowflake.GrantsToUsers", | ||
"p_source_label": "Snowflake Prod", | ||
"CREATED_ON": "2024-10-08 11:24:50.682000000", | ||
"GRANTED_BY": "SNOWFLAKE", | ||
"GRANTED_TO": "APPLICATION_ROLE", | ||
"GRANTEE_NAME": "TRUST_CENTER_VIEWER" | ||
} |
18 changes: 18 additions & 0 deletions
18
rules/snowflake_rules/snowflake_stream_brute_force_by_ip.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
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" | ||
|
||
|
||
def title(event): | ||
return ( | ||
"Login attempts from IP " | ||
f"{event.get('CLIENT_IP', '<UNKNOWN IP>')} " | ||
"have exceeded the failed logins threshold" | ||
) | ||
|
||
|
||
def dedup(event): | ||
return event.get("CLIENT_IP", "<UNKNOWN IP>") + event.get( | ||
"REPORTED_CLIENT_TYPE", "<UNKNOWN CLIENT TYPE>" | ||
) |
56 changes: 56 additions & 0 deletions
56
rules/snowflake_rules/snowflake_stream_brute_force_by_ip.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
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' | ||
Tests: | ||
- Name: Successful Login | ||
ExpectedResult: false | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 14:38:46.061000000", | ||
"p_log_type": "Snowflake.LoginHistory", | ||
"p_source_label": "Snowflake Prod", | ||
"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": "[email protected]" | ||
} | ||
- Name: Unsuccessful Login | ||
ExpectedResult: true | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 14:38:46.061000000", | ||
"p_log_type": "Snowflake.LoginHistory", | ||
"p_source_label": "Snowflake Prod", | ||
"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": "[email protected]" | ||
} |
16 changes: 16 additions & 0 deletions
16
rules/snowflake_rules/snowflake_stream_brute_force_by_username.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
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" | ||
|
||
|
||
def title(event): | ||
return ( | ||
f"User {event.get('USER_NAME', '<UNKNOWN USER>')} has exceeded the failed logins threshold" | ||
) | ||
|
||
|
||
def dedup(event): | ||
return event.get("USER_NAME", "<UNKNOWN USER>") + event.get( | ||
"REPORTED_CLIENT_TYPE", "<UNKNOWN CLIENT TYPE>" | ||
) |
56 changes: 56 additions & 0 deletions
56
rules/snowflake_rules/snowflake_stream_brute_force_by_username.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
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' | ||
Tests: | ||
- Name: Successful Login | ||
ExpectedResult: false | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 14:38:46.061000000", | ||
"p_log_type": "Snowflake.LoginHistory", | ||
"p_source_label": "Snowflake Prod", | ||
"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": "[email protected]" | ||
} | ||
- Name: Unsuccessful Login | ||
ExpectedResult: true | ||
Log: | ||
{ | ||
"p_event_time": "2024-10-08 14:38:46.061000000", | ||
"p_log_type": "Snowflake.LoginHistory", | ||
"p_source_label": "Snowflake Prod", | ||
"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": "[email protected]" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# CONFIGURATION REQUIRED | ||
# Be sure to add code to exclude any transfers from acounts designed to host data shares. Either | ||
# add those account names to the set below, or add a rule filter to exclude events with those | ||
# account names. | ||
DATA_SHARE_HOSTING_ACCOUNTS = { | ||
# Add account names here | ||
} | ||
|
||
|
||
def rule(event): | ||
return all( | ||
[ | ||
event.get("ACCOUNT_NAME") not in get_data_share_hosting_accounts(), | ||
event.get("SOURCE_CLOUD"), | ||
event.get("TARGET_CLOUD"), | ||
event.get("BYTES_TRANSFERRED", 0) > 0, | ||
] | ||
) | ||
|
||
|
||
def title(event): | ||
return ( | ||
f"{event.get('ORGANIZATION_NAME', '<UNKNOWN ORGANIZATION>')}: " | ||
"A data export has been initiated from source cloud " | ||
f"{event.get('SOURCE_CLOUD', '<UNKNOWN SOURCE CLOUD>')} " | ||
f"in source region {event.get('SOURCE_REGION', '<UNKNOWN SOURCE REGION>')} " | ||
f"to target cloud {event.get('TARGET_CLOUD', '<UNKNOWN TARGET CLOUD>')} " | ||
f"in target region {event.get('TARGET_REGION', '<UNKNOWN TARGET REGION>')} " | ||
f"with transfer type {event.get('TRANSFER_TYPE', '<UNKNOWN TRANSFER TYPE>')} " | ||
f"for {event.get('BYTES_TRANSFERRED', '<UNKNOWN VOLUME>')} bytes" | ||
) | ||
|
||
|
||
def get_data_share_hosting_accounts(): | ||
"""Getter function. Used so we can mock during unit tests.""" | ||
return DATA_SHARE_HOSTING_ACCOUNTS |
54 changes: 54 additions & 0 deletions
54
rules/snowflake_rules/snowflake_stream_external_shares.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
AnalysisType: rule | ||
Filename: snowflake_stream_external_shares.py | ||
RuleID: Snowflake.Stream.ExternalShares | ||
DisplayName: Snowflake External Data Share | ||
Enabled: true | ||
LogTypes: | ||
- Snowflake.DataTransferHistory | ||
Severity: Medium | ||
Reports: | ||
MITRE ATT&CK: | ||
- TA0010:T1537 | ||
Description: Detect when an external share has been initiated from one source cloud | ||
to another target cloud. | ||
Runbook: Determine if this occurred as a result of a valid business request. | ||
Tags: | ||
- Configuration Required | ||
- Snowflake | ||
- '[MITRE] Exfiltration' | ||
- '[MITRE] Transfer Data to Cloud Account' | ||
Tests: | ||
- Name: Allowed Share | ||
ExpectedResult: false | ||
Mocks: | ||
- objectName: get_data_share_hosting_accounts | ||
returnValue: '{DP_EUROPE}, {DP_ASIA}, {DP_AMERICA}' | ||
Log: | ||
{ | ||
"ORGANIZATION_NAME": "DAILY_PLANET", | ||
"ACCOUNT_NAME": "DP_EUROPE", | ||
"REGION": "US-EAST-2", | ||
"SOURCE_CLOUD": "AWS", | ||
"SOURCE_REGION": "US-EAST-2", | ||
"TARGET_CLOUD": "AWS", | ||
"TARGET_REGION": "EU-WEST-1", | ||
"BYTES_TRANSFERRED": 61235879, | ||
"TRANSFER_TYPE": "COPY" | ||
} | ||
- Name: Disallowed Share | ||
ExpectedResult: true | ||
Mocks: | ||
- objectName: get_data_share_hosting_accounts | ||
returnValue: '{DP_EUROPE}, {DP_ASIA}, {DP_AMERICA}' | ||
Log: | ||
{ | ||
"ORGANIZATION_NAME": "LEXCORP", | ||
"ACCOUNT_NAME": "LEX_SECRET_ACCOUNT", | ||
"REGION": "US-EAST-2", | ||
"SOURCE_CLOUD": "AWS", | ||
"SOURCE_REGION": "US-EAST-2", | ||
"TARGET_CLOUD": "AWS", | ||
"TARGET_REGION": "EU-WEST-1", | ||
"BYTES_TRANSFERRED": 61235879, | ||
"TRANSFER_TYPE": "COPY" | ||
} |
Oops, something went wrong.