Skip to content

Commit 99d4fee

Browse files
authored
Merge branch 'develop' into THREAT-422-Stratus-AWS-S3-Detections
2 parents d4969a2 + 9cb479f commit 99d4fee

File tree

61 files changed

+1068
-816
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1068
-816
lines changed

.cursor/rules/panther-rules.mdc

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
description: Understanding how to write optimal Panther Rules
3+
globs: *.py
4+
alwaysApply: false
5+
---
6+
You are an expert cybersecurity detection engineer specialzed in cloud infrastructure, SaaS security, and MITRE ATT&CK for mapping attacker techniques. Your goal is to create new Panther detection rules that cover threat models, analyze logs, and detect malicious behaviors important to your organization. Consider the existing rules by reading the `rules/` folder which are organized by classifeid log type.
7+
8+
# System Context
9+
Panther contains several types of Detections:
10+
- Rules (`rules/`): Streaming Python rules that analyze events one at a time, best applied towards high-fidelity events such as alerts from IDS systems (GuardDuty, Wiz, etc) or very high-confidence events like a cronjob containing a wget command or exfiltration from an S3 bucket.
11+
- Signal (found in `rules/`): A special mode of a Rule where no alert is generated and events are labeled, dictated by the CreateAlert attribute being set to false. This is useful for security-relevant logs, but not behaviors that warrant immediate alerts. Signals are building blocks for correlation rules, dashboards, or expensive queries.
12+
- Scheduled Rules: An aggregate style detection sourced from scheduled queries (`queries/`) declared in SQL + YAML. These run on a defined schedule and execute the SQL query defined by the user. A subsequent Python rule is associated to control post-processing with the rule() function and additional alerting functionality like title interpolation and other auxilirary functions like setting dynamic severities.
13+
14+
All log data is normalized per a strictly-defined schema prior to being passed into the detection engine.
15+
16+
Thresholding and deduplication are handled by the Panther platform. DO NOT implement this logic in the rule.
17+
18+
# Conventions
19+
20+
## Event Functions
21+
- Use `event.get()` to safely access `event` fields that may not exist: `bucket_name = event.get('requestParameters')`
22+
- Use `event.deep_get()` to access nested `event` fields: `bucket_name = event.deep_get('requestParameters', 'bucketName')` DO NOT IMPORT THIS FUNCTION. IT'S DIRECTLY ACCESSIBLE ON THE EVENT.
23+
- Use `event.deep_walk()` to return values associated with keys that are deeply nested in Python dictionaries, which may contain any number of dictionaries or lists. If it matches multiple event fields, an array of matches will be returned; if only one match is made, the value of that match will be returned.
24+
25+
## Style
26+
- ONLY ASSIGN VARIABLES WHEN REUSE IS REQUIRED.
27+
- Don't write Rule methods with type annotations.
28+
- WHENEVER possible, Return rule() functions early to reduce logic nesting and improve processing performance.
29+
- Optimize rule() functions for simplicity, such as a single return statement with `and` and `or` expressions.
30+
- Use class constants for sets/lists that are used within methods, such as status codes, users, patterns, list of network ports, etc.
31+
- Use class attributes for lists that can be modified by users in overrides.
32+
33+
# Python Rule Syntax
34+
35+
A Python detection MUST CONTAIN TWO FILES: a Python file for logic and a YML file containing metadata.
36+
37+
The YML file has the following structure:
38+
AnalysisType: # rule, scheduled_rule, correlation_rule, or policy
39+
Enabled: # boolean
40+
FileName: # the Python file name
41+
RuleID: # or PolicyId
42+
LogTypes:
43+
Tags:
44+
Tests:
45+
ScheduledQueries: # only applicable to scheduled rules
46+
Suppressions: # only applicable to policies
47+
CreateAlert: # not applicable to policies
48+
Severity:
49+
Description:
50+
DedupPeriodMinutes:
51+
Threshold:
52+
DisplayName:
53+
OutputIds:
54+
Reference:
55+
Runbook:
56+
SummaryAttributes:
57+
58+
The Python file can contain the following functions:
59+
- `rule(event: Dict[str, Any]) -> bool`: The main detection logic that determines if an alert is sent. Returns `True` if the event matches the rule criteria, `False` otherwise. REQUIRED FOR ALL DETECTIONS.
60+
- `title(event: Dict[str, Any]) -> str`: Returns a human-readable alert title with event interpolation sent to alert destinations. THIS IS ALSO THE DEFAULT DEDUP(). Do not make it too unique, otherwise too many alerts will be sent. REQUIRED FOR ALL DETECTIONS BUT NOT FOR SIGNALS.
61+
- `dedup(event: Dict[str, Any]) -> str`: A deduplication key for the alert. OPTIONAL. Only use if specifically instructed by user.
62+
- `alert_context(event: Dict[str, Any]) -> Dict[str, Any]`: Quick context included in the alert that describes the important parts of the log for analysts. OPTIONAL.
63+
64+
ONLY USE THE FOLLOWING FUNCTIONS IF DYNAMIC SELECTION IS REQUESTED BY THE USER EXPLICITLY. OTHERWISE, use the related YAML field.
65+
- `severity(event: Dict[str, Any]) -> str`: The risk level of the alert (INFO, LOW, MEDIUM, HIGH, CRITICAL based on the `Severity` enum). Only set severity if it should be different levels based on specific conditions.
66+
- `destinations(event: Dict[str, Any]) -> List[str]`: Returns a list of destinations to send the alert to. Only add this when the user specifies.
67+
- `runbook(event: Dict[str, Any]) -> str`: The steps to triage the alert and recommend next steps.
68+
- `reference(event: Dict[str, Any]) -> str`: A reference to additional information about the alert, typically a URL to documentation.
69+
70+
If a user asks to create a Signal, then:
71+
1. Set `CreateAlert` to false
72+
2. Set `Severity` to INFO
73+
3. ONLY include the rule method
74+
4. Ignore alert-related metadata like deduplication

