V2 Detections

Configure detections fully in Python

Overview

V2 detections are in closed beta starting with Panther version 1.108. Please share any bug reports and feature requests with your Panther support team.

V2 detections are Panther’s evolved approach to detections-as-code. In this framework, detections are defined fully in Python, enabling component reusability and simple rule overrides. The foundation of v2 detections is the Panther-managed pypanther Python library.

Key features

  • An entirely Python-native experience for importing, writing, modifying, testing, and uploading rules—eliminating the need to manage a fork or clone of panther-analysis.

    # Import the Panther-managed BoxNewLogin rule
    from pypanther.rules.box_rules.box_new_login import BoxNewLogin
  • The ability to apply custom configurations to Panther-managed rules through overrides, filters, and inheritance.

    from pypanther import Severity
    from pypanther.wrap import exclude
    
    # Set multiple attribute overrides
    BoxNewLogin.override(
        default_severity=Severity.MEDIUM,
        tags=['Initial Access'],
        default_runbook="Ask user in Slack if this login was actually from them.",
    )
    
    # Add a simple filter to exclude all logins from Alice
    exclude(lambda e: e.deep_get('created_by', 'name') != 'Alice')(BoxNewLogin)
  • The ability to selectively choose the set of rules you want to include in your Panther deployment package.

    from pypanther import register
    
    # Register a single rule to test and upload
    register(BoxNewLogin)

Benefits

V2 detections have the following benefits:

  • No upstream merge conflicts: In the Python (v1) model, merge conflicts can arise when syncing your customized fork of panther-analysis with the upstream repository. In this v2 model, Panther-managed rules exist separately from your rule configurations, eliminating the possibility of merge conflicts.

  • Full flexibility and composability: This feature offers complete flexibility in rule creation, enabling full modularity, composability, the ability to override any rule attribute, and full Python inheritance—all providing a customizable and user-centric experience.

  • First-class developer experience: Backed by a portable, open-source Python library called pypanther, this framework provides a superior local development experience by hooking into native applications and developer workflows. This library can also be loaded into any Python environment, such as Jupyter Notebooks.

V2 vs. v1 detections

Panther has developed a tool that translates detections from v1 to v2.

V1 detections are rules created in Python as described in Writing Python Detections. V2 detections differ from v1 in the following areas:

  • File structure: A rule in the v1 framework requires two files: a Python file to define rule logic and dynamic alerting functions and a YAML file to set metadata. A v2 rule is written entirely in Python.

    • This singular rule definition in a Python class—which contains all functions, properties, and helpers—enables overrides and composability.

  • Process for retrieving Panther-managed detections: In v1 detections, you must periodically sync your copy of panther-analysis with upstream changes. With v2 detections, however, no Git syncing is required—the latest Panther-managed content is always available in the pypanther Python library.

  • Packs: Panther-managed v1 detections are bundled in Detection Packs. In v2 detections, you can choose which detections you want to include in your Panther instance using get_panther_rules() (or direct imports) with register().

The same detection, Box.New.Login is defined below in both versions:

v1 Box New Login rulev2 Box New Login rule

# box_new_login.py

from panther_base_helpers import deep_get

def rule(event):
    return event.get("event_type") == "ADD_LOGIN_ACTIVITY_DEVICE"

def title(event):
    return (
        f"User [{deep_get(event, 'created_by', 'name', default='<UNKNOWN_USER>')}] "
        f"logged in from a new device."
    )
# box_new_login.yml

AnalysisType: rule
Filename: box_new_login.py
RuleID: "Box.New.Login"
DisplayName: "Box New Login"
Enabled: true
LogTypes:
  - Box.Event
Tags:
  - Box
  - Initial Access:Valid Accounts
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
Severity: Info
Description: A user logged in from a new device.
Reference: <https://support.box.com/hc/en-us/articles/360043691914-Controlling-Devices-Used-to-Access-Box>
Runbook: Investigate whether this is a valid user login.
SummaryAttributes:
  - ip_address
Tests:
	- ...

# box_new_login.py v2 rule

from pypanther import Rule, Severity, LogType

class BoxNewLogin(Rule):
    id = "Box.New.Login"
    display_name = "Box New Login"
    log_types = [LogType.BOX_EVENT]
    tags = ["Box", "Initial Access:Valid Accounts"]
    reports = {"MITRE ATT&CK": ["TA0001:T1078"]}
    default_severity = Severity.INFO
    default_description = "A user logged in from a new device.\\n"
    default_reference = "<https://support.box.com/hc/en-us/articles/360043691914-Controlling-Devices-Used-to-Access-Box>"
    default_runbook = "Investigate whether this is a valid user login.\\n"
    summary_attributes = ["ip_address"]
    tests = box_new_login_tests # Not shown in the example

    def rule(self, event):
        return event.get("event_type") == "ADD_LOGIN_ACTIVITY_DEVICE"

    def title(self, event):
        return f"User [{event.deep_get('created_by', 'name', default='<UNKNOWN_USER>')}] logged in from a new device."

Limitations of v2 detections

V2 detections are currently designed for real-time rules developed in the CLI workflow.

While v2 detections are evolving rapidly, they currently have the following limitations:

Creating v2 detections

Before writing v2 detections, you’ll need to set up your environment. See Getting started using v2 detections below.

When working with v2 detections:

  • A main.py file controls your entire detection configuration, outlining which rules to register, override configurations, and custom rule definitions. You can either:

    • (Recommended) Define your detections in various other files/folders, then import them into main.py

    • Define your detections in main.py

  • A v2 rule is defined in a single Python file. Within it, you can import Panther-managed (or your own custom) v2 rules and specify overrides. A single Python file can define multiple detections.

  • All v2 rules subclass the pypanther Rule class or a parent class of type Rule.

  • Rules must be registered to be tested and uploaded to your Panther instance.

  • All event object functions currently available in v1 detections are available in v2 detections. These include: get(), deep_get(), deep_walk(), and udm().

  • All alert functions available in Python (v1) detections are available in v2 detections, such as title() and severity(). See Rule auxiliary/alerting function reference.

  • Use the pypanther type-ahead hints in your IDE, like searching for available rules or viewing properties of classes.

Writing a custom v2 detection from scratch

Custom v2 rules are defined in a Python class that subclasses the pypanther Rule class. In this class, you must:

  • Define a rule() function

  • Define certain attributes, such as log_types

    • (Optional) Define additional attributes, such as threshold or dedup_period_minutes

The id attribute is only required for a rule if you plan to register it.

See the Rule property reference section below for a full list of required and optional fields.

from pypanther import Rule, Severity, LogType

# Custom rule for a Panther-supported log type
class MyCloudTrailRule(Rule):
    id = "MyCloudTrailRule"
    tests = True
    log_types = [LogType.AWS_CLOUDTRAIL]
    default_severity = Severity.MEDIUM
    threshold = 50
    dedup_period_minutes = 1

    def rule(self, event) -> bool:
        return (
	    event.get("eventType") == "AssumeRole" and 
	        400 <= int(event.get("errorCode", 0)) <= 413
	)

Importing Panther-managed rules

Panther-managed rules can be imported directly or using the get_panther_rules() function.

Panther-managed rules currently all have a -prototype suffix (e.g., AWS.Root.Activity-prototype). This is temporary, and will be removed in the future.

You may want to import Panther-managed rules (into main.py or another file) to either register them individually as-is, set overrides on them, or subclass them. You can import Panther-managed rules directly (from the pypanther.rules module) or by using the get_panther_rules() function.

To import a Panther-managed rule directly using the rules module, you would use a statement like:

from pypanther.rules.github_rules.github_advanced_security_change import GitHubAdvancedSecurityChange

The get_panther_rules() function can filter on any Rule class attribute, such as default_severity, log_types, or tag. When filtering, keys use AND logic, and values use OR logic.

Get all Panther-managed rules using get_panther_rules():

from pypanther import get_panther_rules

all_rules = get_panther_rules()

Get Panther-managed rules with certain severities using get_panther_rules():

from pypanther import get_panther_rules, Severity

important_rules = get_panther_rules(
    default_severity=[
        Severity.HIGH,
        Severity.CRITICAL,
    ]
)

Get Panther-managed rules for certain log types using get_panther_rules():

from pypanther import get_panther_rules, LogType

cloudtrail_okta_rules = get_panther_rules(
    log_types=[
        LogType.AWS_CLOUDTRAIL,
        LogType.OKTA_SYSTEM_LOG
    ]
)

Get Panther-managed rules that meet multiple criteria using get_panther_rules():

from pypanther import get_panther_rules, Severity

rules_i_care_about = get_panther_rules(
    enabled=True,
    default_severity=[Severity.CRITICAL, Severity.HIGH],
    tags=["Configuration Required"],
)

See Using list comprehension for an example of how to use get_panther_rules() with advanced Python.

Once you’ve imported a Panther-managed rule, you can modify it using inheritance or overrides.

Creating include or exclude filters

