Writing Python Detections
Construct Python detections in the Console or CLI workflow
Overview
You can write your own Python detections in the Panther Console or locally, following the CLI workflow. When writing Python detections, try to follow these best practices, and remember that certain alert fields can be set dynamically.
You can alternatively use the no-code detection builder in the Console to create rules, or write them locally in YAML. If you aren't sure whether to write detections locally in YAML or Python, see the Using Python vs. YAML section.
Before you write a new Python detection, see if there's a Panther-managed detection that meets your needs (or almost meets your needs—Panther-managed rules can be tuned with Inline Filters). Leveraging a Panther-managed detection not only saves you from the effort of writing one yourself, but also provides the ongoing benefit of continuous updates to core detection logic, as Panther releases new versions.
How to create detections in Python
How to create a rule in Python
You can write a Python rule in both the Panther Console and CLI workflow.
How to create a scheduled rule in Python
You can write a Python scheduled rule in both the Panther Console and CLI workflow.
How to create a policy in Python
To learn how to create a policy, see the How to write a policy instructions on Policies.
Python detection syntax
A local Python detection is made up of two files: a Python file and a YAML file. When a Python detection is created in the Panther Console, there is only a Python text editor (not a YAML one). The keys listed in the YAML column, below, are set in fields in the user interface.
The Python file can contain: | The YAML file can contain: |
---|---|
|
|
Basic Python rule structure
Only a rule()
function and the YAML keys shown below are required for a Python rule. Additional Python alert functions, however, can make your alerts more dynamic. Additional YAML keys are available, too—see Python rule specification reference.
rule.py | rule.yml |
---|---|
For more templates, see the panther-analysis repo on GitHub.
InlineFilters
InlineFilters
Learn more about using InlineFilters
in Python rules on Modifying Detections with Inline Filters.
Alert functions in Python detections
Panther's detection auxiliary functions are Python functions that control analysis logic, generated alert title, event grouping, routing of alerts, and metadata overrides. Rules are customizable and can import from standard Python libraries or global helpers.
Applicable to both rules and policies, each function below takes a single argument of event
(rules) or resource
(policies). Advanced users may define functions, variables, or classes outside of the functions defined below.
Each of the below alert functions are optional, but can add dynamic context to your alerts.
Detection alert function name | Description | Overrides | Return Value |
---|---|---|---|
The level of urgency of the alert | In YAML: In Console: Severity field |
| |
The generated alert title | In YAML: In Console: Name field > ID field |
| |
The string to group related events with, limited to 1000 characters | In Python/YAML: In Console: |
| |
Additional context to pass to the alert destination(s) | Does not override a YAML nor Console field |
| |
An explanation about why the rule exists | In YAML: In Console: Description field |
| |
A reference URL to an internal document or online resource about the rule | In YAML: In Console: Reference field |
| |
A list of instructions to follow once the alert is generated | In YAML: In Console: Runbook field |
| |
The label or ID of the destinations to specifically send alerts to. An empty list will suppress all alerts. | In YAML: In Console: Destination Overrides field |
|
severity
severity
In some scenarios, you may need to upgrade or downgrade the severity level of an alert. The severity levels of an alert can be mapped to INFO, LOW, MEDIUM, HIGH, CRITICAL, or DEFAULT. Return DEFAULT to fall back to the statically defined rule severity.
In all cases, the severity string returned is case insensitive, meaning you can return, for example, Critical
or default
, depending on your style preferences.
Example where a HIGH severity alert is returned if an API token is created - otherwise we create an INFO level alert:
Reference: Template Rule
Example using DEFAULT
:
title
title
The title function is optional, but it is recommended to include it to provide additional context. In the example below, the log type, relevant username, and a static string are returned to the destination. The function checks to see if the event is related the AWS.CloudTrail log type and return the AWS Account Name if that is true. Learn more about how an alert title is set on Rules and Scheduled Rules.
Example:
Reference: Template Rule
dedup
dedup
Deduplication is the process of grouping related events into a single alert to prevent receiving duplicate alerts. Events triggering the same detection that also share a deduplication string, within the deduplication period, are grouped together in a single alert. The dedup
function is one way to define a deduplication string. It is limited to 1000 characters. Learn more about deduplication on Rules and Scheduled Rules.
Example:
Reference: AWS S3 Bucket Deleted Rule
destinations
destinations
By default, Alerts are sent to specific destinations based on severity level or log type event. Each Detection has the ability to override their default destination and send the Alert to one or more specific destination(s). In some scenarios, a destination override is required, providing more advance criteria based on the logic of the Rule.
Example:
A rule used for multiple log types utilizes the destinations function to reroute the Alert to another destination if the log type is "AWS.CloudTrail". The Alert is suppressed to this destination using return ["SKIP"]
if the log type is not CloudTrail.
Reference: Template Rule
alert_context
alert_context
This function allows the detection to pass any event details as additional context, such as usernames, IP addresses, or success/failure, to the Alert destination(s).
Example:
The code below returns all event data in the alert context.
runbook
, reference
, and description
runbook
, reference
, and description
These functions can provide additional context around why an alert was triggered and how to resolve the related issue. Depending on what conditions are met, a string can be overridden and returned to the specified field in the alert.
The example below dynamically provides a link within the runbook
field in an alert.
Python best practices
Python Enhancement Proposals publishes resources on how to cleanly and effectively write and style your Python code. For example, you can use autopep8 to automatically ensure that your written detections all follow a consistent style.
Available Python libraries
The following Python libraries are available to be used in Panther in addition to boto3
, provided by AWS Lambda:
Package | Version | Description | License |
|
| JSONPath Implementation | Apache v2 |
|
| Parse AWS ARNs and Policies | Apache v2 |
|
| Easy HTTP Requests | Apache v2 |
Python detection writing best practices
Writing tests for your detections
Before enabling new detections, it is recommended to write tests that define scenarios where alerts should or should not be generated. Best practice dictates at least one positive and one negative to ensure the most reliability.
Casing for event fields
Lookups for event fields are not case sensitive. event.get("Event_Type")
or event.get("event_type")
will return the same result.
Understanding top level fields and nested fields
Top-level fields represent the parent fields in a nested data structure. For example, a record may contain a field called user
under which there are other fields such as ip_address
. In this case, user
is the top-level field, and ip_address
is a nested field underneath it.
Nesting can occur many layers deep, and so it is valuable to understand the schema structure and know how to access a given field for a detection.
Accessing top-level fields safely
Basic Rules match a field’s value in the event, and a best practice to avoid errors is to leverage Python’s built-in get()
function.
The example below is a best practice because it leverages a get()
function. get()
will look for a field, and if the field doesn't exist, it will return None
instead of an error, which will result in the detection returning False
.
In the example below, if the field exists, the value of the field will be returned. Otherwise, False
will be returned:
Bad practice example
The rule definition below is bad practice because the code is explicit about the field name. If the field doesn't exist, Python will throw a KeyError
:
Reference: Safely Accessing Event Fields
Using Global Helper functions
Once many detections are written, a set of patterns and repeated code will begin to emerge. This is a great use case for Global Helper functions, which provide a centralized location for this logic to exist across all detections. For example, see the deep_get()
function referenced in the next section.
Accessing nested fields safely
If the field is nested deep within the event, use a Panther-supplied function called deep_get()
to safely access the fields value. deep_get()
must be imported by the panther_base_helpers
library.
deep_get()
takes two or more arguments:
The event object itself (required)
The top-level field name (required)
Any nested fields, in order (as many nested fields as needed)
Example:
AWS CloudTrail logs nest the type of user accessing the console underneath userIdentity
.
JSON CloudTrail root activity:
Here is how you could check that value safely with deep_get
:
Reference: AWS Console Root Login
Checking fields for specific values
You may want to know when a specific event has occurred. If it did occur, then the detection should trigger an alert. Since Panther stores everything as normalized JSON, you can check the value of a field against the criteria you specify.
For example, to detect the action of granting Box technical support access to your Box account, the Python below would be used to match events where the event_type
equals ACCESS_GRANTED
:
If the field is event_type
and the value is equal to ACCESS_GRANTED
then the rule function will return true
and an Alert will be created.
Checking fields for Integer values
You may need to compare the value of a field against integers. This allows you to use any of Python’s built-in comparisons against your events.
For example, you can create an alert based on HTTP response status codes:
Reference:
Using the Universal Data Model
Data Models provide a way to configure a set of unified fields across all log types. By default, Panther comes with built-in Data Models for several log types. Custom Data Models can be added in the Panther Console or via the Panther Analysis Tool.
event.udm()
can only be used with log types that have an existing Data Model in your Panther environment.
Example:
References:
Using multiple conditions
The and
keyword is a logical operator and is used to combine conditional statements. It is often required to match multiple fields in an event using the and
keyword. When using and
, all statements must be true:
"string_a" == "this"
and
"string_b" == "that"
Example:
To track down successful root user access to the AWS console you need to look at several fields:
The or
keyword is a logical operator and is used to combine conditional statements. When using or
, either of the statements may be true:
"string_a" == "this"
or
"string_b" == "that"
Example:
This example detects if the field contains either Port 80 or Port 22:
Searching values in lists
Comparing and matching events against a list of IP addresses, domains, users etc. is very quick and easy in Python. This is often used in conjunction with choosing not to alert on an event if the field being checked also exists in the list. This helps with reducing false positives for known behavior in your environment.
Example: If you have a list of IP addresses that you would like to add to your allow list, but you want to know if an IP address comes through outside of that list, we recommend using a Python set. Sets are similar to Python lists and tuples, but are more memory efficient.
In the example below, we use the Panther helper pattern_match_list
:
Reference: Teleport Create User Accounts
Matching events with regex
If you want to match against events using regular expressions - to match subdomains, file paths, or a prefix/suffix of a general string - you can use regex. In Python, regex can be used by importing the re
library and looking for a matching value.
In the example below, the regex pattern will match Administrator or administrator against the nested value of the privilegeGranted field.
In the example below, we use the Panther helper pattern_match
:
References:
Python rule specification reference
Required fields are in bold.
Field Name | Description | Expected Value |
| Indicates whether this analysis is a rule, scheduled_rule, policy, or global | Rules: |
| Whether this rule is enabled | Boolean |
| The path (with file extension) to the python rule body | String |
| The unique identifier of the rule | String
Cannot include |
| The list of logs to apply this rule to | List of strings |
| What severity this rule is | One of the following strings: |
| The list of Scheduled Query names to apply this rule to | List of strings |
| A brief description of the rule | String |
| The time period (in minutes) during which similar events of an alert will be grouped together |
|
| A friendly name to show in the UI and alerts. The | String |
| Static destination overrides. These will be used to determine how alerts from this rule are routed, taking priority over default routing based on severity. | List of strings |
| The reason this rule exists, often a link to documentation | String |
| A mapping of framework or report names to values this rule covers for that framework | Map of strings to list of strings |
| The actions to be carried out if this rule returns an alert, often a link to documentation | String |
| A list of fields that alerts should summarize. | List of strings |
| How many events need to trigger this rule before an alert will be sent. | Integer |
| Tags used to categorize this rule | List of strings |
| Unit tests for this rule. | List of maps |
Python Policy Specification Reference
Required fields are in bold.
A complete list of policy specification fields:
Field Name | Description | Expected Value |
| Indicates whether this specification is defining a policy or a rule |
|
| Whether this policy is enabled | Boolean |
| The path (with file extension) to the python policy body | String |
| The unique identifier of the policy | String
Cannot include |
| What resource types this policy will apply to | List of strings |
| What severity this policy is | One of the following strings: |
| A brief description of the policy | String |
| What name to display in the UI and alerts. The | String |
| The reason this policy exists, often a link to documentation | String |
| A mapping of framework or report names to values this policy covers for that framework | Map of strings to list of strings |
| The actions to be carried out if this policy fails, often a link to documentation | String |
| Patterns to ignore, e.g., | List of strings |
| Tags used to categorize this policy | List of strings |
| Unit tests for this policy. | List of maps |
Troubleshooting Detections
Visit the Panther Knowledge Base to view articles about detections that answer frequently asked questions and help you resolve common errors and issues.
Last updated