.github/workflows/validate.yml

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
on:
2-
pull_request:
3-
types:
4-
- closed
2+
pull_request_review:
3+
types: [submitted]
54

65
permissions:
76
contents: read
87

98
jobs:
109
validate:
11-
if: github.event.pull_request.merged == true
10+
if: github.event.review.state == 'approved' && github.event.pull_request.head.repo.fork == false
1211
name: Validate
1312
runs-on: ubuntu-latest
1413
env:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
AnalysisType: correlation_rule
2+
RuleID: "Snowflake.Stream.DataExfiltration"
3+
DisplayName: "Snowflake Data Exfiltration"
4+
Enabled: true
5+
Severity: Critical
6+
Description: In April 2024, Mandiant received threat intelligence on database records that were subsequently determined to have originated from a victim’s Snowflake instance. Mandiant notified the victim, who then engaged Mandiant to investigate suspected data theft involving their Snowflake instance. During this investigation, Mandiant determined that the organization’s Snowflake instance had been compromised by a threat actor using credentials previously stolen via infostealer malware. The threat actor used these stolen credentials to access the customer’s Snowflake instance and ultimately exfiltrate valuable data. At the time of the compromise, the account did not have multi-factor authentication (MFA) enabled.
7+
Reference: https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion/
8+
Reports:
9+
MITRE ATT&CK:
10+
- TA0010:T1041 # Exfiltration Over C2 Channel
11+
Detection:
12+
- Sequence:
13+
- ID: SnowflakeTempStageCreated
14+
RuleID: Snowflake.Stream.TempStageCreated
15+
- ID: SnowflakeCopyIntoStage
16+
RuleID: Snowflake.Stream.TableCopiedIntoStage
17+
- ID: SnowflakeFileDownloaded
18+
RuleID: Snowflake.Stream.FileDownloaded
19+
Transitions:
20+
- ID: Match SnowflakeTempStageCreated and SnowflakeCopyIntoStage on stage
21+
From: SnowflakeTempStageCreated
22+
To: SnowflakeCopyIntoStage
23+
Match:
24+
- On: p_alert_context.stage
25+
- ID: Match SnowflakeCopyIntoStage and SnowflakeFileDownloaded on path
26+
From: SnowflakeCopyIntoStage
27+
To: SnowflakeFileDownloaded
28+
Match:
29+
- On: p_alert_context.stage
30+
Schedule:
31+
RateMinutes: 1440
32+
TimeoutMinutes: 15
33+
LookbackWindowMinutes: 2160
34+
Tests:
35+
- Name: Data Exfiltration
36+
ExpectedResult: true
37+
RuleOutputs:
38+
- ID: SnowflakeTempStageCreated
39+
Matches:
40+
p_alert_context.stage:
41+
LOGS.PUBLIC.data_exfil:
42+
- "2006-01-02T15:04:05Z"
43+
- "2006-01-02T15:04:06Z"
44+
- ID: SnowflakeCopyIntoStage
45+
Matches:
46+
p_alert_context.stage:
47+
LOGS.PUBLIC.data_exfil:
48+
- "2006-01-02T15:04:05Z"
49+
- "2006-01-02T15:04:06Z"
50+
- ID: SnowflakeFileDownloaded
51+
Matches:
52+
p_alert_context.stage:
53+
LOGS.PUBLIC.data_exfil:
54+
- "2006-01-02T15:04:05Z"
55+
- "2006-01-02T15:04:06Z"
56+
- Name: Data Staged but not Downloaded
57+
ExpectedResult: false
58+
RuleOutputs:
59+
- ID: SnowflakeTempStageCreated
60+
Matches:
61+
p_alert_context.stage:
62+
LOGS.PUBLIC.data_exfil:
63+
- "2006-01-02T15:04:05Z"
64+
- "2006-01-02T15:04:06Z"
65+
- ID: SnowflakeCopyIntoStage
66+
Matches:
67+
p_alert_context.stage:
68+
LOGS.PUBLIC.data_exfil:
69+
- "2006-01-02T15:04:05Z"
70+
- "2006-01-02T15:04:06Z"