V2 detection filters let you exclude certain events from being evaluated by a rule. Filters are designed to be applied on top of existing rule logic (likely for Panther-managed v2 detections you are importing).

Each filter defines logic that is run before the rule() function, and the outcome of the filter determines whether or not the event should go on to be evaluated by the rule.

Common use cases for filters include:

  • To target only certain environments, like prod

  • To exclude events that are known false positives, due to a misconfiguration or other non-malicious scenario

There are two types of filters:

  • include filters: If the filter returns True for an event, the event is evaluated by rule()

  • exclude filters: If the filter returns True for an event, the event is dismissed (i.e., not evaluated by rule())

include and exclude, which can be imported from the pypanther.wrap module, can run as standalone functions or as rule class decorators.

Examples as standalone functions:

from pypanther.wrap import include
from pypanther.rules.github_rules.github_advanced_security_change import GitHubAdvancedSecurityChange

# Include only repos that are production
prod_repos = ["prod_repo1", "prod_repo2"]
include(lambda e: e.get("repo") in prod_repos)(GitHubAdvancedSecurityChange)
from pypanther.wrap import exclude
from pypanther.rules.box_rules.box_policy_violation import BoxContentWorkflowPolicyViolation

# Exclude accounts that are for development
dev_repos = ["dev_repo1", "dev_repo2"]
exclude(lambda e: e.get("repo") in dev_repos)(GitHubAdvancedSecurityChange)

Example as rule class decorators:

from pypanther.wrap import include, exclude
from pypanther.rules.github_rules.github_advanced_security_change import GitHubAdvancedSecurityChange

# In a real-world scenario, likely only one of the below would be necessary
@include(lambda e: e.get("repo") in prod_repos)
@exclude(lambda e: e.get("repo") in dev_repos)
class GitHubAdvancedSecurityChangeOverride(GitHubAdvancedSecurityChange):
    id = "GitHubAdvancedSecurityChangeOverride"
    ...

Filters can also be reused with a for loop to decorate multiple rules:

from pypanther.wrap import include

rules = [
    # Panther-managed and custom rules
    ...
]

def prod_filter(event):
    return event.get("repo") in prod_repos
		
for rule in rules:
    include(prod_filter)(rule)

Applying overrides on existing rules

If your objective is to modify a rule's logic, it's recommended to use include/exclude filters instead of overriding the rule() function itself. Learn more in Use filters instead of overriding rule().

Overriding single attributes

You can override a rule’s attributes with a one-line statement:

from pypanther import Severity
from pypanther.rules.aws_cloudtrail_rules.aws_cloudtrail_created import AWSCloudTrailCreated

# Set rule severity to High
AWSCloudTrailCreated.Severity = Severity.HIGH

Overriding multiple attributes with the override function

It’s also possible to configure multi-line overrides with the override() function:

from pypanther import Severity
from pypanther.rules.box_rules.box_policy_violation import BoxContentWorkflowPolicyViolation

BoxContentWorkflowPolicyViolation.override(
    default_severity=Severity.HIGH,
    default_runbook="Check if other internal users hit this same violation"
)

Applying overrides on multiple v2 rules

To apply overrides on multiple rules at once, iterate over the collection using a for loop.

This could be useful, for example, when updating a certain attribute for all rules associated to a certain LogType.

from pypanther import get_panther_rules, Severity, LogType

rules = get_panther_rules(
    enabled=True,
    default_severity=[Severity.CRITICAL, Severity.HIGH],
    tags=["Configuration Required"],
)

# Define the destination [override](<https://docs.panther.com/detections/rules/python#destinations>)
def aws_cloudtrail_destinations(self, event):
    if event.get('recipientAccountId') == 112233445566:
        # Name or UUID of a configured Slack Destination
        return ['AWS Notifications']
    # Suppress the alert, doesn't deliver
    return ['SKIP']

# Appending to the Panther-managed rules' destinations and tags
for rule in rules:
    rule.default_destinations.append("Slack #security")
    rule.tags.append("Production")
		
    # Updating the destinations for CloudTrail rules
    if LogType.AWS_CLOUDTRAIL in rule.log_types:
        rule.destinations = aws_cloudtrail_destinations

Ensuring necessary fields are set on configuration-required rules

Panther-managed rules that require some customer configuration before they are uploaded into a Panther environment may include a validate_config() function, which defines one or more conditions that must be met for the rule to pass the test command (and function properly).

Most commonly, validate_config() verifies that some class variable, such as an allowlist or denylist, has been assigned a value. If the requirements included in validate_config() are not met, an exception will be raised when the pypanther test command is run (if the rule is registered).

Example:

class ValidateMyRule(Rule):
    id = "Validate.MyRule"
    log_types = [LogType.PANTHER_AUDIT]
    default_severity = Severity.HIGH

    allowed_domains: list[str] = []

    def rule(self, event):
        return event.get("domain") not in self.allowed_domains

    @classmethod 
    def validate_config(cls):
        assert (
            len(cls.allowed_domains) > 0
        ), "The allowed_domains field on your rule must be populated"

In this example, if allowed_domains is not assigned a non-empty list, an assertion error will be thrown during pypanther test.

To set this value, you can use a statement like:

ValidateMyRule.allowed_domains = ["example.com"]

Creating v2 rules with inheritance

You can use inheritance to create rules that are subclasses of other rules (that are Panther-managed or custom).

It’s recommended to use inheritance when you’re creating a collection of rules that all share a number of characteristics—for example, rule() function logic, property values, class variables, or helper functions. In this case, it’s useful to create a base rule that all related rules inherit from.

For example, it may be useful to create a base rule for each LogType, from which all rules for that LogType are extended.

Inheritance is commonly used with filters—i.e., subclassed rules can define additional criteria an event must meet in order to be processed by the rule.

If you don’t plan to register a base rule, it’s not required to provide it an id property.

Example:

