Skip to content

Commit

Permalink
Convert Snowflake Scheduled Rules into Streaming Rules (#1387)
Browse files Browse the repository at this point in the history
Co-authored-by: Ariel Ropek <[email protected]>
  • Loading branch information
ben-githubs and arielkr256 authored Nov 4, 2024
1 parent 18e11fd commit 9c91c75
Show file tree
Hide file tree
Showing 28 changed files with 960 additions and 0 deletions.
47 changes: 47 additions & 0 deletions correlation_rules/snowflake_potential_brute_force_success.yml
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]
11 changes: 11 additions & 0 deletions global_helpers/panther_snowflake_helpers.py
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>"),
}
5 changes: 5 additions & 0 deletions global_helpers/panther_snowflake_helpers.yml
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
23 changes: 23 additions & 0 deletions packs/snowflake_streaming.yml
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 rules/snowflake_rules/snowflake_stream_account_admin_assigned.py
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 rules/snowflake_rules/snowflake_stream_account_admin_assigned.yml
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 rules/snowflake_rules/snowflake_stream_brute_force_by_ip.py
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 rules/snowflake_rules/snowflake_stream_brute_force_by_ip.yml
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 rules/snowflake_rules/snowflake_stream_brute_force_by_username.py
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 rules/snowflake_rules/snowflake_stream_brute_force_by_username.yml
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]"
}
36 changes: 36 additions & 0 deletions rules/snowflake_rules/snowflake_stream_external_shares.py
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 rules/snowflake_rules/snowflake_stream_external_shares.yml
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"
}
Loading

0 comments on commit 9c91c75

Please sign in to comment.