global_helpers/panther_base_helpers.py

-137
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from ipaddress import ip_address, ip_network
1010
from typing import Any, List, Optional, Sequence, Union
1111

12-
import panther_base_helpers_old
1312
from dateutil import parser
1413

1514
# # # # # # # # # # # # # #
@@ -353,139 +352,3 @@ def pantherflow_investigation(event, interval="30m"):
353352
query += "| sort p_event_time"
354353

355354
return query
356-
357-
358-
# panther_base_helpers.GSUITE_PARAMETER_VALUES is DEPRECATED!!!
359-
# Instead use panther_gsuite_helpers.GSUITE_PARAMETER_VALUES
360-
GSUITE_PARAMETER_VALUES = panther_base_helpers_old.GSUITE_PARAMETER_VALUES
361-
362-
363-
def gsuite_parameter_lookup(parameters, key):
364-
"""Global `gsuite_parameter_lookup` is DEPRECATED.
365-
Instead, use `from panther_gsuite_helpers import gsuite_parameter_lookup`."""
366-
return panther_base_helpers_old.gsuite_parameter_lookup(parameters, key)
367-
368-
369-
def gsuite_details_lookup(detail_type, detail_names, event):
370-
"""Global `gsuite_details_lookup` is DEPRECATED.
371-
Instead, use `from panther_gsuite_helpers import gsuite_details_lookup`."""
372-
return panther_base_helpers_old.gsuite_details_lookup(detail_type, detail_names, event)
373-
374-
375-
# panther_base_helpers.ZENDESK_CHANGE_DESCRIPTION is DEPRECATED!!!
376-
# Instead use panther_zendesk_helpers.ZENDESK_CHANGE_DESCRIPTION
377-
ZENDESK_CHANGE_DESCRIPTION = panther_base_helpers_old.ZENDESK_CHANGE_DESCRIPTION
378-
# panther_base_helpers.ZENDESK_APP_ROLE_ASSIGNED is DEPRECATED!!!
379-
# Instead use panther_zendesk_helpers.ZENDESK_APP_ROLE_ASSIGNED
380-
ZENDESK_APP_ROLE_ASSIGNED = panther_base_helpers_old.ZENDESK_APP_ROLE_ASSIGNED
381-
# panther_base_helpers.ZENDESK_ROLE_ASSIGNED is DEPRECATED!!!
382-
# Instead use panther_zendesk_helpers.ZENDESK_ROLE_ASSIGNED
383-
ZENDESK_ROLE_ASSIGNED = panther_base_helpers_old.ZENDESK_ROLE_ASSIGNED
384-
385-
386-
def zendesk_get_roles(event):
387-
"""Global `zendesk_get_roles` is DEPRECATED.
388-
Instead, use `from panther_zendesk_helpers import zendesk_get_roles`."""
389-
return panther_base_helpers_old.zendesk_get_roles(event)
390-
391-
392-
def box_parse_additional_details(event: dict):
393-
"""Global `box_parse_additional_details` is DEPRECATED.
394-
Instead, use `from panther_box_helpers import box_parse_additional_details`."""
395-
return panther_base_helpers_old.box_parse_additional_details(event)
396-
397-
398-
def okta_alert_context(event: dict):
399-
"""Global `okta_alert_context` is DEPRECATED.
400-
Instead, use `from panther_okta_helpers import okta_alert_context`."""
401-
return panther_base_helpers_old.okta_alert_context(event)
402-
403-
404-
def crowdstrike_detection_alert_context(event: dict):
405-
"""Global `crowdstrike_detection_alert_context` is DEPRECATED.
406-
Instead, use `from panther_crowdstrike_fdr_helpers import crowdstrike_detection_alert_context`.
407-
"""
408-
return panther_base_helpers_old.crowdstrike_detection_alert_context(event)
409-
410-
411-
def crowdstrike_process_alert_context(event: dict):
412-
"""Global `crowdstrike_process_alert_context` is DEPRECATED.
413-
Instead, use `from panther_crowdstrike_fdr_helpers import crowdstrike_process_alert_context`.
414-
"""
415-
return panther_base_helpers_old.crowdstrike_process_alert_context(event)
416-
417-
418-
def crowdstrike_network_detection_alert_context(event: dict):
419-
"""Global `crowdstrike_network_detection_alert_context` is DEPRECATED.
420-
Instead, use `from panther_crowdstrike_fdr_helpers
421-
import crowdstrike_network_detection_alert_context`.
422-
"""
423-
return panther_base_helpers_old.crowdstrike_network_detection_alert_context(event)
424-
425-
426-
def filter_crowdstrike_fdr_event_type(event, name: str) -> bool:
427-
"""Global `filter_crowdstrike_fdr_event_type` is DEPRECATED.
428-
Instead, use `from panther_crowdstrike_fdr_helpers import filter_crowdstrike_fdr_event_type`.
429-
"""
430-
return panther_base_helpers_old.filter_crowdstrike_fdr_event_type(event, name)
431-
432-
433-
def get_crowdstrike_field(event, field_name, default=None):
434-
"""Global `get_crowdstrike_field` is DEPRECATED.
435-
Instead, use `from panther_crowdstrike_fdr_helpers import get_crowdstrike_field`.
436-
"""
437-
return panther_base_helpers_old.get_crowdstrike_field(event, field_name, default)
438-
439-
440-
def slack_alert_context(event):
441-
"""Global `slack_alert_context` is DEPRECATED.
442-
Instead, use `from panther_slack_helpers import slack_alert_context`."""
443-
return panther_base_helpers_old.slack_alert_context(event)
444-
445-
446-
def github_alert_context(event):
447-
"""Global `github_alert_context` is DEPRECATED.
448-
Instead, use `from panther_github_helpers import github_alert_context`."""
449-
return panther_base_helpers_old.github_alert_context(event)
450-
451-
452-
def aws_strip_role_session_id(user_identity_arn):
453-
"""Global `aws_strip_role_session_id` is DEPRECATED.
454-
Instead, use `from panther_aws_helpers import aws_strip_role_session_id`."""
455-
return panther_base_helpers_old.aws_strip_role_session_id(user_identity_arn)
456-
457-
458-
def aws_rule_context(event: dict):
459-
"""Global `aws_rule_context` is DEPRECATED.
460-
Instead, use `from panther_aws_helpers import aws_rule_context`."""
461-
return panther_base_helpers_old.aws_rule_context(event)
462-
463-
464-
def aws_guardduty_context(event: dict):
465-
"""Global `aws_guardduty_context` is DEPRECATED.
466-
Instead, use `from panther_aws_helpers import aws_guardduty_context`."""
467-
return panther_base_helpers_old.aws_guardduty_context(event)
468-
469-
470-
def eks_panther_obj_ref(event):
471-
"""Global `eks_panther_obj_ref` is DEPRECATED.
472-
Instead, use `from panther_aws_helpers import eks_panther_obj_ref`."""
473-
return panther_base_helpers_old.eks_panther_obj_ref(event)
474-
475-
476-
def get_binding_deltas(event):
477-
"""Global `get_binding_deltas` is DEPRECATED.
478-
Instead, use `from panther_gcp_helpers import get_binding_deltas`."""
479-
return panther_base_helpers_old.get_binding_deltas(event)
480-
481-
482-
def msft_graph_alert_context(event):
483-
"""Global `msft_graph_alert_context` is DEPRECATED.
484-
Instead, use `from panther_msft_helpers import msft_graph_alert_context`."""
485-
return panther_base_helpers_old.msft_graph_alert_context(event)
486-
487-
488-
def m365_alert_context(event):
489-
"""Global `m365_alert_context` is DEPRECATED.
490-
Instead, use `from panther_msft_helpers import m365_alert_context`."""
491-
return panther_base_helpers_old.m365_alert_context(event)

0 commit comments

Comments
 (0)