Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Snowflake Scheduled Rules into Streaming Rules #1387

Merged
merged 7 commits into from
Nov 4, 2024
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_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_account_admin_assigned.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
AnalysisType: rule
Filename: snowflake_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_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_brute_force_by_ip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
AnalysisType: rule
Filename: snowflake_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_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_brute_force_by_username.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
AnalysisType: rule
Filename: snowflake_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_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_external_shares.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
AnalysisType: rule
Filename: snowflake_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
Loading