# Custom rule for a custom log type — the parent rule
class HostIDSBaseRule(Rule):
    # id not necessary since we're not uploading this parent rule
    log_types = ['Custom.HostIDS']
    default_severity = Severity.HIGH
    threshold = 1
    dedup_period_minutes = 6 * 60 # 6 hours

    def rule(self, event) -> bool:
        return event.get('event_name') == 'confirmed_compromise'
    
    def host_user_lookup(self, hostname):
	return 'groot'
		
    def title(self, event): -> str:
	return f"Confirmed compromise from hostname {event.get('hostname')}"
		
    def alert_context(self, event):
	user = self.host_user_lookup(event.get('hostName') 
	return {
	    'hostname': event['hostName'],
	    'time': event['p_event_time'],
	    'user': user,
	}
# Inherited rule #1

# Filter on event_type (in addition to base rule function)
@include(lambda e: e.get('event_type') == 'c2')
class IDSCommandAndControl(HostIDSBaseRule):
    id = 'IDSCommandAndControl'
    threshold = 19
    
    def title(self, event):
	return f"Confirmed c2 on host {event.get('hostname')}"
	
    # From parent rule, inherits rule(), alert_context(), LogTypes, Severity, and DedupPeriodMinutes
    # From PantherRule, inherits other fields (like Enabled)
# Inherited rule #2

# Filter on event_type (in addition to base rule function)
@include(lambda e: e.get('event_type') == 'malware')
class IDSCommandAndControl(HostIDSBaseRule):
    id = 'HostIDSMalware'
    threshold = 2
    default_severity = Severity.CRITICAL

    def title(self, event):
	return f"Confirmed malware on host {event.get('hostname')}"
    
    # From parent rule, inherits rule(), alert_context(), LogTypes, and DedupPeriodMinutes
    # From PantherRule, inherits other fields (like Enabled)

Using advanced Python in v2 rule creation

Because v2 rules are fully defined in Python, you can use its full expressiveness when customizing your detections.

Calling super()

For more advanced use cases, you can supplement the logic in functions defined by the parent rule.

# Creating an inherited rule
class MyRule(AnExistingRule):
    def alert_context(self, event):
	# Preserve the parent rule's alert context and extend with a new field
	context = super().alert_context(event)
	context["new field"] = "new_value"
	return context
				
    def severity(self, event):
	# Conditionally increment the parent rule's severity
	if event.get("env") == "prod":
	    return Severity.CRITICAL
	return super().severity(event)

Using list comprehension

You can use Python’s list comprehension functionality to create a new list based on an existing list with condensed syntax. This may be particularly useful when you want to filter a list of detections fetched using get_panther_rules().

# Collection of rules that have more than one LogType
rules = [rule for rule in get_panther_rules() if len(rule.log_types) > 1]
					
# Collection of rules that do not require configuration
rules = [rule for rule in get_panther_rules() if "Configuration Required" not in rule.tags]

Registering

Registering a v2 rule means including it in your Panther deployment package.

To register a rule, pass it in to register() in your main.py file. When you run the pypanther upload or test commands, only rules passed in to register() will be uploaded to your Panther instance or tested.

If a previously uploaded rule is not passed in to register() on a subsequent invocation of upload, it will be deleted in your Panther instance.

Registering a single rule more than once (perhaps because it’s included in multiple collections, which are all passed into register()) will not result in an error.

It’s not required to register rules used as base rules with inheritance so long as you do not want the base rules themselves uploaded into your Panther instance.

# Sample main.py file

from pypanther import register, get_panther_rules

# Register a single rule (that was defined as a class called "BoxNewLogin")
register(BoxNewLogin)

# Register all Panther-managed rules
register(get_panther_rules())

Importing instances of Rule

You can use the get_rules() function to easily fetch rules you might want to register in main.py. get_rules() takes in an imported package (folder) or module (file) name, and returns all rules from that package or module that inherit the pypanther Rule class.

Each folder containing rules imported with get_rules() must contain an __init__.py file. Learn more about recommended repository structure in the V2 Detections Style Guide.

Example using get_rules():

# main.py
import custom_rules

all_my_rules = get_rules(custom_rules)

register(all_my_rules)

Registering vs. enabling a rule

All rules have an enabled property, which is different from being registered. See the table below for all possible outcomes:

enabled = Trueenabled = False

Registered

Uploaded to Panther and enabled. Unit tests are run during testing.

Uploaded to Panther and disabled. Unit tests are run during testing.

Not registered

Not uploaded to Panther. If uploaded previously, deleted. Unit tests are not run during testing.

Not uploaded to Panther. If uploaded previously, deleted. Unit tests are not run during testing.

Testing v2 detections

Defining tests

Tests for v2 detections are defined by creating instances of the pypanther RuleTest class.

Each instance of RuleTest must set a name, expected_result, and log. Each test can also optionally define:

  • Additional fields (prepended with expected_) that verify the output of alert functions. For example, expected_severity and expected_title.

  • A mocks field, which takes a list of RuleMocks.

See a full list of available fields below, in RuleTest property reference.

my_rule_tests: List[RuleTest] = [
    RuleTest(
	name="RuleShouldReturnTrue"
	expected_result=True,
	log={
	    "name":"value"
	}
    ),
    RuleTest(
	name="RuleShouldReturnFalse"
	expected_result=False,
	log={
	    "name":"value"
	}
    )
]

Tests are associated with rules by assigning them to a rule’s tests field. Tests can be defined directly within a rule, or separately set to a variable (that is either local or imported).

Example

Note that this rule's tests (defined above the rule class) use mocks, expected_title, expected_dedup, and expected_alert_context.

from pypanther import LogType, Rule, RuleMock, RuleTest, Severity, panther_managed
from pypanther.helpers.panther_base_helpers import deep_get
from pypanther.helpers.panther_default import lookup_aws_account_name
from pypanther.helpers.panther_oss_helpers import geoinfo_from_ip_formatted

aws_console_root_login_tests: list[RuleTest] = [
    RuleTest(
        name="Successful Root Login",
        expected_result=True,
        expected_title="AWS root login detected from [111.111.111.111] (111.111.111.111 in SF, California in USA) in account [sample-account]",
        expected_dedup="123456789012-ConsoleLogin-2019-01-01T00:00:00Z",
        expected_alert_context={
            "sourceIPAddress": "111.111.111.111",
            "userIdentityAccountId": "123456789012",
            "userIdentityArn": "arn:aws:iam::123456789012:root",
            "eventTime": "2019-01-01T00:00:00Z",
            "mfaUsed": "No",
        },
        mocks=[
            RuleMock(object_name="geoinfo_from_ip_formatted", return_value="111.111.111.111 in SF, California in USA")
        ],
        log={
            "eventVersion": "1.05",
            "userIdentity": {
                "type": "Root",
                "principalId": "1111",
                "arn": "arn:aws:iam::123456789012:root",
                "accountId": "123456789012",
                "userName": "root",
            },
            "eventTime": "2019-01-01T00:00:00Z",
            "eventSource": "signin.amazonaws.com",
            "eventName": "ConsoleLogin",
            "awsRegion": "us-east-1",
            "sourceIPAddress": "111.111.111.111",
            "userAgent": "Mozilla",
            "requestParameters": None,
            "responseElements": {"ConsoleLogin": "Success"},
            "additionalEventData": {
                "LoginTo": "https://console.aws.amazon.com/console/",
                "MobileVersion": "No",
                "MFAUsed": "No",
            },
            "eventID": "1",
            "eventType": "AwsConsoleSignIn",
            "recipientAccountId": "123456789012",
        },
    ),
    RuleTest(
        name="Non-Login Event",
        expected_result=False,
        expected_title="AWS root login detected from [111.111.111.111] (111.111.111.111 in SF, California in USA) in account [sample-account]",
        expected_dedup="123456789012-DescribeTable-2019-01-01T00:00:00Z",
        expected_alert_context={
            "sourceIPAddress": "111.111.111.111",
            "userIdentityAccountId": "123456789012",
            "userIdentityArn": "arn:aws:sts::123456789012:user/tester",
            "eventTime": "2019-01-01T00:00:00Z",
            "mfaUsed": None,
        },
        mocks=[
            RuleMock(object_name="geoinfo_from_ip_formatted", return_value="111.111.111.111 in SF, California in USA")
        ],
        log={
            "eventVersion": "1.06",
            "userIdentity": {
                "type": "AssumedRole",
                "principalId": "1111:tester",
                "arn": "arn:aws:sts::123456789012:user/tester",
                "accountId": "123456789012",
                "accessKeyId": "1",
                "sessionContext": {
                    "sessionIssuer": {
                        "type": "Role",
                        "principalId": "1111",
                        "arn": "arn:aws:iam::123456789012:user/tester",
                        "accountId": "123456789012",
                        "userName": "tester",
                    },
                    "attributes": {"creationDate": "2019-01-01T00:00:00Z", "mfaAuthenticated": "true"},
                },
            },
            "eventTime": "2019-01-01T00:00:00Z",
            "eventSource": "dynamodb.amazonaws.com",
            "eventName": "DescribeTable",
            "awsRegion": "us-west-2",
            "sourceIPAddress": "111.111.111.111",
            "userAgent": "console.amazonaws.com",
            "requestParameters": {"tableName": "table"},
            "responseElements": None,
            "requestID": "1",
            "eventID": "1",
            "readOnly": True,
            "resources": [{"accountId": "123456789012", "type": "AWS::DynamoDB::Table", "ARN": "arn::::table/table"}],
            "eventType": "AwsApiCall",
            "apiVersion": "2012-08-10",
            "managementEvent": True,
            "recipientAccountId": "123456789012",
        },
    ),
]


@panther_managed
class AWSConsoleRootLogin(Rule):
    id = "AWS.Console.RootLogin-prototype"
    display_name = "Root Console Login"
    dedup_period_minutes = 15
    log_types = [LogType.AWS_CLOUDTRAIL]
    tags = [
        "AWS",
        "Identity & Access Management",
        "Authentication",
        "DemoThreatHunting",
        "Privilege Escalation:Valid Accounts",
    ]
    reports = {"CIS": ["3.6"], "MITRE ATT&CK": ["TA0004:T1078"]}
    default_severity = Severity.HIGH
    default_description = "The root account has been logged into."
    default_runbook = "Investigate the usage of the root account. If this root activity was not authorized, immediately change the root credentials and investigate what actions the root account took.\n"
    default_reference = "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html"
    summary_attributes = ["userAgent", "sourceIpAddress", "recipientAccountId", "p_any_aws_arns"]
    tests = aws_console_root_login_tests

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

    def title(self, event):
        ip_address = event.get("sourceIPAddress")
        return f"AWS root login detected from [{ip_address}] ({geoinfo_from_ip_formatted(ip_address)}) in account [{lookup_aws_account_name(event.get('recipientAccountId'))}]"

    def dedup(self, event):
        # Each Root login should generate a unique alert
        return "-".join([event.get("recipientAccountId"), event.get("eventName"), event.get("eventTime")])

    def alert_context(self, event):
        return {
            "sourceIPAddress": event.get("sourceIPAddress"),
            "userIdentityAccountId": deep_get(event, "userIdentity", "accountId"),
            "userIdentityArn": deep_get(event, "userIdentity", "arn"),
            "eventTime": event.get("eventTime"),
            "mfaUsed": deep_get(event, "additionalEventData", "MFAUsed"),
        }

Running tests

To run all tests defined for your v2 detections, run:

$ pypanther test

To run only a subset of tests, filter the detections for which tests are run by using a filter flag with test, such as --id or --log-types. See a full list of filter flags by running pypanther test --help.

The test command:

  • Only tests those detections passed into register()

  • Skips tests for Panther-managed rules

If any rules fail a test, the error will print next to the rule name that was tested:

MyRuleNameI:
   FAIL: my test name
     - Expected rule() to return 'True', but got 'False'

Testing custom helper functions and data fixtures

The pypanther test command tests all v2 detection classes that have a tests attribute set. In addition to testing rule() and alert function output in this way, if you have created custom helper functions for use in rules, you may want to write targeted tests for these helper functions. To do this, it's recommended to use a common Python testing framework, such as pytest or unittest.

Currently, pypanther will not run these tests during pypanther test, but they can be run locally or as part of your CI/CD workflow.

Example: using pytest to test a custom function

Say you'd like to write a reusable function that reads a list of AWS account IDs from a file and filters it to include only the production ones. (This function could then be used in an include or exclude filter.) This function might look like the following:

import pandas as pd

# A sample list of account data you may use with your detections
_account_data = [
        {"account_id": "012345678901", "account_age_days": 1543, "environment": "prod"},
        {"account_id": "112345678901", "account_age_days": 1753, "environment": "staging"},
        ...
        {"account_id": "756239201432", "account_age_days": 32, "environment": "development"}
        ]

# Load account data into a pandas dataframe for easy access
AWS_ACCOUNTS = pd.DataFrame.from_dict(_account_data) 

def is_prod_aws_account(account_id: str) -> bool:
    """
    Checks the AWS_ACCOUNTS dataframe to see if an account_id is in production
    """
    return AWS_ACCOUNTS.loc[AWS_ACCOUNTS['account_id'] == account_id, "environment"] == "prod"

Given this function, there are a few ways in which you may want to test both the data fixture (AWS_ACCOUNTS) itself, as well as the function is_prod_aws_account(). For instance:

  • To verify that the AWS_ACCOUNTS data fixture:

    • Has a certain number of rows

    • Has a certain required field

  • To verify that certain account IDs are included in the prod list (and others are not)

To test these characteristics using pytest, you would add the following functions (all with the test_ prefix):

def test_accounts_metadata_is_correct_size():
    """
    Ensure that the account list has 100 rows.
    """
    assert AWS_ACCOUNTS.shape[0] == 100

def test_critical_list_is_marked_prod():
    """
    Ensure that specific accounts are marked as production while other ones are not
    """
    expected_prod_list = ["012345678901", "012345678902", "012345678903"]
    for account_id in expected_prod_list:
        assert is_prod_aws_account(account_id)
    
    expected_non_prod_list = ["012345678905", "012345678906", "012345678907"]
    for account_id in expected_non_prod_list:
        assert not is_prod_aws_account(account_id)

def test_accounts_metadata_has_account_age_field():
    """
    Ensure the metadata data fixture has a field we may require for another helper function
    """
    assert "account_age_days" in AWS_ACCOUNTS.columns

After you define these tests, you can invoke them by running pytest from your command line.

Uploading v2 detections to Panther

In order to use the pypanther upload functionality, it must first be enabled for you. If you would like to upload detections, please reach out to your Panther Support team.

To upload all v2 detections that are registered, run:

$ pypanther upload

You must authenticate when using upload. See Authenticating CLI commands, below.

You can see all upload options by running pypanther upload -h, but may find the following options particularly useful:

  • --verbose: Generates verbose output, which includes a list of tests by detection (and their pass/fail statuses), a list of registered detections, and a list of included files

  • --output {text, json}: Prints output in the provided format

    • text is the default value, but json can be useful if you plan to port the output into another workflow

Sample output for pypanther upload --verbose --output json
{
    "result": "UPLOAD_SUCCEEDED",
    "upload_statistics": {
      "rules": {
        "new": 0,
        "total": 569,
        "modified": 569,
        "deleted": 0
      }
    },
    "tests": {
      "test_results": {
        // Only a subset of rule tests are shown below, for conciseness
        "AWS.ECR.EVENTS": [
          {
            "test_name": "Authorized account, unauthorized region",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Unauthorized account",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Authorized account",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Osquery.Mac.OSXAttacksKeyboardEvents": [
          {
            "test_name": "App running on Desktop that is watching keyboard events",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "App is running from approved path",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Unrelated query does not alert",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Dropbox.User.Disabled.2FA": [
          {
            "test_name": "2FA Disabled",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Other",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Push.Security.Authorized.IdP.Login": [
          {
            "test_name": "Google Workspace Password Login",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Microsoft 365 OIDC Login",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Okta Login",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Password Login",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "GSuite.GroupBannedUser": [
          {
            "test_name": "User Added",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "User Banned from Group",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "AWS.S3.ServerAccess.Error": [
          {
            "test_name": "Amazon Access Error",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Access Error",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "403 on HEAD.BUCKET",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Internal Server Error",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "OneLogin.PasswordAccess": [
          {
            "test_name": "User accessed their own password",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "User accessed another user's password",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Push.Security.New.App.Detected": [
          {
            "test_name": "New App",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "App Updated",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "GCP.VPC.Flow.Logs.Disabled": [
          {
            "test_name": "Disable Flow Logs Event",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Enable Flow Logs Event",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Duo.Admin.Bypass.Code.Viewed": [
          {
            "test_name": "Bypass View",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Bypass Create",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Netskope.ManyDeletes": [
          {
            "test_name": "True positive",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "True negative",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Slack.AuditLogs.UserPrivilegeEscalation": [
          {
            "test_name": "Owner Transferred",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Permissions Assigned",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Role Changed to Admin",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Role Changed to Owner",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "User Logout",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Dropbox.External.Share": [
          {
            "test_name": "Domain in Allowlist",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "external share",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ],
        "Slack.AuditLogs.AppAccessExpanded": [
          {
            "test_name": "App Scopes Expanded",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "App Resources Added",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "App Resources Granted",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "Bot Token Upgraded",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          },
          {
            "test_name": "User Logout",
            "passed": true,
            "exceptions": [],
            "failed_results": []
          }
        ]
      },
      "failed_tests_summary": [],
      "test_summary": {
        "skipped_rules": 96,
        "passed_rules": 464,
        "failed_rules": 0,
        "total_rules": 560,
        "passed_tests": 1373,
        "failed_tests": 0,
        "total_tests": 1373
      }
    },
    "registered_rules": [
      "AWS.ECR.EVENTS",
      "Osquery.Mac.OSXAttacksKeyboardEvents",
      "Dropbox.User.Disabled.2FA",
      "Push.Security.Authorized.IdP.Login",
      "GSuite.GroupBannedUser",
      "GCP.IAM.serviceAccounts.signBlob",
      "Okta.App.Unauthorized.Access.Attempt",
      "GCP.K8S.Service.Type.NodePort.Deployed",
      "Snyk.Project.Settings",
      "Notion.SharingSettingsUpdated",
      "GCP.UnusedRegions",
      "AWS.User.Login.Profile.Modified",
      "aws_cloudtrail_security_group_change_ingress_egress",
      "Notion.LoginFromBlockedIP",
      "github_push_protection_bypass_detected",
      "Azure.Audit.ManyFailedSignIns",
      "AWS.EC2.ManualSecurityGroupChange",
      "OneLogin.HighRiskFailedLogin",
      "GCP.Destructive.Queries",
      "Atlassian.User.LoggedInAsUser",
      "AWS.EC2.VPCModified",
      "Zoom.New.Meeting.Passcode.Required.Disabled",
      "Slack.AuditLogs.OrgCreated",
      "Tines.Story.Jobs.Clearance",
      "DUO.User.BypassCode.Used",
      "GCP.IAM.CustomRoleChanges",
      "GSuite.Workspace.GmailDefaultRoutingRuleModified",
      "Box.Access.Granted",
      "CarbonBlack.Audit.Admin.Grant",
      "Github.Repo.Created",
      "gcp_breakglass_container_workload_deployed",
      "AWS.RDS.PublicRestore",
      "AWS.RDS.ManualSnapshotCreated",
      "Crowdstrike.DNS.Request",
      "Crowdstrike.Unusual.Parent.Child.Processes",
      "AWS.GuardDuty.MediumSeverityFinding",
      "Tailscale.HTTPS.Disabled",
      "okta_fastpass_phishing_detection",
      "Teleport.AuthErrors",
      "Teleport.SuspiciousCommands",
      "Okta.Identity.Provider.SignIn",
      "Decoy.Secret.Accessed",
      "AWS.Root.Activity",
      "aws_rds_public_db_restore",
      "okta_user_account_locked_out",
      "gcp_service_account_disabled_or_deleted",
      "aws_efs_fileshare_mount_modified_or_deleted",
      "Asana.Team.Privacy.Public",
      "GSuite.LeakedPassword",
      "GCP.IAM.serviceAccounts.signJwt.Privilege.Escalation",
      "AWS.ECR.CRUD",
      "Standard.MaliciousSSODNSLookup",
      "AWS.Console.LoginWithoutSAML",
      "Notion.TeamspaceOwnerAdded",
      "GCP.Workload.Identity.Pool.Created.or.Updated",
      "Retrieve.SSO.access.token",
      "Snyk.Role.Change",
      "Okta.Refresh.Access.Token.Reuse",
      "github_push_protection_disabled",
      "Azure.Audit.LegacyAuth",
      "AWS.CloudTrail.Created",
      "OneLogin.HighRiskLogin",
      "Push.Security.MFA.Method.Changed",
      "Duo.Admin.Bypass.Code.Created",
      "MongoDB.External.UserInvited.NoConfig",
      "Osquery.Mac.UnwantedChromeExtensions",
      "GitHub.Branch.PolicyOverride",
      "Slack.AuditLogs.OrgDeleted",
      "Teleport.CompanyDomainLoginWithoutSAML",
      "aws_cloudtrail_security_group_change_loadbalancer",
      "Tines.Team.Destruction",
      "Box.Shield.Anomalous.Download",
      "CarbonBlack.Audit.API.Key.Created.Retrieved",
      "GitHub.Repo.HookModified",
      "GSuite.Workspace.GmailPredeliveryScanningDisabled",
      "gcp_bucket_enumeration",
      "GCP.Workforce.Pool.Created.or.Updated",
      "Microsoft365.Brute.Force.Login.by.User",
      "GCP.User.Added.to.IAP.Protected.Service",
      "Okta.PotentiallyStolenSession",
      "AppOmni.Alert.Passthrough",
      "AWS.S3.ServerAccess.IPWhitelist",
      "Auth0.CIC.Credential.Stuffing",
      "AWS.CloudTrail.RootAccessKeyCreated",
      "okta_identity_provider_created",
      "AWS.CloudTrail.SecurityConfigurationChange",
      "aws_root_account_usage",
      "AWS.IPSet.Modified",
      "aws_eks_cluster_created_or_deleted",
      "AWS.CloudTrail.Account.Discovery",
      "Crowdstrike.RealTimeResponse.Session",
      "Crowdstrike.Macos.Plutil.Usage",
      "Osquery.OSSECRootkitDetected",
      "GCP.Log.Bucket.Or.Sink.Deleted",
      "Google.Workspace.Admin.Custom.Role",
      "Notion.Audit.Log.Exported",
      "AWS.RDS.SnapshotShared",
      "Okta.Group.Admin.Role.Assigned",
      "Slack.AuditLogs.AppRemoved",
      "AWS.S3.ServerAccess.Error",
      "AWS.WAF.Disassociation",
      "github_repo_or_org_transferred",
      "OneLogin.PasswordAccess",
      "Push.Security.New.App.Detected",
      "GCP.VPC.Flow.Logs.Disabled",
      "Duo.Admin.Bypass.Code.Viewed",
      "MongoDB.Identity.Provider.Activity",
      "Asana.Service.Account.Created",
      "GCP.Firewall.Rule.Created",
      "DUO.User.Endpoint.Failure",
      "Standard.MFADisabled",
      "Slack.AuditLogs.PassthroughAnomaly",
      "Teleport.LocalUserLoginWithoutMFA",
      "aws_cloudtrail_security_group_change_rds",
      "GCP.Inbound.SSO.Profile.Created",
      "Decoy.Systems.Manager.Parameter.Accessed",
      "Tines.Tenant.AuthToken",
      "Box.Event.Triggered.Externally",
      "Snyk.System.ExternalAccess",
      "CarbonBlack.Audit.Data.Forwarder.Stopped",
      "AWS.EC2.Monitoring",
      "GSuite.Workspace.GmailSecuritySandboxDisabled",
      "GSuite.LoginType",
      "Okta.Rate.Limits",
      "AWS.S3.BucketDeleted",
      "gcp_service_account_modified",
      "Auth0.Integration.Installed",
      "okta_mfa_reset_or_deactivated",
      "gcp_bucket_modified_or_deleted",
      "Github.Repo.VisibilityChange",
      "GCP.GKE.Kubernetes.Cron.Job.Created.Or.Modified",
      "Slack.AuditLogs.ApplicationDoS",
      "aws_elasticache_security_group_created",
      "AWS.IAM.AccessKeyCompromised",
      "Push.Security.New.SaaS.Account.Created",
      "Teleport.CreateUserAccounts",
      "AWS.CloudTrail.IAMAnythingChanged",
      "Snyk.ServiceAccount.Change",
      "Auth0.Custom.Role.Created",
      "aws_route_53_domain_transferred_lock_disabled",
      "Notion.Workspace.Exported",
      "Zoom.PasscodeDisabled",
      "github_secret_scanning_feature_disabled",
      "Role.Assumed.by.AWS.Service",
      "OneLogin.PasswordChanged",
      "Duo.Admin.Create.Admin",
      "Osquery.OutdatedAgent",
      "MongoDB.Logging.Toggled",
      "GitHub.Repo.InitialAccess",
      "GitHub.Action.Failed",
      "Okta.SSO.to.AWS",
      "GCP.Access.Attempts.Violating.VPC.Service.Controls",
      "Crowdstrike.FDR.LOLBAS",
      "Slack.AuditLogs.PotentiallyMaliciousFileShared",
      "Google.Workspace.Advanced.Protection.Program",
      "aws_cloudtrail_ssm_malicious_usage",
      "CarbonBlack.Audit.Flagged",
      "Okta.Identity.Provider.Created.Modified",
      "Zoom.Sign.In.Method.Modified",
      "okta_user_created",
      "GSuite.DeviceCompromise",
      "GSuite.Workspace.PasswordEnforceStrongDisabled",
      "AWS.CloudTrail.UserAccessKeyAuth",
      "okta_network_zone_deactivated_or_deleted",
      "gcp_dlp_re_identifies_sensitive_information",
      "Wiz.Alert.Passthrough",
      "Crowdstrike.Base64EncodedArgs",
      "gcp_sql_database_modified_or_deleted",
      "Slack.AuditLogs.DLPModified",
      "gcp_access_policy_deleted",
      "Asana.Workspace.Default.Session.Duration.Never",
      "Push.Security.Open.Security.Finding",
      "AWS.S3.BucketPolicyModified",
      "Microsoft365.External.Document.Sharing",
      "AWS.DNS.Crypto.Domain",
      "AWS.S3.ServerAccess.Unauthenticated",
      "Crowdstrike.WMI.Query.Detection",
      "Role.Assumed.by.User",
      "OneLogin.AuthFactorRemoved",
      "AWS.KMS.CustomerManagedKeyLoss",
      "Duo.Admin.Lockout",
      "MongoDB.org.Membership.Restriction.Disabled",
      "aws_route_53_domain_transferred_to_another_account",
      "OneLogin.ActiveLoginActivity",
      "GCP.BigQuery.Large.Scan",
      "Slack.AuditLogs.PrivateChannelMadePublic",
      "Google.Workspace.Apps.Marketplace.Allowlist",
      "aws_config_disable_recording",
      "GCP.Logging.Settings.Modified",
      "Notion.SAML.SSO.Configuration.Changed",
      "AWS.EC2.NetworkACLModified",
      "CarbonBlack.Audit.User.Added.Outside.Org",
      "github_self_hosted_runner_changes_detected",
      "AWS.CloudTrail.LoginProfileCreatedOrModified",
      "Zoom.Sign.In.Requirements.Changed",
      "GCP.GCS.IAMChanges",
      "Teleport.LockCreated",
      "GSuite.DeviceUnlockFailure",
      "okta_user_session_start_via_anonymised_proxy",
      "Standard.DNSBase64",
      "GitHub.Advanced.Security.Change",
      "Osquery.UnsupportedMacOS",
      "Snyk.System.PolicySetting",
      "okta_new_behaviours_admin_console",
      "aws_elasticache_security_group_modified_or_deleted",
      "gcp_dns_zone_modified_or_deleted",
      "Zendesk.MobileAppAccessUpdated",
      "Slack.AuditLogs.EKMConfigChanged",
      "AWS.CloudTrail.Password.Policy.Discovery",
      "Asana.Workspace.Email.Domain.Added",
      "GSuite.Workspace.PasswordReuseEnabled",
      "Push.Security.Phishable.MFA.Method",
      "GCP.Firewall.Rule.Modified",
      "AWS.Suspicious.SAML.Activity",
      "GCP.K8s.ExecIntoPod",
      "AWS.VPC.HealthyLogStatus",
      "AWS.S3.ServerAccess.Insecure",
      "gcp_vpn_tunnel_modified_or_deleted",
      "Github.Repository.Transfer",
      "Crowdstrike.API.Key.Created",
      "OneLogin.ThresholdAccountsDeleted",
      "DUO.Admin.Action.MarkedFraudulent",
      "MongoDB.User.Created.Or.Deleted",
      "Okta.Support.Reset",
      "aws_s3_data_management_tampering",
      "Snyk.System.SSO",
      "Auth0.MFA.Factor.Setting.Enabled",
      "GCP.Cloud.Run.Service.Created",
      "Slack.AuditLogs.UserPrivilegeChangedToUser",
      "Google.Workspace.Apps.Marketplace.New.Domain.Application",
      "GCP.Logging.Sink.Modified",
      "Notion.Workspace.Public.Page.Added",
      "AWS.EC2.RouteTableModified",
      "Zendesk.NewAPIToken",
      "CarbonBlack.AlertV2.Passthrough",
      "github_ssh_certificate_config_changed",
      "AWS.LAMBDA.CRUD",
      "Crowdstrike.Remote.Access.Tool.Execution",
      "Zoom.Two.Factor.Authentication.Disabled",
      "GSuite.DeviceSuspiciousActivity",
      "Box.Item.Shared.Externally",
      "GitHub.Branch.ProtectionDisabled",
      "Osquery.SSHListener",
      "okta_password_in_alternateid_field",
      "Notion.LoginFromNewLocation",
      "aws_console_getsignintoken",
      "Auth0.MFA.Policy.Disabled",
      "GCP.K8s.IOC.Activity",
      "AWS.VPC.InboundPortWhitelist",
      "aws_enum_buckets",
      "Okta.Login.Without.Push.Marker",
      "Slack.AuditLogs.EKMSlackbotUnenrolled",
      "AWS.Console.LoginWithoutMFA",
      "Connection.to.Embargoed.Country",
      "Asana.Workspace.Form.Link.Auth.Requirement.Disabled",
      "Standard.NewAWSAccountCreated",
      "Push.Security.Phishing.Attack",
      "Tailscale.Machine.Approval.Requirements.Disabled",
      "gcp_firewall_rule_modified_or_deleted",
      "github_delete_action_invoked",
      "Microsoft365.MFA.Disabled",
      "Teleport.LongLivedCerts",
      "OneLogin.ThresholdAccountsModified",
      "AWS.CloudTrail.Stopped",
      "Duo.Admin.MFA.Restrictions.Updated",
      "AWS.Unsuccessful.MFA.attempt",
      "Okta.Login.Success",
      "Sign-in.with.AWS.CLI.prompt",
      "AWS.SecurityHub.Finding.Evasion",
      "Box.Malicious.Content",
      "Osquery.SuspiciousCron",
      "aws_securityhub_finding_evasion",
      "GCP.GCS.Public",
      "Slack.AuditLogs.ServiceOwnerTransferred",
      "Google.Workspace.Apps.New.Mobile.App.Installed",
      "GSuite.Workspace.TrustedDomainsAllowlist",
      "Okta.Support.Access",
      "Zendesk.AccountOwnerChanged",
      "AWS.Console.RootLogin",
      "GCP.Permissions.Granted.to.Create.or.Manage.Service.Account.Key",
      "CiscoUmbrella.DNS.Blocked",
      "Tailscale.Magic.DNS.Disabled",
      "Crowdstrike.API.Key.Deleted",
      "GSuite.Rule",
      "MongoDB.User.Roles.Changed",
      "Okta.ThreatInsight.Security.Threat.Detected",
      "GitHub.Org.AuthChange",
      "GCP.Cloud.Run.Set.IAM.Policy",
      "aws_delete_identity",
      "Auth0.MFA.Policy.Enabled",
      "GCP.K8s.New.Daemonset.Deployed",
      "AWS.VPC.InboundPortBlacklist",
      "GitHub.Secret.Scanning.Alert.Created",
      "Osquery.Linux.AWSCommandExecuted",
      "aws_guardduty_disruption",
      "GSuite.AdvancedProtection",
      "okta_admin_activity_from_proxy_query",
      "AWS.EC2.SecurityGroupModified",
      "Slack.AuditLogs.EKMUnenrolled",
      "Snyk.User.Management",
      "Asana.Workspace.Guest.Invite.Permissions.Anyone",
      "AWS.ConfigService.Created",
      "Zoom.User.Promoted.to.Privileged.Role",
      "Notion.Many.Pages.Deleted",
      "GCP.IAM.CorporateEmail",
      "github_disable_high_risk_configuration",
      "Teleport.NetworkScanning",
      "GCP.Privilege.Escalation.By.Deployments.Create",
      "Microsoft365.Exchange.External.Forwarding",
      "okta_policy_modified_or_deleted",
      "OneLogin.UnauthorizedAccess",
      "Duo.Admin.New.Admin.API.App.Integration",
      "Box.New.Login",
      "Panther.Detection.Deleted",
      "aws_snapshot_backup_exfiltration",
      "GCP.Cloud.Storage.Buckets.Modified.Or.Deleted",
      "Push.Security.Unauthorized.IdP.Login",
      "Slack.AuditLogs.SSOSettingsChanged",
      "AWS.S3.ServerAccess.UnknownRequester",
      "Okta.Global.MFA.Disabled",
      "Zendesk.SensitiveDataRedactionOff",
      "GSuite.Drive.ExternalFileShare",
      "Crowdstrike.Macos.Add.Trusted.Cert",
      "gcp_full_network_traffic_packet_capture",
      "CiscoUmbrella.DNS.FuzzyMatching",
      "AWS.Macie.Evasion",
      "Teleport.RoleCreated",
      "Tines.Actions.DisabledChanges",
      "Standard.AdminRoleAssigned",
      "GSuite.SuspiciousLogins",
      "Netskope.AdminLoggedOutLoginFailures",
      "Okta.New.Behavior.Accessing.Admin.Console",
      "GitHub.Org.IpAllowlist",
      "aws_disable_bucket_versioning",
      "Auth0.MFA.Risk.Assessment.Enabled",
      "GCP.K8s.Pod.Attached.To.Node.Host.Network",
      "AWS.VPC.UnapprovedOutboundDNS",
      "Osquery.Linux.LoginFromNonOffice",
      "aws_iam_backdoor_users_keys",
      "AWS.EC2.Startup.Script.Change",
      "okta_admin_role_assigned_to_user_or_group",
      "Microsoft.Graph.Passthrough",
      "Crowdstrike.Credential.Dumping.Tool",
      "Asana.Workspace.New.Admin",
      "Dropbox.Admin.sign.in.as.Session",
      "Okta.User.Account.Locked",
      "Notion.Many.Pages.Exported",
      "AWS.ConfigService.DisabledDeleted",
      "aws_attached_malicious_lambda_layer",
      "GCP.CloudBuild.Potential.Privilege.Escalation",
      "github_disabled_outdated_dependency_or_vulnerability",
      "GSuite.CalendarMadePublic",
      "Slack.AuditLogs.IDPConfigurationChanged",
      "GSuite.DriveOverlyVisible",
      "okta_policy_rule_modified_or_deleted",
      "AWS.CloudTrail.UnauthorizedAPICall",
      "GCP.Service.Account.Access.Denied",
      "OneLogin.UserAccountLocked",
      "Duo.Admin.Policy.Updated",
      "Netskope.AdminUserChange",
      "Amazon.EKS.Audit.Multiple403",
      "Box.Content.Workflow.Policy.Violation",
      "Panther.SAML.Modified",
      "aws_sso_idp_change",
      "Auth0.MFA.Risk.Assessment.Disabled",
      "Snyk.Misc.Settings",
      "Crowdstrike.Reverse.Shell.Tool.Executed",
      "AWS.EC2.Vulnerable.XZ.Image.Launched",
      "GitHub.Team.Modified",
      "Zendesk.UserAssumption",
      "Tines.Custom.CertificateAuthority",
      "GSuite.TwoStepVerification",
      "GitHub.Org.Moderators.Add",
      "AWS.Console.Login",
      "Auth0.User.Invitation.Created",
      "GCP.IAM.OrgFolderIAMChanges",
      "aws_iam_s3browser_loginprofile_creation",
      "Osquery.Linux.Mac.VulnerableXZliblzma",
      "Okta.Anonymizing.VPN.Login",
      "Decoy.DynamoDB.Accessed",
      "gcp_kubernetes_admission_controller",
      "okta_admin_role_assignment_created",
      "CiscoUmbrella.DNS.Suspicious",
      "Okta.User.MFA.Factor.Suspend",
      "Notion.PagePerms.APIPermsChanged",
      "Okta.Org2org.Creation.Modification",
      "Salesforce.Admin.Login.As.User",
      "aws_cloudtrail_disable_logging",
      "AWS.Software.Discovery",
      "aws_ec2_disable_encryption",
      "AWS.CloudTrail.CodebuildProjectMadePublic",
      "AWS.CloudTrail.SnapshotMadePublic",
      "github_fork_private_repos_enabled_or_cleared",
      "AWS.Console.RootLoginFailed",
      "Asana.Workspace.Org.Export",
      "GSuite.DocOwnershipTransfer",
      "Slack.AuditLogs.InformationBarrierModified",
      "Okta.AdminRoleAssigned",
      "okta_security_threat_detected",
      "GCP.SQL.ConfigChanges",
      "OneLogin.UserAssumption",
      "Duo.Admin.SSO.SAML.Requirement.Disabled",
      "GCP.Cloudfunctions.Functions.Create",
      "Netskope.ManyDeletes",
      "GCP.K8S.Pot.Create.Or.Modify.Host.Path.Volume.Mount",
      "Standard.BruteForceByIP",
      "Box.Shield.Suspicious.Alert",
      "Teleport.RootLogin",
      "Slack.AuditLogs.UserPrivilegeEscalation",
      "AWS.CloudTrail.IAMCompromisedKeyQuarantine",
      "Dropbox.External.Share",
      "GCP.iam.roles.update.Privilege.Escalation",
      "AWS.IAMUser.ReconAccessDenied",
      "Auth0.Post.Login.Action.Flow",
      "GitHub.User.AccessKeyCreated",
      "Zendesk.UserRoleChanged",
      "MongoDB.2FA.Disabled",
      "AWS.EC2.StopInstances",
      "aws_sts_assumerole_misuse",
      "Tines.Enqueued.Retrying.Job.Destruction",
      "GSuite.UserSuspended",
      "GCP.Service.Account.or.Keys.Created",
      "GitHub.Org.Modified",
      "Push.Security.App.Banner.Acknowledged",
      "Okta.User.MFA.Reset.Single",
      "Osquery.Mac.ApplicationFirewallSettings",
      "okta_api_token_created",
      "GSuite.DriveVisibilityChanged",
      "AWS.Console.Sign-In",
      "aws_iam_s3browser_templated_s3_bucket_policy_creation",
      "aws_cloudtrail_imds_malicious_usage",
      "aws_ec2_startup_script_change",
      "Okta.PasswordAccess",
      "AWS.Modify.Cloud.Compute.Infrastructure",
      "gcp_kubernetes_cronjob",
      "github_new_org_member",
      "GCP.iam.serviceAccountKeys.create",
      "Slack.AuditLogs.IntuneMDMDisabled",
      "MongoDB.Access.Allowed.From.Anywhere",
      "okta_suspicious_activity_enduser_report",
      "Crowdstrike.Cryptomining.Tools",
      "OnePassword.Lut.Sensitive.Item",
      "Notion.PagePerms.GuestPermsChanged",
      "Duo.Admin.User.MFA.Bypass.Enabled",
      "AWS.CloudTrail.IAMEntityCreatedWithoutCloudFormation",
      "GCP.Cloudfunctions.Functions.Update",
      "GCP.K8s.Pod.Using.Host.PID.Namespace",
      "Netskope.NetskopePersonnelActivity",
      "SentinelOne.Threats",
      "Box.Untrusted.Device",
      "Panther.Sensitive.Role",
      "Teleport.SAMLCreated",
      "AWS.IAM.PolicyModified",
      "Decoy.IAM.Assumed",
      "GSuite.ExternalMailForwarding",
      "Dropbox.Linked.Team.Application.Added",
      "Asana.Workspace.Password.Requirements.Simple",
      "GitHub.User.RoleUpdated",
      "Zendesk.UserSuspension",
      "Amazon.EKS.Audit.SystemNamespaceFromPublicIP",
      "Tines.Global.Resource.Destruction",
      "GCP.serviceusage.apiKeys.create.Privilege.Escalation",
      "Github.Organization.App.Integration.Installed",
      "AWS.CloudTrail.IAMAssumeRoleBlacklistIgnored",
      "AWS.EC2.EBS.Encryption.Disabled",
      "Osquery.Mac.AutoUpdateEnabled",
      "aws_sts_getsessiontoken_misuse",
      "okta_api_token_revoked",
      "Cloudflare.Firewall.L7DDoS",
      "Okta.APIKeyCreated",
      "MongoDB.Alerting.Disabled.Or.Deleted",
      "OnePassword.Sensitive.Item",
      "Snyk.Org.Settings",
      "aws_iam_s3browser_user_or_accesskey_creation",
      "SentinelOne.Alert.Passthrough",
      "aws_cloudtrail_new_acl_entries",
      "aws_ec2_vm_export_failure",
      "github_new_secret_created",
      "GSuite.GoogleAccess",
      "Auth0.User.Joined.Tenant",
      "Crowdstrike.Systemlog.Tampering",
      "okta_unauthorized_access_to_app",
      "Crowdstrike.Detection.passthrough",
      "Notion.PageSharedToWeb",
      "DUO.User.Action.Fraudulent",
      "AWS.UnusedRegion",
      "GCP.compute.instances.create.Privilege.Escalation",
      "Netskope.UnauthorizedAPICalls",
      "Box.Large.Number.Downloads",
      "GCP.Access.Attempts.Violating.IAP.Access.Controls",
      "Teleport.SAMLLoginWithoutCompanyDomain",
      "Okta.User.MFA.Reset.All",
      "AWS.RDS.MasterPasswordUpdated",
      "Okta.Password.Extraction.via.SCIM",
      "Dropbox.Ownership.Transfer",
      "Asana.Workspace.Require.App.Approvals.Disabled",
      "AWS.CloudTrail.ResourceMadePublic",
      "GitLab.Audit.Password.Reset.Multiple.Emails",
      "Zoom.All.Meetings.Secured.With.One.Option.Disabled",
      "Slack.AuditLogs.LegalHoldPolicyModified",
      "Tines.SSO.Settings",
      "Crowdstrike.Macos.Osascript.Administrator",
      "Github.Public.Repository.Created",
      "AWS.IAM.Group.Read.Only.Events",
      "Osquery.Mac.OSXAttacks",
      "AWS.EC2.Traffic.Mirroring",
      "okta_application_modified_or_deleted",
      "Okta.APIKeyRevoked",
      "Snyk.OU.Change",
      "GSuite.Workspace.CalendarExternalSharingSetting",
      "MongoDB.External.UserInvited",
      "GCP.Storage.Hmac.Keys.Create",
      "OnePassword.Unusual.Client",
      "aws_passed_role_to_glue_development_endpoint",
      "GCP.DNS.Zone.Modified.or.Deleted",
      "gcp_kubernetes_rolebinding",
      "aws_cloudtrail_new_route_added",
      "aws_ecs_task_definition_cred_endpoint_query",
      "aws_susp_saml_activity",
      "AWS.EC2.GatewayModified",
      "AWS.CloudTrail.NetworkACLPermissiveEntry",
      "Okta.User.Reported.Suspicious.Activity",
      "Panther.User.Modified",
      "GCP.IAM.serviceAccounts.getAccessToken.Privilege.Escalation",
      "AWS.GuardDuty.HighSeverityFinding",
      "GSuite.GovernmentBackedAttack",
      "AWS.CloudTrail.AMIModifiedForPublicAccess",
      "Decoy.S3.Accessed",
      "Notion.Workspace.SCIM.Token.Generated",
      "DUO.User.Denied.AnomalousPush",
      "Box.Large.Number.Permission.Updates",
      "Slack.AuditLogs.AppAdded",
      "github_outside_collaborator_detected",
      "Teleport.ScheduledJobs",
      "Standard.NewUserAccountCreated",
      "Okta.Phishing.Attempt.Blocked.FastPass",
      "GCP.K8S.Privileged.Pod.Created",
      "Asana.Workspace.SAML.Optional",
      "MongoDB.Atlas.ApiKeyCreated",
      "GitLab.Production.Password.Reset.Multiple.Emails",
      "Standard.ImpossibleTravel.Login",
      "Duo.Admin.App.Integration.Secret.Key.Viewed",
      "Zoom.Automatic.Sign.Out.Disabled",
      "Slack.AuditLogs.MFASettingsChanged",
      "GSuite.Workspace.DataExportCreated",
      "Tines.Story.Items.Destruction",
      "AWS.IAM.CredentialsUpdated",
      "Azure.Audit.RiskLevelPassthrough",
      "Notion.AccountChangedAfterLogin",
      "Github.Repo.CollaboratorChange",
      "AWS.IAM.Backdoor.User.Keys",
      "AWS.CloudTrail.RootPasswordChanged",
      "AWS.GuardDuty.LowSeverityFinding",
      "okta_application_sign_on_policy_modified_or_deleted",
      "Cloudflare.HttpRequest.BotHighVolume",
      "aws_rds_change_master_password",
      "GCP.Firewall.Rule.Deleted",
      "gcp_kubernetes_secrets_modified_or_deleted",
      "Slack.AuditLogs.AppAccessExpanded",
      "aws_efs_fileshare_modified_or_deleted"
    ],
    "included_files": [
      "mitre.py",
      "main.py",
      "rules/__init__.py",
      "rules/sigma/__init__.py",
      "rules/sigma/cloud/__init__.py",
      "rules/sigma/cloud/gcp/gcp_dns_zone_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_kubernetes_rolebinding.py",
      "rules/sigma/cloud/gcp/gcp_full_network_traffic_packet_capture.py",
      "rules/sigma/cloud/gcp/gcp_sql_database_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_kubernetes_secrets_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_kubernetes_admission_controller.py",
      "rules/sigma/cloud/gcp/gcp_dlp_re_identifies_sensitive_information.py",
      "rules/sigma/cloud/gcp/gcp_firewall_rule_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/__init__.py",
      "rules/sigma/cloud/gcp/gcp_bucket_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_access_policy_deleted.py",
      "rules/sigma/cloud/gcp/gcp_service_account_disabled_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_service_account_modified.py",
      "rules/sigma/cloud/gcp/gcp_vpn_tunnel_modified_or_deleted.py",
      "rules/sigma/cloud/gcp/gcp_kubernetes_cronjob.py",
      "rules/sigma/cloud/gcp/gcp_breakglass_container_workload_deployed.py",
      "rules/sigma/cloud/gcp/gcp_bucket_enumeration.py",
      "rules/sigma/cloud/github/github_new_secret_created.py",
      "rules/sigma/cloud/github/github_delete_action_invoked.py",
      "rules/sigma/cloud/github/github_self_hosted_runner_changes_detected.py",
      "rules/sigma/cloud/github/__init__.py",
      "rules/sigma/cloud/github/github_outside_collaborator_detected.py",
      "rules/sigma/cloud/github/github_repo_or_org_transferred.py",
      "rules/sigma/cloud/github/github_push_protection_disabled.py",
      "rules/sigma/cloud/github/github_secret_scanning_feature_disabled.py",
      "rules/sigma/cloud/github/github_disabled_outdated_dependency_or_vulnerability.py",
      "rules/sigma/cloud/github/github_push_protection_bypass_detected.py",
      "rules/sigma/cloud/github/github_new_org_member.py",
      "rules/sigma/cloud/github/github_disable_high_risk_configuration.py",
      "rules/sigma/cloud/github/github_fork_private_repos_enabled_or_cleared.py",
      "rules/sigma/cloud/github/github_ssh_certificate_config_changed.py",
      "rules/sigma/cloud/okta/okta_admin_role_assigned_to_user_or_group.py",
      "rules/sigma/cloud/okta/okta_network_zone_deactivated_or_deleted.py",
      "rules/sigma/cloud/okta/okta_application_modified_or_deleted.py",
      "rules/sigma/cloud/okta/okta_policy_modified_or_deleted.py",
      "rules/sigma/cloud/okta/okta_mfa_reset_or_deactivated.py",
      "rules/sigma/cloud/okta/okta_security_threat_detected.py",
      "rules/sigma/cloud/okta/okta_policy_rule_modified_or_deleted.py",
      "rules/sigma/cloud/okta/okta_application_sign_on_policy_modified_or_deleted.py",
      "rules/sigma/cloud/okta/__init__.py",
      "rules/sigma/cloud/okta/okta_suspicious_activity_enduser_report.py",
      "rules/sigma/cloud/okta/okta_api_token_revoked.py",
      "rules/sigma/cloud/okta/okta_user_session_start_via_anonymised_proxy.py",
      "rules/sigma/cloud/okta/okta_user_account_locked_out.py",
      "rules/sigma/cloud/okta/okta_unauthorized_access_to_app.py",
      "rules/sigma/cloud/okta/okta_fastpass_phishing_detection.py",
      "rules/sigma/cloud/okta/okta_api_token_created.py",
      "rules/sigma/cloud/okta/okta_user_created.py",
      "rules/sigma/cloud/okta/okta_admin_role_assignment_created.py",
      "rules/sigma/cloud/okta/okta_identity_provider_created.py",
      "rules/sigma/cloud/okta/okta_password_in_alternateid_field.py",
      "rules/sigma/cloud/okta/okta_admin_activity_from_proxy_query.py",
      "rules/sigma/cloud/okta/okta_new_behaviours_admin_console.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_iam_s3browser_templated_s3_bucket_policy_creation.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_elasticache_security_group_created.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_route_53_domain_transferred_to_another_account.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_root_account_usage.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_ec2_startup_script_change.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_config_disable_recording.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_passed_role_to_glue_development_endpoint.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_elasticache_security_group_modified_or_deleted.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_efs_fileshare_mount_modified_or_deleted.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_route_53_domain_transferred_lock_disabled.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_security_group_change_ingress_egress.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_sso_idp_change.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_disable_logging.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_disable_bucket_versioning.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_sts_assumerole_misuse.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_security_group_change_rds.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_iam_backdoor_users_keys.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_new_acl_entries.py",
      "rules/sigma/cloud/aws_cloudtrail/__init__.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_sts_getsessiontoken_misuse.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_snapshot_backup_exfiltration.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_delete_identity.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_enum_buckets.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_iam_s3browser_loginprofile_creation.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_iam_s3browser_user_or_accesskey_creation.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_attached_malicious_lambda_layer.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_s3_data_management_tampering.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_eks_cluster_created_or_deleted.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_ec2_disable_encryption.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_security_group_change_loadbalancer.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_console_getsignintoken.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_ec2_vm_export_failure.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_efs_fileshare_modified_or_deleted.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_susp_saml_activity.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_ecs_task_definition_cred_endpoint_query.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_rds_change_master_password.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_imds_malicious_usage.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_securityhub_finding_evasion.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_rds_public_db_restore.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_ssm_malicious_usage.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_cloudtrail_new_route_added.py",
      "rules/sigma/cloud/aws_cloudtrail/aws_guardduty_disruption.py",
      "helpers/__init__.py"
    ]
  }
  

If a previously uploaded rule is not passed in to register() on a subsequent invocation of upload, it will be deleted in your Panther instance.

Uploading a v2 rule with the same id as an existing v1 rule's RuleId will overwrite it. The same is true in the other direction—i.e., uploading a v1 rule with the same RuleId as an existing v2 rule's id will overwrite it.

upload limitations

Currently, the following limitations on pypanther upload apply:

  • A maximum of 500 rules can be uploaded at once.

  • The total size of the zip file pypanther produces for upload cannot exceed 4 MB.

Getting started using v2 detections

Panther has provided this pypanther-starter-kit repository, containing v2 detection examples, which you can clone to quickly get up and running.

Get started by following the setup instructions in the repository's README.

Rule property reference

  • Required properties are bolded.

PropertyData typeOverwritten byDefault valueDescription/notes

log_types

List[LogType | String]

id

String

Required only for registered rules

default_severity

Severity | String

severity()

create_alert

Boolean

True

dedup_period_minutes

Non-negative integer

60

default_description

String

description()

“”

display_name

String

“”

enabled

Boolean

True

default_destinations

List[String]

destinations()

[]

default_reference

String

reference()

“”

reports

Dictionary[String,List[String]]

{}

default_runbook

String

runbook()

“”

summary_attributes

List[String]

[]

tags

List[String]

[]

tests

List[RuleTest]

[]

threshold

Positive integer

1

RuleTest property reference

PropertyData typeDefaultDescription

name

String

The name of the test case

expected_result

Boolean

Whether rule() should return true or false

log

Dictionary | String

The log event that should be tested against the detection

mocks

list[RuleMock]

[]

expected_severity

Severity | String

None

The expected severity of the resulting alert

expected_title

String

None

The expected title of the resulting alert

expected_dedup

String

None

The expected deduplication string of the resulting alert

expected_runbook

String

None

The expected runbook of the resulting alert

expected_reference

String

None

The expected reference of the resulting alert

expected_description

String

None

The expected description of the resulting alert

expected_alert_context

Dictionary

None

The expected alert context of the resulting alert

RuleMock property reference

PropertyData typeDefault

object_name

String

return_value

Any

None

side_effect

Any

None

new

Any

None

Rule auxiliary/alerting function reference

  • Required methods are bolded.

FunctionData type it returnsDefault valueDescription/notes

rule()

Boolean

severity()

Severity | String

Value of default_severity

title()

String

Value of display_name

dedup()

String

Value of title() > display_name > id

destinations()

List[String]

Value of default_destinations

runbook()

String

Value of default_runbook

reference()

String

Value of default_reference

description()

String

Value of default_description

alert_context()

Dictionary

pypanther convenience function reference

Convenience functionHow it works

get_panther_rules()

get_rules()

register()

override()

pypanther CLI command reference

CommandRequired flag(s)How it worksRequired API permission(s)

test

See Running tests To see a full list of command options, run $ pypanther test --help

None

list rules

Either --managed or --registered (both can be supplied)

Lists rule attributes in a table for easy viewing

To see a full list of command options, run $ pypanther list rules --help

None

get rule $RULE_ID

Gets the attributes of a single rule. Can also retrieve the original class definition

To see a full list of command options, run $ pypanther get rule --help

None

upload

Warning: In order to use the pypanther upload functionality, it must first be enabled for you. If you would like to upload detections, please reach out to your Panther Support team. To see a full list of command options, run $ pypanther upload --help

Bulk Upload

Authenticating CLI commands

Certain pypanther CLI commands, like upload, require authentication with your Panther instance. This means they require a valid Panther API host URL and API token. After you locate/generate these values, you will make them visible to pypanther.

Step 1: Locate/generate your Panther API host URL and token

Step 2: Make API host and token values visible to pypanther

Once you have API host and token values, you can choose how to expose them to pypanther when you are executing a CLI command. The following methods are in order of precedence, meaning option one overrides option two:

  1. Pass the host and token on the command line using --api-token and --api-host.

  2. Set the host and token as environment variables using PANTHER_API_TOKEN and PANTHER_API_HOST.

Last updated