# Writing and Editing Detections

## Overview

You can write your own Python detections in the Panther Console or locally, following the [CI/CD workflow](/~/changes/15ann7vKLltCCAGHtdQr/panther-developer-workflows/ci-cd.md). This page contains detection writing examples and best practices, available auxiliary functions, and guidance on how to configure a detection dynamically.

For instructions on how to write detections, see the following pages:

* [Instructions for writing Rules and Scheduled Rules](/~/changes/15ann7vKLltCCAGHtdQr/detections/writing-and-editing-detections/rules.md#how-to-write-rules)
* [Instructions for writing Policies](/~/changes/15ann7vKLltCCAGHtdQr/detections/writing-and-editing-detections/policies.md#how-to-write-a-policy)

{% hint style="info" %}
Before you write a new detection, see if there's a [Panther-managed detection](/~/changes/15ann7vKLltCCAGHtdQr/detections/panther-managed.md) that meets your needs (or *almost* meets your needs—Panther-managed rules can be tuned with [Rule Filters](/~/changes/15ann7vKLltCCAGHtdQr/detections/rule-filters.md)). 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.
{% endhint %}

## Detection examples and best practices

### Python best practices

Python Enhancement Proposals [publishes resources](https://peps.python.org/pep-0008/) on how to cleanly and effectively write and style your Python code. For example, you can use [autopep8](https://pypi.org/project/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](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html):

<table data-header-hidden><thead><tr><th width="204">Package</th><th width="174">Version</th><th width="186">Description</th><th>License</th></tr></thead><tbody><tr><td>Package</td><td>Version</td><td>Description</td><td>License</td></tr><tr><td><code>jsonpath-ng</code></td><td><code>1.5.2</code></td><td>JSONPath Implementation</td><td>Apache v2</td></tr><tr><td><code>policyuniverse</code></td><td><code>1.3.3.20210223</code></td><td>Parse AWS ARNs and Policies</td><td>Apache v2</td></tr><tr><td><code>requests</code></td><td><code>2.23.0</code></td><td>Easy HTTP Requests</td><td>Apache v2</td></tr></tbody></table>

### **Understanding the structure of a detection in Panther**

A detection's only required function is `def rule(event)`, shown below. Additional functions can make your alerts more dynamic—see [Configuring detection functions dynamically](#configuring-detection-functions-dynamically) for more information.

#### Basic rule template

```python
def rule(event): 
    if event.get("Something"): 
        return True 
    return False
```

`return True` triggers an alert, while `return False` does not trigger an alert.

For more templates, see the [panther\_analysis repo on GitHub](https://github.com/panther-labs/panther-analysis/tree/master/templates).

### Detection writing best practices

#### Writing tests for your detections

Before enabling new detections, it is [recommended to write tests ](/~/changes/15ann7vKLltCCAGHtdQr/detections/testing.md)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`.

```python
def rule(event):
    return event.get('field') == 'value'
```

In the example below, if the field exists, the value of the field will be returned. Otherwise, `False` will be returned:

```python
def rule(event):
    if event.get('field')
        return event.get('field')
    return False
```

{% hint style="warning" %}
**Bad practice example**\
The example below is bad practice because the code is explicit about the field name. If the field doesn't exist, Python will throw a KeyError:\
\
`def rule(event):`

`return event['field'] == 'value'`
{% endhint %}

Reference: [Safely Accessing Event Fields](https://docs.panther.com/writing-detections/rules#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](/~/changes/15ann7vKLltCCAGHtdQr/detections/globals.md), 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:

```json
{ 	
       "eventVersion": "1.05",
       "userIdentity": { 	
               "type": "Root", 	
               "principalId": "1111", 	
               "arn": "arn:aws:iam::123456789012:root", 	
               "accountId": "123456789012", 		
               "userName": "root" 
               }, 	
        ... 
 }
```

Here is how you could check that value safely with `deep_get`:

```python
from panther_base_helpers import deep_get

def rule(event):
    return deep_get(event, "userIdentity", "type") == "Root"
```

Reference: [AWS Console Root Login](https://github.com/panther-labs/panther-analysis/blob/cd220c87982011d4ad156c7daecd2857c358d154/rules/aws_cloudtrail_rules/aws_console_root_login.py)

#### **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`:

```python
def rule(event):
    return event.get("event_type") == "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:

```python
# returns True if 'status_code' equals 404
def rule(event):
    if event.get("status_code"):
        return event.get("status_code") == 404
    else:
        return False

# returns True if 'status_code' greater than 400
def rule(event):
    if event.get("status_code"):
        return event.get("status_code") > 404
    else:
        return False
```

Reference:

* [box\_access\_granted.py](https://github.com/panther-labs/panther-analysis/blob/cd220c87982011d4ad156c7daecd2857c358d154/rules/box_rules/box_access_granted.py)
* [Python Operators](https://www.w3schools.com/python/python_operators.asp)

#### **Using the Universal Data Model**

[Data Models](https://docs.panther.com/writing-detections/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](https://docs.panther.com/writing-detections/panther-analysis-tool#data-models).

`event.udm()` can only be used with log types that have an existing Data Model in your Panther environment.

Example:

```python
import panther_event_type_helpers as event_type

def rule(event):
    # filter events on unified data model field ‘event_type’
    return event.udm("event_type") == event_type.FAILED_LOGIN
```

References:

* [Data Models Guide](https://docs.panther.com/writing-detections/data-models)
* [Data Models](https://github.com/panther-labs/panther-analysis/tree/master/data_models)
* [Brute Force by IP](https://github.com/panther-labs/panther-analysis/blob/cd220c87982011d4ad156c7daecd2857c358d154/indexes/standard.md)

#### 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:

```python
from panther_base_helpers import deep_get

def rule(event):
    return (event.get("eventName") == "ConsoleLogin" and
            deep_get(event, "userIdentity", "type") == "Root" and
	    deep_get(event, "responseElements", "ConsoleLogin") == "Success")
```

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:

```python
# returns True if 'port_number' is 80 or 22
def rule(event):
    return event.get("port_number") == 80 or event.get("port_number") == 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.

```python
# Set - Recommended over tuples or lists for performance
ALLOW_IP = {'192.0.0.1', '192.0.0.2', '192.0.0.3'}

def rule(event):
    return event.get("ip_address") not in ALLOW_IP
```

In the example below, we use the Panther helper `pattern_match_list`:

```python
from panther_base_helpers import pattern_match_list

USER_CREATE_PATTERNS = [
    "chage",  # user password expiry
    "passwd",  # change passwords for users
    "user*",  # create, modify, and delete users
]


def rule(event):
    # Filter the events
    if event.get("event") != "session.command":
        return False
    # Check that the program matches our list above
    return pattern_match_list(event.get("program", ""), USER_CREATE_PATTERNS)
```

Reference: [Teleport Create User Accounts](https://github.com/panther-labs/panther-analysis/blob/cd220c87982011d4ad156c7daecd2857c358d154/rules/gravitational_teleport_rules/teleport_create_user_accounts.py)

#### 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.

```python
import re
from panther_base_helpers import deep_get

#The regex pattern is stored in a variable
# Note: This is better performance than putting it in the rule function, which is evaluated on each event
ADMIN_PATTERN = re.compile(r"[aA]dministrator")

def rule(event):
    # using the deep_get function we can pull out the nested value under the "privilegeGranted" field
    value_to_search = deep_get(event, "debugContext", "debugData", "privilegeGranted")
    # finally we use the regex object we created earlier to check against our value
    # if there is a match, "True" is returned 
    return (bool(ADMIN_PATTERN.search(value_to_search, default="")))
```

In the example below, we use the Panther helper `pattern_match`:

```python
from panther_base_helpers import pattern_match

def rule(event):
    return pattern_match(event.get("operation", ""), "REST.*.OBJECT")
```

References:

* [re.compile](https://docs.python.org/3/library/re.html#functions)
* [Pythex: simple RegEx editor and tester](https://pythex.org/)
* [AWS S3 Insecure Access](https://github.com/panther-labs/panther-analysis/blob/cd220c87982011d4ad156c7daecd2857c358d154/rules/aws_s3_rules/aws_s3_insecure_access.py)

## Panther Detection functions and features

### Detection alerting functions

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](/~/changes/15ann7vKLltCCAGHtdQr/detections/globals.md).

Applicable to both Rules and Policies, each function listed takes a single argument of `event` (Rules) or `resource` (Policies). Advanced users may define functions, variables, or classes outside of the functions defined below.

The only required function is `def rule(event)`, but other functions make your Alerts more dynamic.

| Detection Alerting Function Name | Description                                                                                                 | Return Value                                    | Default Return Value                                              |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| `title`                          | The generated alert title                                                                                   | `String`                                        | If not defined, the `Display Name, RuleID`, or `PolicyID` is used |
| `dedup`                          | The string to group related events with, limited to 1000 characters                                         | `String`                                        | If not defined, the `title`function output is used.               |
| `alert_context`                  | Additional context to pass to the alert destination(s)                                                      | `Dict[String: Any]`                             | An empty `Dict`                                                   |
| `severity`                       | The level of urgency of the alert                                                                           | `INFO, LOW, MEDIUM, HIGH, CRITICAL, or DEFAULT` | The severity as defined in the detection metadata                 |
| `description`                    | An explanation about why the rule exists                                                                    | `String`                                        | The description as defined in the detection metadata              |
| `reference`                      | A reference URL to an internal document or online resource about the rule                                   | `String`                                        | The reference as defined in the detection metadata                |
| `runbook`                        | A list of instructions to follow once the alert is generated                                                | `String`                                        | The runbook as defined in the detection metadata                  |
| `destinations`                   | The label or ID of the destinations to specifically send alerts to. An empty list will suppress all alerts. | `List[Destination Name/ID]`                     | The destinations as defined in the detection metadata             |

### Configuring detection functions dynamically

#### `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.

If the dedup function is not present, the title is used to group related events for deduplication purposes.

Example:

```python
def title(event):
    # use unified data model field in title
    log_type = event.get("p_log_type")
    title_str = (
        f"{log_type}: User [{event.udm('actor_user')}] has exceeded the failed logins threshold"
    )
    if log_type == "AWS.CloudTrail":
        title_str += f" in [{lookup_aws_account_name(event.get('recipientAccountId'))}]"
    return title_str
```

Reference: [Template Rule](https://github.com/panther-labs/panther-analysis/blob/master/templates/example_rule.py#L15)

#### `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](/~/changes/15ann7vKLltCCAGHtdQr/detections/writing-and-editing-detections/rules.md#deduplication).&#x20;

Example:

```python
def dedup(event):
	user_identity = event.get("userIdentity", {})

	if user_identity.get("type") == "AssumedRole":
		return helper_strip_role_session_id(user_identity.get("arn", ""))

	return user_identity.get("arn")
```

Reference: [AWS S3 Bucket Deleted Rule](https://github.com/panther-labs/panther-analysis/blob/4b6b79846fb4cb1596908fd31ce983c75a39baaa/aws_cloudtrail_rules/aws_s3_bucket_deleted.py)

#### `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:

```python
def severity(event):
    if event.get('eventType') == 'system.api_token.create':
        return "HIGH"
    return "INFO"
```

Reference: [Template Rule](https://github.com/panther-labs/panther-analysis/blob/master/templates/example_rule.py#L33)

Example using DEFAULT:

```python
def severity(event):
    if event.get('eventType') == 'system.api_token.create':
        return "HIGH"
    return "DEFAULT"
```

#### **`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.

```python
def destinations(event):
    if event.get("p_log_type") == "AWS.CloudTrail":
        return ["slack-security-alerts"] ### Name or UUID of destination
    # Do not send alert to an external destination
    return ["SKIP"]
```

Reference: [Template Rule](https://github.com/panther-labs/panther-analysis/blob/master/templates/example_rule.py#L59)

#### `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.

```python
def rule(event):
    return (
        event.get("actionName") == "UPDATE_SAML_SETTINGS"
        and event.get("actionResult") == "SUCCEEDED"
    )

def alert_context(event):
    return {
        "user": event.udm("actor_user"),
        "ip": event.udm("source_ip")
    }
```

#### `alert 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
def runbook(event):
	log_type = event.get("p_log_type")
	if log_type == "OnePassword.SignInAttempt":
		return: f"<https://link/to/resource>"
	elif log_type == "Okta.SystemLog":
		return: f"<https://link/to/resource/2>"
	else: 
		return: f"<https://default/link>"
```

## Troubleshooting Detections

Visit the Panther Knowledge Base to [view articles about detections](https://help.panther.com/Detections) that answer frequently asked questions and help you resolve common errors and issues.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.panther.com/~/changes/15ann7vKLltCCAGHtdQr/detections/writing-and-editing-detections.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
