Detection rules › Panther

Panther rules: aws

RuleSeverity
A CloudTrail Was Created or Updatedinformational
Account Security Configuration Changedmedium
Amazon Machine Image (AMI) Modified to Allow Public Accessmedium
AWS Access Key Rotationmedium
AWS Access Keys At Account Creationlow
AWS ACM Certificate Expirationmedium
AWS ACM Certificate Statushigh
AWS ACM Secure Algorithmshigh
AWS Administrative IAM User Createdinformational
AWS AMI Sharinghigh
AWS Application Load Balancer Web ACLhigh
AWS Authentication From CrowdStrike Unmanaged Devicemedium
AWS Authentication from CrowdStrike Unmanaged Device
AWS Authentication from CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)
AWS Backdoor Administrative IAM Role Createdhigh
AWS Bedrock Guardrail Updated or Deletedmedium
AWS Bedrock Model Invocation Abnormal Token Usageinformational
AWS Bedrock Model Invocation GuardRail Intervenedinformational
AWS Bedrock Model Invocation Logging Configuration Deletedmedium
AWS CDE EC2 Volume Encryptionmedium
AWS CloudFormation Stack Driftlow
AWS CloudFormation Stack IAM Service Roleinformational
AWS CloudFormation Stack Termination Protectioninformational
AWS CloudTrail Account Discoveryinformational
AWS CloudTrail Attempt To Leave Orginformational
AWS CloudTrail CloudWatch Logslow
AWS CloudTrail Least Privilege Accessmedium
AWS CloudTrail Log Encryptionmedium
AWS CloudTrail Log Validationmedium
AWS CloudTrail Management Events Enabledhigh
AWS CloudTrail Password Policy Discoveryinformational
AWS CloudTrail Password Sprayingmedium
AWS Cloudtrail Region Enabledmedium
AWS CloudTrail Retention Lifecycle Too Shortinformational
AWS CloudTrail S3 Bucket Access Loggingmedium
AWS CloudTrail S3 Bucket Publichigh
AWS CloudTrail SES Check Identity Verificationsinformational
AWS CloudTrail SES Check Send Quotainformational
AWS CloudTrail SES Check SES Sending Enabledinformational
AWS CloudTrail SES Enumerationmedium
AWS CloudTrail SES List Identitiesinformational
AWS CloudWatch Log Encryptioninformational
AWS CloudWatch Logs Data Retentionlow
AWS Compromised IAM Key Quarantinehigh
AWS Config Global Resourceslow
AWS Config Recording Statusmedium
AWS Config Records All Resource Typeslow
AWS Config Service Createdinformational
AWS Config Service Disabledmedium
AWS Config Statushigh
AWS Console GetSigninToken Potential Abusemedium
AWS Console Logininformational
AWS Console Sign-In WITHOUT Okta Redirecthigh
AWS Decrypt SSM Parametersmedium
AWS DNS Crypto Domainhigh
AWS DNS Logs Deletedinformational
AWS DynamoDB Table Autoscalinglow
AWS DynamoDB Table Autoscaling Configurationlow
AWS DynamoDB Table TTLlow
AWS EC2 AMI Approved Hostlow
AWS EC2 AMI Approved Instance Typelow
AWS EC2 AMI Approved Tenancylow
AWS EC2 Download Instance User Datamedium
AWS EC2 EBS Encryption Disabledmedium
AWS EC2 Image Monitoringinformational
AWS EC2 Instance Approved AMIhigh
AWS EC2 Instance Approved Hostlow
AWS EC2 Instance Approved Instance Typelow
AWS EC2 Instance Approved Tenancylow
AWS EC2 Instance Approved VPChigh
AWS EC2 Instance Detailed Monitoringlow
AWS EC2 Instance EBS Optimizationlow
AWS EC2 Launch Unusual EC2 Instancesinformational
AWS EC2 Manual Security Group Changemedium
AWS EC2 Many Password Read Attemptsinformational
AWS EC2 Multi Instance Connectinformational
AWS EC2 Startup Script Changehigh
AWS EC2 Traffic Mirroringmedium
AWS EC2 Volume Encryptionhigh
AWS EC2 Volume Snapshot Encryptionhigh
AWS EC2 Vulnerable XZ Image Launchedhigh
AWS ECR Eventsinformational
AWS ELB SSL Policiesmedium
AWS Enforces SSL Policiesmedium
AWS GuardDuty Critical Severity Findingcritical
AWS GuardDuty Enabledhigh
AWS GuardDuty High Severity Findinghigh
AWS GuardDuty Low Severity Findinglow
AWS GuardDuty Master Accounthigh
AWS GuardDuty Medium Severity Findingmedium
AWS IAM Access Key Compromise Detectionhigh
AWS IAM Group Read Only Eventsinformational
AWS IAM Group Userslow
AWS IAM Password Unusedlow
AWS IAM Policy Administrative Privilegeshigh
AWS IAM Policy Assigned to Userlow
AWS IAM Policy Blocklistmedium
AWS IAM Policy Does Not Grant Any Administrative Accessmedium
AWS IAM Policy Does Not Grant Network Admin Accessmedium
AWS IAM Policy Role Mappinghigh
AWS IAM Resource Does Not Have Inline Policymedium
AWS IAM Role Grants (permission) to Non-organizational Accountcritical
AWS IAM Role Restricts Usagemedium
AWS IAM Role Trust Relationship for GitHub Actionshigh
AWS IAM User MFA high
AWS IAM User Not In Conflicting Groupsmedium
AWS IMDS Credential Usage Outside Expected Serviceshigh
AWS KMS CMK Key Rotationlow
AWS KMS Key Restricts Usagehigh
AWS Lambda Public Accesshigh
AWS Macie Disabled/Updatedmedium
AWS Modify Cloud Compute Infrastructuremedium
AWS Network ACL Overly Permissive Entry Createdmedium
AWS Network ACL Restricts Inbound Trafficmedium
AWS Network ACL Restricts Insecure Protocolslow
AWS Network ACL Restricts Outbound Trafficlow
AWS Network ACL Restricts SSHhigh
AWS Password Policy Complexity Guidelineshigh
AWS Password Policy Password Age Limitmedium
AWS Password Policy Password Reusemedium
AWS Potential Backdoor Lambda Function Through Resource-Based Policyinformational
AWS Potentially Stolen Service Rolehigh
AWS Potentially Stolen Service Role
AWS Privilege Escalation Via User Compromisemedium
AWS Public RDS Restorehigh
AWS RDS Activity Stream Stoppedhigh
AWS RDS Automated Backup Deletedcritical
AWS RDS Cluster Failover Initiatedmedium
AWS RDS Deletion Protection Disabledhigh
AWS RDS Instance Backupmedium
AWS RDS Instance Encryptionhigh
AWS RDS Instance Has Acceptable Backup Retention Periodlow
AWS RDS Instance High Availabilitylow
AWS RDS Instance Minor Version Upgradeslow
AWS RDS Instance Modified to be Publicly Accessiblecritical
AWS RDS Instance or Cluster Deletedhigh
AWS RDS Instance or Cluster Rebootedmedium
AWS RDS Instance Public Accesshigh
AWS RDS Instance Snapshot Public Accesscritical
AWS RDS Log File Downloadedlow
AWS RDS Manual/Public Snapshot Createdlow
AWS RDS Master Password Updatedlow
AWS RDS Security Group Ingress Authorizedmedium
AWS RDS Snapshot Copied Cross-Regionmedium
AWS RDS Snapshot Deletedhigh
AWS RDS Snapshot Enumeration with Public or Shared Flaglow
AWS RDS Snapshot Exported to S3high
AWS RDS Snapshot Sharedhigh
AWS Redshift Cluster Encryptionhigh
AWS Redshift Cluster Has Acceptable Snapshot Retention Periodlow
AWS Redshift Cluster Loggingmedium
AWS Redshift Cluster Maintenance Windowinformational
AWS Redshift Cluster Snapshot Retentionmedium
AWS Redshift Cluster Version Upgradelow
AWS Resource Made Publicmedium
AWS Resource Minimum Tags low
AWS Resource Required Tagslow
AWS Root Account Access Keyscritical
AWS Root Account Hardware MFAhigh
AWS Root Account MFAcritical
AWS S3 Access Errorinformational
AWS S3 Access IP Allowlistmedium
AWS S3 Bucket Action Restrictionsmedium
AWS S3 Bucket Encryptionhigh
AWS S3 Bucket Lifecycle Configurationlow
AWS S3 Bucket Logginglow
AWS S3 Bucket MFA Deletelow
AWS S3 Bucket Name DNS Complianceinformational
AWS S3 Bucket Object Lock Configuredlow
AWS S3 Bucket Policy Allow With Not Principalhigh
AWS S3 Bucket Policy Modifiedinformational
AWS S3 Bucket Principal Restrictionshigh
AWS S3 Bucket Public Access Blockmedium
AWS S3 Bucket Public Readhigh
AWS S3 Bucket Public Writecritical
AWS S3 Bucket Secure Accesslow
AWS S3 Bucket Versioninglow
AWS S3 Copy Object with Client-Side Encryptionmedium
AWS S3 Delete Object Detectioninformational
AWS S3 Delete Objects Detectioninformational
AWS S3 Insecure Accesslow
AWS S3 Large Downloadinformational
AWS S3 Large Download
AWS S3 Object Copied to External Account Bucketmedium
AWS S3 Object Exfiltration WITH Object Deletionhigh
AWS S3 Ransomware Note Upload Detectionmedium
AWS S3 Security Control Disablinghigh
AWS S3 Security Controls Disabledhigh
AWS S3 Unauthenticated Accesslow
AWS S3 Unknown Requesterlow
AWS SAML Activitymedium
AWS Secrets Manager Batch Retrieve Secretsinformational
AWS Secrets Manager Batch Retrieve Secrets Catch-Allinformational
AWS Secrets Manager Retrieve Secrets Multi-Regioninformational
AWS Security Group - Only DMZ Publicly Accessiblemedium
AWS Security Group Administrative Ingresshigh
AWS Security Group Restricts Access To CDEmedium
AWS Security Group Restricts Inbound Trafficmedium
AWS Security Group Restricts Inter-SG Trafficlow
AWS Security Group Restricts Outbound Trafficlow
AWS Security Group Restricts Traffic Leaving CDEmedium
AWS Security Group Tightly Restricts Inbound Trafficlow
AWS Security Group Tightly Restricts Outbound Trafficlow
AWS SecurityHub Finding Evasionhigh
AWS Snapshot Made Publicmedium
AWS Software Discoveryinformational
AWS SSM Distributed Commandinformational
AWS SSM Multiple Sessionsinformational
AWS SSM Multiple Sessions
AWS SSO Access Token Retrieved by Unauthenticated IPmedium
AWS STS GetCallerIdentity via TruffleHogmedium
AWS STS GetSessionToken by IAM Userlow
AWS Trusted IPSet Modifiedhigh
AWS Unsuccessful MFA attempthigh
AWS Unused Access Keylow
AWS User API Key Createdmedium
AWS User Login Profile Created or Modifiedlow
AWS User Takeover Via Password Resethigh
AWS VPC Default Network ACL Restricts All Trafficlow
AWS VPC Default Security Group Restrictions low
AWS VPC Flow Logsmedium
AWS VPC Flow Logs Removedinformational
AWS VPC Healthy Log Statusinformational
AWS WAF Disassociationcritical
AWS WAF Has XSS Predicatemedium
AWS WAF Logging Configuredhigh
AWS WAF Managed Admin Protection Passthrough Rulehigh
AWS WAF Managed Anti-DDoS Passthrough Rulelow
AWS WAF Managed Bot Control Passthrough Ruleinformational
AWS WAF Managed Core Rule Set Passthrough Rulemedium
AWS WAF Managed IP Reputation Passthrough Ruleinformational
AWS WAF Managed Known Bad Inputs Passthrough Rulehigh
AWS WAF Managed SQL Database Passthrough Rulehigh
AWS WAF ReactJS RCE Attempt via Bodyhigh
AWS WAF Rule Orderinghigh
AWS WAF WebACL Has Associated Resourcesmedium
AWS.CloudTrail.UserAccessKeyAuthinformational
CloudTrail EC2 StopInstancesinformational
CloudTrail Event Selectors Disabledmedium
CloudTrail Stoppedmedium
CodeBuild Project made Publichigh
DEPRECATED - AWS User Login Profile Modifiedhigh
Detect Reconnaissance from IAM Usersinformational
EC2 Network ACL Modifiedinformational
EC2 Network Gateway Modifiedinformational
EC2 Route Table Modifiedinformational
EC2 Secrets Manager Retrieve Secretsinformational
EC2 Security Group Modifiedinformational
EC2 VPC Modifiedinformational
ECR CRUD Actionsinformational
External Principal Accessing AWS Resources Via VPC Endpointmedium
Failed Root Console Loginhigh
IAM Administrator Role Policy Attachedinformational
IAM Assume Role Blocklist Ignoredhigh
IAM Changeinformational
IAM Entity Created Without CloudFormationmedium
IAM Inline Policy Network Adminmedium
IAM Policy Modifiedinformational
IAM Role Added to RDS Instance or Clustermedium
IAM Role Createdinformational
IAM Role Policy Updated to Allow Internet Accessmedium
IAM User Createdinformational
IAM User Policy Attached with Administrator Accessinformational
KMS CMK Disabled or Deletedinformational
Lambda Code Updated by Userhigh
Lambda Configuration Updated with Layers by Userhigh
Lambda CRUD Actionshigh
Logins Without MFAhigh
Logins Without SAMLhigh
Monitor Unauthorized API Callsinformational
New IAM Credentials Updatedinformational
Root Account Access Key Createdcritical
Root Account Activityhigh
Root Console Loginhigh
Root Password Changedhigh
S3 Access Via VPC Endpoint From External IPmedium
S3 Bucket Deletedinformational
S3 Bucket Encryption Deletedmedium
S3 Bucket Logging Disabledlow
S3 Bucket Policy Confused Deputy Protection for Service Principalshigh
S3 Bucket Replication Deletedmedium
S3 Bucket Versioning Suspendedlow
S3 MFA Delete Disabledlow
S3 Object Encrypted with External KMS Keyhigh
S3 Public Access Block Deletedmedium
Sensitive API Calls Via VPC Endpointmedium
Sensitive AWS CloudWatch Log Encryptionhigh
SIGNAL - AWS Console SSO Sign-Ininformational
Signal - VPC Flow Logs Allowed SSHinformational
StopInstance WITH ModifyInstanceAttributeshigh
Unused AWS Regionhigh
VPC Endpoint Access Deniedmedium
VPC Flow Logs Inbound Port Allowlisthigh
VPC Flow Logs Inbound Port Blocklisthigh
VPC Flow Logs Unapproved Outbound DNS Trafficmedium
VPC Flow Port Scanningmedium

A CloudTrail Was Created or Updated

#
Severity
informational
Compliance
CIS 3.5; Stratus Red Team aws.defense-evasion.cloudtrail-stop
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Discovery:Cloud Service Dashboard
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A CloudTrail Trail was created, updated, or enabled.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of CloudTrail changes
CLOUDTRAIL_CREATE_UPDATE = {
    "CreateTrail",
    "UpdateTrail",
    "StartLogging",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in CLOUDTRAIL_CREATE_UPDATE


def title(event):
    return f"CloudTrail [{event.deep_get('requestParameters', 'name')}] was created/updated"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_created.py
RuleID: "AWS.CloudTrail.Created"
DisplayName: "A CloudTrail Was Created or Updated"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Discovery:Cloud Service Dashboard
Reports:
  CIS:
    - 3.5
  MITRE ATT&CK:
    - TA0007:T1538

  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-stop
Severity: Info
Description: >
  A CloudTrail Trail was created, updated, or enabled.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified
Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateTrail, UpdateTrail, StartLogging

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CreateTrail
  • StartLogging
  • UpdateTrail
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
namerequestParameters.name

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateTrail",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "name": "arn:aws:cloudtrail:us-west-2:123456789012:trail/example-trail"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

Account Security Configuration Changed

#
Severity
medium
Compliance
Stratus Red Team aws.defense-evasion.cloudtrail-delete, aws.defense-evasion.cloudtrail-stop, aws.defense-evasion.vpc-remove-flow-logs
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An account wide security configuration was changed.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import json
from fnmatch import fnmatch
from unittest.mock import MagicMock

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

SECURITY_CONFIG_ACTIONS = {
    "DeleteAccountPublicAccessBlock",
    "DeleteDeliveryChannel",
    "DeleteDetector",
    "DeleteFlowLogs",
    "DeleteRule",
    "DeleteTrail",
    "DisableEbsEncryptionByDefault",
    "DisableRule",
    "StopConfigurationRecorder",
    "StopLogging",
}

ALLOW_LIST = [
    # Add expected events and users here to suppress alerts
    # {"userName": "ExampleUser", "eventName": "DeleteRule"},
]


def rule(event):
    global ALLOW_LIST  # pylint: disable=global-statement
    if isinstance(ALLOW_LIST, MagicMock):
        ALLOW_LIST = json.loads(ALLOW_LIST())  # pylint: disable=not-callable

    if not aws_cloudtrail_success(event):
        return False

    for entry in ALLOW_LIST:
        if fnmatch(
            event.deep_get(
                "userIdentity",
                "sessionContext",
                "sessionIssuer",
                "userName",
                default="",
            ),
            entry["userName"],
        ):
            if fnmatch(event.get("eventName"), entry["eventName"]):
                return False

    if event.get("eventName") == "UpdateDetector":
        return not event.deep_get("requestParameters", "enable", default=True)

    return event.get("eventName") in SECURITY_CONFIG_ACTIONS


def title(event):
    return f"Sensitive AWS API call {event.get('eventName')} made by {event.udm('actor_user')}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_security_configuration_change.py
RuleID: "AWS.CloudTrail.SecurityConfigurationChange"
DisplayName: "Account Security Configuration Changed"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion:Impair Defenses
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0005:T1562

  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-delete
    - aws.defense-evasion.cloudtrail-stop
    - aws.defense-evasion.vpc-remove-flow-logs
Description: An account wide security configuration was changed.
Runbook: >
  Verify that this change was planned. If not, revert the change and update the access control policies to ensure this doesn't happen again.
Reference: https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/controls-acct.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • any of:
    • all of:
      • eventName is UpdateDetector
      • requestParameters.enable is empty
    • all of:
      • eventName is not UpdateDetector
      • eventName is one of DeleteAccountPublicAccessBlock, DeleteDeliveryChannel, DeleteDetector, DeleteFlowLogs, DeleteRule (+5 more values, see Indicators below)

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNameeq
  • UpdateDetector
field:"aws::eventName" kind:eq value:"UpdateDetector"
eventNamein
  • DeleteAccountPublicAccessBlock
  • DeleteDeliveryChannel
  • DeleteDetector
  • DeleteFlowLogs
  • DeleteRule
  • DeleteTrail
  • DisableEbsEncryptionByDefault
  • DisableRule
  • StopConfigurationRecorder
  • StopLogging
field:"aws::eventName" kind:in
eventNamene
  • UpdateDetector
field:"aws::eventName" kind:ne value:"UpdateDetector"
requestParameters.enableis_null
  • (no value, null check)
field:"requestParameters.enable" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Verify that this change was planned. If not, revert the change and update the access control policies to ensure this doesn't happen again.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1111",
  "eventName": "DeleteTrail",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "name": "example-trail"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla/2.0 (compatible; NEWT ActiveX; Win32)",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

Amazon Machine Image (AMI) Modified to Allow Public Access

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration:Transfer Data to Cloud Account
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An Amazon Machine Image (AMI) was modified to allow it to be launched by anyone. Any sensitive configuration or application data stored in the AMI's block devices is at risk.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_detection_helpers.caching import check_account_age


def rule(event):
    # Only check successful ModiyImageAttribute events
    if not aws_cloudtrail_success(event) or event.get("eventName") != "ModifyImageAttribute":
        return False

    added_perms = event.deep_get(
        "requestParameters", "launchPermission", "add", "items", default=[{}]
    )

    for item in added_perms:
        if item.get("group") == "all":
            return True
        if check_account_age(
            item.get("userId", "") + "-" + event.udm("user_account_id", default="")
        ):  # checking if the account is new
            return True

    return False


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ami_modified_for_public_access.py
RuleID: "AWS.CloudTrail.AMIModifiedForPublicAccess"
DisplayName: "Amazon Machine Image (AMI) Modified to Allow Public Access"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration:Transfer Data to Cloud Account
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Description: >
  An Amazon Machine Image (AMI) was modified to allow it to be launched by anyone.
  Any sensitive configuration or application data stored in the AMI's block devices is at risk.
Runbook: |
  Determine if the AMI is intended to be publicly accessible.
  If not, first modify the AMI to not be publicly accessible then change any sensitive data stored
  in the block devices associated to the AMI (as they may be compromised).
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/sharingamis-intro.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is ModifyImageAttribute

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneModifyImageAttributeexcludes:eventName field:"eventName" value:"ModifyImageAttribute"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Determine if the AMI is intended to be publicly accessible.

If not, first modify the AMI to not be publicly accessible then change any sensitive data stored

in the block devices associated to the AMI (as they may be compromised).

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1111",
  "eventName": "ModifyImageAttribute",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "attributeType": "launchPermission",
    "imageId": "ami-1111",
    "launchPermission": {
      "add": {
        "items": [
          {
            "group": "all"
          }
        ]
      }
    }
  },
  "responseElements": {
    "_return": true
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla/2.0 (compatible; NEWT ActiveX; Win32)",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Access Key Rotation

#
Severity
medium
Compliance
CIS 1.4
Resource types
AWS.IAM.RootUser, AWS.IAM.User
Tags
AWS, Identity & Access Management, Credential Access:Unsecured Credentials
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that AWS IAM account access keys are rotated every 90 days. Rotating access keys will reduce the window of opportunity for an access key that is associated with a compromised or terminated account to be used.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

import datetime

from panther_base_helpers import resolve_timestamp_string

TIMEOUT_DAYS = datetime.timedelta(days=90)


def aged_out(timestamp):
    if not timestamp:
        return False
    datetime_ts = resolve_timestamp_string(timestamp)
    if not datetime_ts:
        return False
    return (datetime.datetime.now() - datetime_ts) > TIMEOUT_DAYS


def policy(resource):
    # If a user is less than 4 hours old, it may not have a credential report generated yet.
    # It will be re-scanned periodically until a credential report is found, at which point this
    # policy will be properly evaluated.
    report = resource.get("CredentialReport")
    if not report:
        return True

    if report.get("AccessKey1Active"):
        if aged_out(report.get("AccessKey1LastRotated")):
            return False
    if report.get("AccessKey2Active"):
        if aged_out(report.get("AccessKey2LastRotated")):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_access_key_rotation.py
PolicyID: "AWS.AccessKey.Rotation"
DisplayName: "AWS Access Key Rotation"
Enabled: true
ResourceTypes:
  - AWS.IAM.RootUser
  - AWS.IAM.User
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Unsecured Credentials
Reports:
  CIS:
    - 1.4
  MITRE ATT&CK:
    - TA0006:T1552
Severity: Medium
Description: >
  This policy validates that AWS IAM account access keys are rotated every 90 days.
  Rotating access keys will reduce the window of opportunity for an access key that
  is associated with a compromised or terminated account to be used.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-rotated-every-90-days
Reference: >
  https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html

Stages and Predicates

Flags AWS.IAM.RootUser, AWS.IAM.User resources when the condition below holds.

Condition

  • CredentialReport is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReportis_null(no value, null check)excludes:CredentialReport

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CredentialReportis_not_null
  • (no value, null check)
field:"CredentialReport" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-rotated-every-90-days

AWS Access Keys At Account Creation

#
Severity
low
Compliance
CIS 1.21
Resource types
AWS.IAM.User, AWS.IAM.RootUser
Tags
AWS, Identity & Access Management
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that AWS IAM user accounts do not have access keys that were created during account creation. This results in excess keys being generated, and unnecessary management work in auditing and rotating these keys.

Detection logic

from datetime import timedelta

from panther_base_helpers import deep_get, resolve_timestamp_string

AWS_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
DEFAULT_TIME = "0001-01-01T00:00:00Z"
MAX_SECONDS_TO_AUTOGEN_KEY = timedelta(seconds=7)


def policy(resource):
    # If a user is less than 4 hours old, it may not have a credential report generated yet.
    # It will be re-scanned periodically until a credential report is found, at which point this
    # policy will be properly evaluated.
    if not resource.get("CredentialReport"):
        return True

    key_rot = deep_get(resource, "CredentialReport", "AccessKey1LastRotated")
    if key_rot == DEFAULT_TIME:
        return True

    create = resource.get("TimeCreated", "")
    key_rot_date = resolve_timestamp_string(key_rot)
    create_date = resolve_timestamp_string(create)

    if not key_rot_date or not create_date:
        return True

    return (key_rot_date - create_date) >= MAX_SECONDS_TO_AUTOGEN_KEY

Rule specification

AnalysisType: policy
Filename: aws_access_keys_at_account_creation.py
DisplayName: "AWS Access Keys At Account Creation"
PolicyID: "AWS.AccessKeys.AccountCreation"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
  - AWS.IAM.RootUser
Tags:
  - AWS
  - Identity & Access Management
Reports:
  CIS:
    - 1.21
Severity: Low
Description: >
  This policy validates that AWS IAM user accounts do not have access keys that were
  created during account creation. This results in excess keys being generated,
  and unnecessary management work in auditing and rotating these keys.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-are-not-created-at-account-creation
Reference: >
  https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html

Stages and Predicates

Flags AWS.IAM.User, AWS.IAM.RootUser resources when all of the conditions below hold.

Condition

  • CredentialReport is present
  • CredentialReport.AccessKey1LastRotated is not 0001-01-01T00:00:00Z

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReportis_null(no value, null check)excludes:CredentialReport
CredentialReport.AccessKey1LastRotatedeq0001-01-01T00:00:00Zexcludes:CredentialReport.AccessKey1LastRotated field:"CredentialReport.AccessKey1LastRotated" value:"0001-01-01T00:00:00Z"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-are-not-created-at-account-creation

AWS ACM Certificate Expiration

#
Severity
medium
Resource types
AWS.ACM.Certificate
Tags
AWS, Availability, Resource Development:Obtain Capabilities
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

When a certificate is 60 days away from expiration, ACM automatically attempts to renew it every hour.

MITRE ATT&CK coverage

TacticTechniques
Resource Development

Detection logic

import datetime

from panther_base_helpers import resolve_timestamp_string

AWS_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
EXPIRATION_BUFFER = datetime.timedelta(days=60)


def policy(resource):
    if not resource.get("NotAfter"):
        return False

    timestamp = resolve_timestamp_string(resource.get("NotAfter"))

    if not timestamp:
        return True

    time_to_expiration = timestamp - datetime.datetime.now()

    return time_to_expiration >= EXPIRATION_BUFFER

Rule specification

AnalysisType: policy
Filename: aws_acm_certificate_expiration.py
PolicyID: "AWS.ACM.Certificate.Expiration"
DisplayName: "AWS ACM Certificate Expiration"
Enabled: true
ResourceTypes:
  - AWS.ACM.Certificate
Reports:
  MITRE ATT&CK:
    - TA0042:T1588
Tags:
  - AWS
  - Availability
  - Resource Development:Obtain Capabilities
Severity: Medium
Description: >
  When a certificate is 60 days away from expiration, ACM automatically attempts to renew it every hour.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-acm-certificate-is-not-expired
Reference: https://docs.aws.amazon.com/acm/latest/userguide/troubleshooting-renewal.html

Stages and Predicates

Flags AWS.ACM.Certificate resources when the condition below holds.

Condition

  • NotAfter is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
NotAfteris_not_null(no value, null check)excludes:NotAfter

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
NotAfteris_null
  • (no value, null check)
field:"NotAfter" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-acm-certificate-is-not-expired

AWS ACM Certificate Status

#
Severity
high
Resource types
AWS.ACM.Certificate
Tags
AWS, Operations, Panther, Resource Development:Obtain Capabilities
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy checks if an ACM certificate renewal is pending or has failed and is in use by any other resources within the account.

MITRE ATT&CK coverage

TacticTechniques
Resource Development

Detection logic

def policy(resource):
    if not bool(resource["InUseBy"]):
        return True

    return resource["Status"] == "ISSUED"

Rule specification

AnalysisType: policy
Filename: aws_acm_certificate_valid.py
PolicyID: "AWS.ACM.Certificate.Valid"
DisplayName: "AWS ACM Certificate Status"
Enabled: true
ResourceTypes:
  - AWS.ACM.Certificate
Reports:
  MITRE ATT&CK:
    - TA0042:T1588
Tags:
  - AWS
  - Operations
  - Panther
  - Resource Development:Obtain Capabilities
Severity: High
Description: >
  This policy checks if an ACM certificate renewal is pending or has failed and is in use
  by any other resources within the account.
Runbook: >
  From the ACM panel in the AWS console, check and resolve the certificate status.
Reference: https://docs.aws.amazon.com/acm/latest/userguide/check-certificate-renewal-status.html

Stages and Predicates

Flags AWS.ACM.Certificate resources when all of the conditions below hold.

Condition

  • InUseBy is present
  • Status is not ISSUED

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InUseByis_null(no value, null check)excludes:InUseBy
StatuseqISSUEDexcludes:Status field:"Status" value:"ISSUED"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InUseByis_not_null
  • (no value, null check)
field:"InUseBy" kind:is_not_null
Statusne
  • ISSUED
field:"Status" kind:ne value:"ISSUED"

Response runbook

From the ACM panel in the AWS console, check and resolve the certificate status.

AWS ACM Secure Algorithms

#
Severity
high
Compliance
PCI 2.2.3
Resource types
AWS.ACM.Certificate
Tags
AWS, Configuration Required, PCI, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all ACM certificates are using secure key and signature algorithms.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

# Specify what key and signature algorithms meet your organizations level of security here.
# Included is the AWS ACM default when creating new certificates as an example.
SECURE_KEY_ALGORITHMS = {
    "RSA-2048",  # AWS ACM default
}

SECURE_SIGNATURE_ALGORITHMS = {
    "SHA256WITHRSA",  # AWS ACM default
}


def policy(resource):
    return (
        resource["KeyAlgorithm"] in SECURE_KEY_ALGORITHMS
        and resource["SignatureAlgorithm"] in SECURE_SIGNATURE_ALGORITHMS
    )

Rule specification

AnalysisType: policy
Filename: aws_acm_certificate_has_secure_algorithms.py
PolicyID: "AWS.ACM.HasSecureAlgorithms"
DisplayName: "AWS ACM Secure Algorithms"
Enabled: false
ResourceTypes:
  - AWS.ACM.Certificate
Tags:
  - AWS
  - Configuration Required
  - PCI
  - Defense Evasion:Impair Defenses
Reports:
  PCI:
    - 2.2.3
  MITRE ATT&CK:
    - TA0005:T1562
Severity: High
Description: >
  This policy validates that all ACM certificates are using secure key and signature algorithms.
Runbook: >
  Delete the insecure certificate, and re-create with appropriately secure key and signature algorithms.
Reference: https://docs.aws.amazon.com/acm/latest/userguide/acm-certificate.html#algorithms

Stages and Predicates

Flags AWS.ACM.Certificate resources when any of the conditions below holds.

Condition

  • any of:
    • KeyAlgorithm is not one of RSA-2048
    • SignatureAlgorithm is not one of SHA256WITHRSA

Exclusions

The rule actively suppresses these predicates.

Response runbook

Delete the insecure certificate, and re-create with appropriately secure key and signature algorithms.

AWS Administrative IAM User Created

#
Severity
informational
Time window
30h
Match by
p_alert_context.request_username
Tags
Beta
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Identifies when an Administrative IAM user is creates. This could indicate a potential security breach.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.Administrative.IAM.User.Created.Group"
DisplayName: "AWS Administrative IAM User Created"
Enabled: false
Severity: Info
Tags:
  - Beta
Description: Identifies when an Administrative IAM user is creates. This could indicate a potential security breach.
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.persistence.iam-create-admin-user/
Reports:
  MITRE ATT&CK:
    - TA0006:T1078
Detection:
    - Group:
        - ID: CreateUser
          RuleID: AWS.IAM.CreateUser
        - ID: AttachAdminUserPolicy
          RuleID: AWS.IAM.AttachAdminUserPolicy
      MatchCriteria:
        field_name:
          - GroupID: CreateUser
            Match: p_alert_context.request_username
          - GroupID: AttachAdminUserPolicy
            Match: p_alert_context.request_username
      LookbackWindowMinutes: 1800
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.request_username. Each step needs one match unless a higher minimum is shown.

Stage 1: step CreateUser

References detection IAM User Created.

Stage 2: step AttachAdminUserPolicy

References detection IAM User Policy Attached with Administrator Access.

AWS AMI Sharing

#
Severity
high
Resource types
AWS.EC2.AMI
Tags
AWS, Panther, Panther Enterprise, Exfiltration:Transfer Data to Cloud Account
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that AMIs you have created are not configured to allow public access, which could result in accidental data loss. AMI's that you use but do not own are not evaluated by this policy.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

# Add owners as desired
APPROVED_OWNERS = [
    "amazon",
    "microsoft",
]


def policy(resource):
    # These are trusted public snapshot distributors, allow
    if resource["ImageOwnerAlias"] in APPROVED_OWNERS:
        return True

    # Ignore AMIs that are not owned by the scanned account
    if resource["AccountId"] != resource["OwnerId"]:
        return True

    return not resource["Public"]

Rule specification

AnalysisType: policy
Filename: aws_ami_private.py
PolicyID: "AWS.AMI.Private"
DisplayName: "AWS AMI Sharing"
Enabled: true
ResourceTypes:
  - AWS.EC2.AMI
Tags:
  - AWS
  - Panther
  - Panther Enterprise
  - Exfiltration:Transfer Data to Cloud Account
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Severity: High
Description: >
  This policy ensures that AMIs you have created are not configured to allow public access, which could result in accidental data loss. AMI's that you use but do not own are not evaluated by this policy.
Runbook: >
  Immediately remove public access from the AMI until you can determine whether that setting is intentional
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/sharing-amis.html

Stages and Predicates

Flags AWS.EC2.AMI resources when all of the conditions below hold.

Condition

  • ImageOwnerAlias is not one of amazon, microsoft
  • AccountId equals field OwnerId
  • Public is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AccountIdcross_field_compareOwnerIdexcludes:AccountId field:"AccountId" value:"OwnerId"
ImageOwnerAliasinamazon, microsoftexcludes:ImageOwnerAlias field:"ImageOwnerAlias" value:"amazon" field:"ImageOwnerAlias" value:"microsoft"
Publicis_null(no value, null check)excludes:Public

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Publicis_not_null
  • (no value, null check)
field:"Public" kind:is_not_null

Response runbook

Immediately remove public access from the AMI until you can determine whether that setting is intentional

AWS Application Load Balancer Web ACL

#
Severity
high
Resource types
AWS.ELBV2.ApplicationLoadBalancer
Tags
AWS, Configuration Required, Security Control, Initial Access:Exploit Public-Facing Application
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all application load balancers have an associated Web ACl to enforce protections against various web attacks.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_base_helpers import deep_get

# MAPPINGS is a dictionary where the Key is an application load balancer ARN, and the
# Value is a WAF web ACL ID. For each Load Balancer ARN present in MAPPINGS,
# this rule verifies that the load balancer has the associated Web ACL
MAPPINGS = {
    "TEST_LOAD_BALANCER_ARN": "TEST_WAF_WEB_ACL_ID",
}


def policy(resource):
    # Check if a Web ACL is required for this load balancer
    if resource["LoadBalancerArn"] not in MAPPINGS:
        return True

    # Check if a Web ACL exists for this load balancer
    if resource["WebAcl"] is None:
        return False

    # Check that the correct Web ACL is assigned for this load balancer
    return deep_get(resource, "WebAcl", "WebACLId") == MAPPINGS[resource["LoadBalancerArn"]]

Rule specification

AnalysisType: policy
Filename: aws_application_load_balancer_web_acl.py
PolicyID: "AWS.ApplicationLoadBalancer.WebACL"
DisplayName: "AWS Application Load Balancer Web ACL"
Enabled: false
ResourceTypes:
  - AWS.ELBV2.ApplicationLoadBalancer
Tags:
  - AWS
  - Configuration Required
  - Security Control
  - Initial Access:Exploit Public-Facing Application
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
Severity: High
Description: >
  This policy validates that all application load balancers have an associated Web ACl to enforce protections against various web attacks.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-application-load-balancer-has-web-acl
Reference: https://aws.amazon.com/blogs/aws/aws-web-application-firewall-waf-for-application-load-balancers/

Stages and Predicates

Flags AWS.ELBV2.ApplicationLoadBalancer resources when all of the conditions below hold.

Condition

  • LoadBalancerArn is one of TEST_LOAD_BALANCER_ARN
  • WebAcl is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LoadBalancerArneqTEST_LOAD_BALANCER_ARNexcludes:LoadBalancerArn field:"LoadBalancerArn" value:"TEST_LOAD_BALANCER_ARN"
WebAclis_not_null(no value, null check)excludes:WebAcl

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LoadBalancerArnin
  • TEST_LOAD_BALANCER_ARN
field:"LoadBalancerArn" kind:in value:"TEST_LOAD_BALANCER_ARN"
WebAclis_null
  • (no value, null check)
field:"WebAcl" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-application-load-balancer-has-web-acl

AWS Authentication From CrowdStrike Unmanaged Device

#
Severity
medium
Tags
Multi-Table Query
Reference
www.crowdstrike.com
Source
github.com/panther-labs/panther-analysis

Detects AWS Logins from IP addresses not found in CrowdStrike's AIP list. May indicate unmanaged device being used, or faulty CrowdStrike Sensor.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(_):
    return True


def title(event):
    return (
        f"AWS [{event.get('eventName')}] for "
        f"[{event.deep_get('userIdentity', 'arn', default = '<arn_not_found>')}]"
        " from unmanaged IP Address."
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: scheduled_rule
Description: Detects AWS Logins from IP addresses not found in CrowdStrike's AIP list. May indicate unmanaged device being used, or faulty CrowdStrike Sensor.
DisplayName: "AWS Authentication From CrowdStrike Unmanaged Device"
Enabled: false
Filename: aws_authentication_from_crowdstrike_unmanaged_device.py
Reference: https://www.crowdstrike.com/wp-content/uploads/2023/05/crowdstrike-falcon-device-control-data-sheet.pdf
Severity: Medium
DedupPeriodMinutes: 60
RuleID: "AWS.Authentication.From.CrowdStrike.Unmanaged.Device"
Threshold: 1
ScheduledQueries:
  - AWS Authentication from CrowdStrike Unmanaged Device
Tags:
  - Multi-Table Query

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query AWS Authentication from CrowdStrike Unmanaged Device; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/home",
    "MFAIdentifier": "arn:aws:iam::12345:mfa/homer_simpson",
    "MFAUsed": "Yes",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-2",
  "eventCategory": "Management",
  "eventID": "12345",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2023-01-10 20:10:41",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "12345",
  "responseElements": {
    "ConsoleLogin": "Success"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "us-east-2.signin.aws.amazon.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
  "userIdentity": {
    "accountId": "12345",
    "arn": "arn:aws:iam::12345:user/homer_simpson",
    "principalId": "ABCDEF",
    "type": "IAMUser",
    "userName": "homer_simpson"
  }
}

AWS Authentication from CrowdStrike Unmanaged Device

#
Tags
Multi-Table Query
Source
github.com/panther-labs/panther-analysis

Detects AWS Authentication events with IP Addresses not found in CrowdStrike's AIP List

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
Description: Detects AWS Authentication events with IP Addresses not found in CrowdStrike's AIP List
Enabled: false
SnowflakeQuery: |
  SELECT *
  FROM panther_logs.public.aws_cloudtrail
  WHERE p_occurs_since('1 hour')
    AND eventName IN ('ConsoleLogin', 'SignIn', 'GetSessionToken')
    AND eventSource IN ('sts.amazonaws.com', 'signin.amazonaws.com')
    AND sourceIPAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.public.crowdstrike_aidmaster
        WHERE p_occurs_since('3 days')
      )

DatabricksQuery: |
  SELECT *
  FROM panther_logs.aws_cloudtrail
  WHERE p_occurs_since('1 hour')
    AND eventName IN ('ConsoleLogin', 'SignIn', 'GetSessionToken')
    AND eventSource IN ('sts.amazonaws.com', 'signin.amazonaws.com')
    AND sourceIPAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.crowdstrike_aidmaster
        WHERE p_occurs_since('3 days')
      )
QueryName: "AWS Authentication from CrowdStrike Unmanaged Device"
Schedule:
  RateMinutes: 60
  TimeoutMinutes: 3
Tags:
  - Multi-Table Query

Stages and Predicates

Stage 1: source

Table
panther_logs.public.aws_cloudtrail

Stage 2: filter

  • eventName is one of ConsoleLogin, SignIn, GetSessionToken
  • eventSource is one of sts.amazonaws.com, signin.amazonaws.com
  • sourceIPAddress is not in the results of a subquery on panther_logs.public.crowdstrike_aidmaster
Window
1h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • ConsoleLogin
  • GetSessionToken
  • SignIn
field:"aws::eventName" kind:in
eventSourcein
  • signin.amazonaws.com
  • sts.amazonaws.com
field:"aws::eventSource" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

AWS Authentication from CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)

#
Tags
Multi-Table Query
Source
github.com/panther-labs/panther-analysis

Detects AWS Authentication events with IP Addresses not found in CrowdStrike's AIP List

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

# This file is the part of the Crowdstrike FDREvent migration, and it's the equivalent of
# https://github.com/panther-labs/panther-analysis/blob/b61db1ecf3967c5f6a44c1782f8891fd5f54384d/queries/aws_queries/AWS_Authentication_from_CrowdStrike_Unmanaged_Device.yml
#
AnalysisType: scheduled_query
Description: Detects AWS Authentication events with IP Addresses not found in CrowdStrike's AIP List
Enabled: false
SnowflakeQuery: |
  SELECT *
  FROM panther_logs.public.aws_cloudtrail
  WHERE p_occurs_since('1 days')
    AND eventName IN ('ConsoleLogin', 'SignIn', 'GetSessionToken')
    AND eventSource IN ('sts.amazonaws.com', 'signin.amazonaws.com')
    AND sourceIPAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.public.crowdstrike_fdrevent
        WHERE p_occurs_since('3 days') AND panther_logs.public.crowdstrike_fdrevent.fdr_event_type = 'aid_master'
      )

DatabricksQuery: |
  SELECT *
  FROM panther_logs.aws_cloudtrail
  WHERE p_occurs_since('1 days')
    AND eventName IN ('ConsoleLogin', 'SignIn', 'GetSessionToken')
    AND eventSource IN ('sts.amazonaws.com', 'signin.amazonaws.com')
    AND sourceIPAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.crowdstrike_fdrevent
        WHERE p_occurs_since('3 days') AND panther_logs.crowdstrike_fdrevent.fdr_event_type = 'aid_master'
      )
QueryName: "AWS Authentication from CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)"
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 3
Tags:
  - Multi-Table Query

Stages and Predicates

Stage 1: source

Table
panther_logs.public.aws_cloudtrail

Stage 2: filter

  • eventName is one of ConsoleLogin, SignIn, GetSessionToken
  • eventSource is one of sts.amazonaws.com, signin.amazonaws.com
  • sourceIPAddress is not in the results of a subquery on panther_logs.public.crowdstrike_fdrevent
Window
1d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • ConsoleLogin
  • GetSessionToken
  • SignIn
field:"aws::eventName" kind:in
eventSourcein
  • signin.amazonaws.com
  • sts.amazonaws.com
field:"aws::eventSource" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

AWS Backdoor Administrative IAM Role Created

#
Severity
high
Time window
30h
Match by
p_alert_context.request_rolename
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Identifies when CreateRole and AttachAdminRolePolicy CloudTrail events occur in a short period of time. This sequence could indicate a potential security breach.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.Backdoor.Administrative.IAM.Role.Created.Group"
DisplayName: "AWS Backdoor Administrative IAM Role Created"
Enabled: false
Severity: High
Description: Identifies when CreateRole and AttachAdminRolePolicy CloudTrail events occur in a short period of time. This sequence could indicate a potential security breach.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
Reports:
  MITRE ATT&CK:
    - TA0007:T1078
Detection:
    - Group:
        - ID: CreateRole
          RuleID: AWS.IAM.CreateRole
        - ID: AttachAdminRolePolicy
          RuleID: AWS.IAM.AttachAdminRolePolicy
      MatchCriteria:
        field_name:
          - GroupID: CreateRole
            Match: p_alert_context.request_rolename
          - GroupID: AttachAdminRolePolicy
            Match: p_alert_context.request_rolename
      LookbackWindowMinutes: 1800
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.request_rolename. Each step needs one match unless a higher minimum is shown.

Stage 1: step CreateRole

References detection IAM Role Created.

Stage 2: step AttachAdminRolePolicy

References detection IAM Administrator Role Policy Attached.

AWS Bedrock Guardrail Updated or Deleted

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Bedrock, Generative AI Guardrails, AML.T0054, LLM Jailbreak, Impair Defenses: Disable or Modify Tools, Defense Evasion
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An Amazon Bedrock Guardrail was updated or deleted. Amazon Bedrock Guardrails are used to implement application-specific safeguards based on your use cases and responsible AI policies. Updating or deleting a guardrail can have security implications to your AI workloads.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

GUARDRAIL_EVENTS = {"DeleteGuardrail", "UpdateGuardrail"}


def rule(event):
    if (
        event.get("eventSource") == "bedrock.amazonaws.com"
        and event.get("eventName") in GUARDRAIL_EVENTS
        and aws_cloudtrail_success(event)
    ):
        return True
    return False


def title(event):
    user = event.udm("actor_user")
    guardrail = event.deep_get("requestParameters", "guardrailIdentifier")
    action = event.get("eventName").replace("Guardrail", "").lower()
    return f"User [{user}] {action}d Bedrock guardrail [{guardrail}]"


def severity(event):
    if event.get("eventName") == "UpdateGuardrail":
        return "LOW"
    return "DEFAULT"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_bedrock_guardrail_update_delete.py
RuleID: "AWS.Bedrock.GuardrailUpdateDelete"
DisplayName: "AWS Bedrock Guardrail Updated or Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Bedrock
  - Generative AI Guardrails
  - AML.T0054
  - LLM Jailbreak
  - "Impair Defenses: Disable or Modify Tools"
  - Defense Evasion
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.001 # Impair Defenses: Disable or Modify Tools
Description: >
  An Amazon Bedrock Guardrail was updated or deleted.
  Amazon Bedrock Guardrails are used to implement application-specific safeguards based on your use cases and responsible AI policies.
  Updating or deleting a guardrail can have security implications to your AI workloads.
Runbook: |
  Review the guardrail update or deletion to ensure that it was authorized and that it does not introduce security risks to your AI workloads.
  If the guardrail update or deletion was unauthorized, investigate the incident and take appropriate action.
  https://atlas.mitre.org/mitigations/AML.M0020
Reference: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_DeleteGuardrail.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is bedrock.amazonaws.com
  • eventName is one of DeleteGuardrail, UpdateGuardrail
  • errorCode is empty
  • errorMessage is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DeleteGuardrail
  • UpdateGuardrail
field:"aws::eventName" kind:in
eventSourceeq
  • bedrock.amazonaws.com
field:"aws::eventSource" kind:eq value:"bedrock.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
guardrailIdentifierrequestParameters.guardrailIdentifier

Response runbook

Review the guardrail update or deletion to ensure that it was authorized and that it does not introduce security risks to your AI workloads.

If the guardrail update or deletion was unauthorized, investigate the incident and take appropriate action.

https://atlas.mitre.org/mitigations/AML.M0020

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "4d482238-d0c5-4337-800f-d1ed79957fd4",
  "eventName": "UpdateGuardrail",
  "eventSource": "bedrock.amazonaws.com",
  "eventTime": "2025-01-21 17:39:10.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.09",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123123123123",
  "requestID": "4ebcfaab-52e6-4027-9307-dbfe671b1cdb",
  "requestParameters": {
    "guardrailIdentifier": "cmy5azq5koeo",
    "name": "HIDDEN_DUE_TO_SECURITY_REASONS"
  },
  "responseElements": {
    "guardrailArn": "arn:aws:bedrock:us-west-2:123123123123:guardrail/cmy5azq5koeo",
    "guardrailId": "cmy5azq5koeo",
    "updatedAt": "2025-01-21T17:39:10.379877250Z",
    "version": "DRAFT"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "123.123.123.123",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "bedrock.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
  "userIdentity": {
    "accessKeyId": "ASIAQWERQWERQWERQWER",
    "accountId": "123123123123",
    "arn": "arn:aws:sts::123123123123:assumed-role/DevAdmin/dr.evil",
    "principalId": "AROAQWERQWERQWERQWER:dr.evil",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-21T16:08:03Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123123123123",
        "arn": "arn:aws:iam::123123123123:role/aws-reserved/sso.amazonaws.com/us-west-2/DevAdmin",
        "principalId": "AROAQWERQWERQWERQWER",
        "type": "Role",
        "userName": "DevAdmin"
      }
    },
    "type": "AssumedRole"
  }
}

AWS Bedrock Model Invocation Abnormal Token Usage

#
Status
Experimental
Severity
informational
Log types
AWS.BedrockModelInvocation
Tags
AWS, Bedrock, Resource Hijacking
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Monitors for potential misuse or abuse of AWS Bedrock AI models by detecting abnormal token usage patterns and alerts when the total token usage exceeds the appropriate threshold for each different type of model.

MITRE ATT&CK coverage

Detection logic

def rule(event):
    # Only process InvokeModel and Converse operations
    if event.get("operation") not in ["InvokeModel", "Converse"]:
        return False

    # retrieve the necessary values from the logs
    token_usage = event.deep_get("output", "outputBodyJson", "usage", "totalTokens", default=0)
    model_id = event.get("modelId", default="")
    # Get the appropriate threshold for each model
    if "haiku" in model_id:
        threshold = 3000
    elif "sonnet" in model_id:
        threshold = 4000
    elif "opus" in model_id:
        threshold = 5000
    else:
        threshold = 4000  # default threshold
    # Check for abnormal token usage
    if token_usage > threshold:
        return True
    # Flag unusual token patterns (high usage with no actual output)
    output_tokens = event.deep_get("output", "outputBodyJson", "usage", "outputTokens", default=0)
    if token_usage > 1000 and output_tokens == 0:
        return True
    return False


def title(event):
    model_id = event.get("modelId", default="unknown")
    operation_name = event.get("operation", default="unknown")
    account_id = event.get("accountId", default="unknown")
    token_usage = event.deep_get("output", "outputBodyJson", "usage", "totalTokens", default=0)
    title_parts = [
        f"Abnormal token usage detected: {token_usage} tokens",
        f"Model: {model_id}",
        f"Operation: {operation_name}",
        f"Account: {account_id}",
    ]
    return " | ".join(title_parts)

Rule specification

AnalysisType: rule
Filename: aws_bedrockmodelinvocation_abnormaltokenusage.py
RuleID: "AWS.BedrockModelInvocation.AbnormalTokenUsage"
DisplayName: "AWS Bedrock Model Invocation Abnormal Token Usage"
Enabled: true
LogTypes:
    - AWS.BedrockModelInvocation
Tags:
    - AWS
    - Bedrock
    - Resource Hijacking
Status: Experimental
Severity: Info
Reports:
    MITRE ATT&CK:
        - TA0040:T1496.004
Description: Monitors for potential misuse or abuse of AWS Bedrock AI models by detecting abnormal token usage patterns and alerts when the total token usage exceeds the appropriate threshold for each different type of model.
Runbook: Verify the alert details by checking token usage, model ID, and account information to confirm unusual activity, examine user access patterns to identify potential credential compromise, and look for evidence of prompt injection, unusual repetition, or attempts to bypass usage limits. Apply stricter usage quotas to the affected account, block suspicious IP addresses, and enhance the guardrails that are in place.
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.impact.bedrock-invoke-model/
SummaryAttributes:
  - p_any_aws_account_ids
  - p_any_aws_arns
InlineFilters:
    - All: []

Stages and Predicates

Fires on AWS.BedrockModelInvocation events when all of the conditions below hold.

Condition

  • operation is one of InvokeModel, Converse
  • output.outputBodyJson.usage.totalTokens is greater than 1000
  • output.outputBodyJson.usage.outputTokens is 0

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Response runbook

Verify the alert details by checking token usage, model ID, and account information to confirm unusual activity, examine user access patterns to identify potential credential compromise, and look for evidence of prompt injection, unusual repetition, or attempts to bypass usage limits. Apply stricter usage quotas to the affected account, block suspicious IP addresses, and enhance the guardrails that are in place.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "111111111111",
  "identity": {
    "arn": "arn:aws:sts::111111111111:assumed-role/role_details/suspicious.user"
  },
  "input": {
    "inputBodyJson": {
      "messages": [
        {
          "content": [
            {
              "text": "I have a very suspicious question."
            }
          ],
          "role": "user"
        }
      ]
    },
    "inputContentType": "application/json",
    "inputTokenCount": 0
  },
  "modelId": "anthropic.claude-3-haiku-20240307-v1:0",
  "operation": "Converse",
  "output": {
    "outputBodyJson": {
      "metrics": {
        "latencyMs": 249
      },
      "output": {
        "message": {
          "content": [
            {
              "text": "You shouldn't ask this question"
            }
          ],
          "role": "assistant"
        }
      },
      "usage": {
        "inputTokens": 0,
        "outputTokens": 0,
        "totalTokens": 2000
      }
    },
    "outputContentType": "application/json",
    "outputTokenCount": 0
  },
  "region": "us-west-2",
  "requestId": "bb98d9a8-bd9a-47ca-976b-f165ef1f8b67",
  "schemaType": "ModelInvocationLog",
  "schemaVersion": "1.0",
  "timestamp": "2025-05-15 14:17:22.000000000"
}

AWS Bedrock Model Invocation GuardRail Intervened

#
Status
Experimental
Severity
informational
Log types
AWS.BedrockModelInvocation
Tags
AWS, Bedrock, Persistence, Manipulate AI Model
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when AWS Bedrock guardrail features have intervened during AI model invocations. It specifically monitors when an AI model request was blocked by Guardrails. This helps security teams identify when users attempt to generate potentially harmful or inappropriate content through AWS Bedrock models.

MITRE ATT&CK coverage

TacticTechniques
PersistenceNo specific technique
Credential AccessNo specific technique

Detection logic

def rule(event):
    if event.get("operation") != "InvokeModel" and event.get("operation") != "Converse":
        return False

    stop_reason = event.deep_get(
        "output", "outputBodyJSON", "stopReason", default="<UNKNOWN REASON>"
    )
    action_reason = event.deep_get(
        "output",
        "outputBodyJSON",
        "amazon-bedrock-trace",
        "guardrail",
        "actionReason",
        default="<UNKNOWN ACTION REASON>",
    )
    return stop_reason == "guardrail_intervened" or action_reason.startswith("Guardrail blocked")


def title(event):
    model_id = event.get("modelId")
    operation_name = event.get("operation")
    account_id = event.get("accountId")
    stop_reason = event.deep_get(
        "output", "outputBodyJSON", "stopReason", default="<UNKNOWN REASON>"
    )
    action_reason = event.deep_get(
        "output",
        "outputBodyJSON",
        "amazon-bedrock-trace",
        "guardrail",
        "actionReason",
        default="<UNKNOWN ACTION REASON>",
    )
    if action_reason == "<UNKNOWN ACTION REASON>":
        return (
            f"The model [{model_id}] was invoked with the operation [{operation_name}] "
            f"by the account [{account_id}]. Stop reason [{stop_reason}]."
        )
    if stop_reason == "<UNKNOWN REASON>":
        return (
            f"The model [{model_id}] was invoked with the operation [{operation_name}] "
            f"by the account [{account_id}]. Action reason [{action_reason}]."
        )
    # Handle the case when both values are known
    return (
        f"The model [{model_id}] was invoked with the operation [{operation_name}] "
        f"by the account [{account_id}]. Stop reason [{stop_reason}]. "
        f"Action reason [{action_reason}]."
    )

Rule specification

AnalysisType: rule
Filename: aws_bedrockmodelinvocation_guardrailintervened.py
RuleID: "AWS.BedrockModelInvocation.GuardRailIntervened"
DisplayName: "AWS Bedrock Model Invocation GuardRail Intervened"
Enabled: true
LogTypes:
    - AWS.BedrockModelInvocation
Tags:
    - AWS
    - Bedrock
    - Persistence
    - Manipulate AI Model
Status: Experimental
Severity: Info
Reports:
    MITRE ATT&CK:
        - TA0006:T0018.000
Description: Detects when AWS Bedrock guardrail features have intervened during AI model invocations. It specifically monitors when an AI model request was blocked by Guardrails. This helps security teams identify when users attempt to generate potentially harmful or inappropriate content through AWS Bedrock models.
Runbook: Confirm alert details by reviewing the model ID, operation name, account ID, and the specific guardrail intervention reasons provided in the alert description. Analyze the user prompts that triggered the guardrail by examining the Bedrock console logs for the associated requestId, looking for patterns of attempted model poisoning or prompt injection techniques. If suspicious activity is confirmed, temporarily restrict the access of the malicious actor to Bedrock services, preserve all evidence of the interaction, and escalate to the security team for further analysis of potential AI model manipulation attempts. https://atlas.mitre.org/mitigations/AML.M0005
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.impact.bedrock-invoke-model/, https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html
SummaryAttributes:
  - p_any_aws_account_ids
  - p_any_aws_arns
InlineFilters:
    - All: []

Stages and Predicates

Fires on AWS.BedrockModelInvocation events when all of the conditions below hold.

Condition

  • any of:
    • operation is InvokeModel
    • operation is Converse
  • any of:
    • output.outputBodyJSON.stopReason is guardrail_intervened
    • output.outputBodyJSON.amazon-bedrock-trace.guardrail.actionReason starts with Guardrail blocked

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
operationneConverseexcludes:operation field:"operation" value:"Converse"
operationneInvokeModelexcludes:operation field:"operation" value:"InvokeModel"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
modelId
operation
accountId
stopReasonoutput.outputBodyJSON.stopReason
actionReasonoutput.outputBodyJSON.amazon-bedrock-trace.guardrail.actionReason

Response runbook

Confirm alert details by reviewing the model ID, operation name, account ID, and the specific guardrail intervention reasons provided in the alert description. Analyze the user prompts that triggered the guardrail by examining the Bedrock console logs for the associated requestId, looking for patterns of attempted model poisoning or prompt injection techniques. If suspicious activity is confirmed, temporarily restrict the access of the malicious actor to Bedrock services, preserve all evidence of the interaction, and escalate to the security team for further analysis of potential AI model manipulation attempts. https://atlas.mitre.org/mitigations/AML.M0005

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "111111111111",
  "identity": {
    "arn": "arn:aws:sts::111111111111:assumed-role/role_details/suspicious.user"
  },
  "input": {
    "inputBodyJson": {
      "messages": [
        {
          "content": [
            {
              "text": "I have a very suspicious question."
            }
          ],
          "role": "user"
        }
      ]
    },
    "inputContentType": "application/json",
    "inputTokenCount": 0
  },
  "modelId": "anthropic.claude-3-haiku-20240307-v1:0",
  "operation": "Converse",
  "output": {
    "outputBodyJson": {
      "metrics": {
        "latencyMs": 249
      },
      "output": {
        "message": {
          "content": [
            {
              "text": "You shouldn't ask this question"
            }
          ],
          "role": "assistant"
        }
      },
      "stopReason": "guardrail_intervened",
      "usage": {
        "inputTokens": 0,
        "outputTokens": 0,
        "totalTokens": 0
      }
    },
    "outputContentType": "application/json",
    "outputTokenCount": 0
  },
  "region": "us-west-2",
  "requestId": "bb98d9a8-bd9a-47ca-976b-f165ef1f8b67",
  "schemaType": "ModelInvocationLog",
  "schemaVersion": "1.0",
  "timestamp": "2025-05-15 14:17:22.000000000"
}

AWS Bedrock Model Invocation Logging Configuration Deleted

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Bedrock, Impair Defenses: Impair Command History Logging, Defense Evastion
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An Amazon Bedrock Model Invocation Logging Configuration was deleted. Use model invocation logging to collect metadata, requests, and responses for all model invocations in your account. Deleting a model invocation logging configuration can have security implications to your AI workloads.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if (
        event.get("eventSource") == "bedrock.amazonaws.com"
        and event.get("eventName") == "DeleteModelInvocationLoggingConfiguration"
        and aws_cloudtrail_success(event)
    ):
        return True
    return False


def title(event):
    user = event.udm("actor_user")
    return f"User [{user}] deleted Bedrock model invocation logging configuration"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_bedrock_deletemodelinvocationloggingconfiguration.py
RuleID: "AWS.Bedrock.DeleteModelInvocationLoggingConfiguration"
DisplayName: "AWS Bedrock Model Invocation Logging Configuration Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Bedrock
  - "Impair Defenses: Impair Command History Logging"
  - Defense Evastion
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.003 # Impair Defenses: Impair Command History Logging
Description: >
  An Amazon Bedrock Model Invocation Logging Configuration was deleted.
  Use model invocation logging to collect metadata, requests, and responses for all model invocations in your account.
  Deleting a model invocation logging configuration can have security implications to your AI workloads.
Runbook: |
  Review the model invocation logging configuration deletion to ensure that it was authorized and that it does not introduce security risks to your AI workloads.
  If the model invocation logging configuration deletion was unauthorized, investigate the incident and take appropriate action.
Reference: https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is bedrock.amazonaws.com
  • eventName is DeleteModelInvocationLoggingConfiguration
  • errorCode is empty
  • errorMessage is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Review the model invocation logging configuration deletion to ensure that it was authorized and that it does not introduce security risks to your AI workloads.

If the model invocation logging configuration deletion was unauthorized, investigate the incident and take appropriate action.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "28773860-a4fd-47c7-a215-6f0e6e6e532f",
  "eventName": "DeleteModelInvocationLoggingConfiguration",
  "eventSource": "bedrock.amazonaws.com",
  "eventTime": "2025-01-21 17:49:47.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.09",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123123123123",
  "requestID": "7b9b25ca-be2d-4428-9793-0a677c32b823",
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "161.97.249.211",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "bedrock.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
  "userIdentity": {
    "accessKeyId": "ASIAQWERQWERQWERQWER",
    "accountId": "123123123123",
    "arn": "arn:aws:sts::123123123123:assumed-role/DevAdmin/dr.evil",
    "principalId": "AROAQWERQWERQWERQWER:dr.evil",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-21T16:08:03Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123123123123",
        "arn": "arn:aws:iam::123123123123:role/aws-reserved/sso.amazonaws.com/us-west-2/DevAdmin",
        "principalId": "AROAQWERQWERQWERQWER",
        "type": "Role",
        "userName": "DevAdmin"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CDE EC2 Volume Encryption

#
Severity
medium
Compliance
PCI 3.4
Resource types
AWS.EC2.Volume
Tags
AWS, PCI, Collection:Data From Local System
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that all EC2 volumes that contain CDE are encrypted. Be sure to configure CDE definitions before enabling this policy.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):

    return bool(resource["Encrypted"])

Rule specification

AnalysisType: policy
Filename: aws_ec2_cde_volume_encrypted.py
PolicyID: "AWS.EC2.CDEVolumeEncrypted"
DisplayName: "AWS CDE EC2 Volume Encryption"
Enabled: false
ResourceTypes:
  - AWS.EC2.Volume
Tags:
  - AWS
  - PCI
  - Collection:Data From Local System
Reports:
  PCI:
    - 3.4
  MITRE ATT&CK:
    - TA0009:T1005
Severity: Medium
Description: >
  This policy ensures that all EC2 volumes that contain CDE are encrypted.
  Be sure to configure CDE definitions before enabling this policy.
Runbook: >
  https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default-api
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html

Stages and Predicates

Flags AWS.EC2.Volume resources when the condition below holds.

Condition

  • Encrypted is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Encryptedis_not_null(no value, null check)excludes:Encrypted

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Encryptedis_null
  • (no value, null check)
field:"Encrypted" kind:is_null

Response runbook

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default-api

AWS CloudFormation Stack Drift

#
Severity
low
Resource types
AWS.CloudFormation.Stack
Tags
AWS, Operations, Panther, Impact:Resource Hijacking
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

A stack has drifted from its defined configuration.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_base_helpers import deep_get

# CloudFormation stacks tagged with "STACK=(name)" will be marked as passing.
IGNORE_STACK_TAGS = {
    "panther-bootstrap-gateway",
    "panther-cloud-security",
    "panther-core",
    "panther-log-analysis",
}


def policy(resource):
    if deep_get(resource, "DriftInformation", "StackDriftStatus") != "DRIFTED":
        return True

    # Some of Panther's own stacks contain Lambda functions which will always show as "drifted."
    # Panther stacks have a fixed "Stack" tag, even though the real stack name is dynamic.
    tags = resource["Tags"]
    if tags.get("Application") == "Panther" and tags.get("Stack") in IGNORE_STACK_TAGS:
        return True

    return False

Rule specification

AnalysisType: policy
Filename: aws_cloudformation_stack_drifted.py
PolicyID: "AWS.CloudFormation.Stack.Drifted"
DisplayName: "AWS CloudFormation Stack Drift"
Enabled: true
ResourceTypes:
  - AWS.CloudFormation.Stack
Reports:
  MITRE ATT&CK:
    - TA0040:T1496
Tags:
  - AWS
  - Operations
  - Panther
  - Impact:Resource Hijacking
Severity: Low
Description: >
  A stack has drifted from its defined configuration.
Runbook: |
  From the CloudFormation web console, look at the drifted resources for the failing stack.
  If the drift is expected, update the policy ignore list to exclude this stack.
  Otherwise, analyze CloudTrail logs to understand who changed the drifted resource(s) and ensure
  it was legitimate access.
Reference: https://amzn.to/2z8dDFW

Stages and Predicates

Flags AWS.CloudFormation.Stack resources when all of the conditions below hold.

Condition

  • DriftInformation.StackDriftStatus is DRIFTED
  • any of:
    • Tags.Application is not Panther
    • Tags.Stack is not one of panther-bootstrap-gateway, panther-cloud-security, panther-core, panther-log-analysis

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Tags.ApplicationeqPantherexcludes:Tags.Application field:"Tags.Application" value:"Panther"
Tags.Stackinpanther-bootstrap-gateway, panther-cloud-security, panther-core, panther-log-analysisexcludes:Tags.Stack
DriftInformation.StackDriftStatusneDRIFTEDexcludes:DriftInformation.StackDriftStatus field:"DriftInformation.StackDriftStatus" value:"DRIFTED"

Indicators

These rows show field, operator, and value matches.

Response runbook

From the CloudFormation web console, look at the drifted resources for the failing stack.

If the drift is expected, update the policy ignore list to exclude this stack.

Otherwise, analyze CloudTrail logs to understand who changed the drifted resource(s) and ensure

it was legitimate access.

AWS CloudFormation Stack IAM Service Role

#
Severity
informational
Resource types
AWS.CloudFormation.Stack
Tags
AWS, Operations, Panther
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

Associating IAM roles with CloudFormation stacks ensures least privilege when making changes to your account.

Detection logic

def policy(resource):
    # Ignore stack sets as this setting cannot be set on those
    if resource["Name"].startswith("StackSet-"):
        return True

    return resource["RoleARN"] is not None

Rule specification

AnalysisType: policy
Filename: aws_cloudformation_stack_uses_iam_role.py
PolicyID: "AWS.CloudFormation.Stack.UsesIAMServiceRole"
DisplayName: "AWS CloudFormation Stack IAM Service Role"
Enabled: true
ResourceTypes:
  - AWS.CloudFormation.Stack
Tags:
  - AWS
  - Operations
  - Panther
Severity: Info
Description: >
  Associating IAM roles with CloudFormation stacks ensures least privilege when making
  changes to your account.
Runbook: >
  Create a new IAM role to be assumable by the cloudformation service, and then update the
  current stack with the --role-arn argument.
Reference: https://amzn.to/2HAdfny

Stages and Predicates

Flags AWS.CloudFormation.Stack resources when all of the conditions below hold.

Condition

  • Name does not start with StackSet-
  • RoleARN is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Namestarts_withStackSet-excludes:Name field:"Name" value:"StackSet-"
RoleARNis_not_null(no value, null check)excludes:RoleARN

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RoleARNis_null
  • (no value, null check)
field:"RoleARN" kind:is_null

Response runbook

Create a new IAM role to be assumable by the cloudformation service, and then update the current stack with the --role-arn argument.

AWS CloudFormation Stack Termination Protection

#
Severity
informational
Resource types
AWS.CloudFormation.Stack
Tags
AWS, Operations, Panther, Impact:Resource Hijacking
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

Protects a CloudFormation stack from accidentally being deleted. If you attempt to delete a stack with termination protection enabled, the deletion fails and the stack, including its status, will remain unchanged.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

def policy(resource):
    # On nested stacks, this can only be set on the root stack
    if resource["RootId"] is not None:
        return True

    return resource["EnableTerminationProtection"] is True

Rule specification

AnalysisType: policy
Filename: aws_cloudformation_termination_protection.py
PolicyID: "AWS.CloudFormation.Stack.TerminationProtection"
DisplayName: "AWS CloudFormation Stack Termination Protection"
Enabled: true
ResourceTypes:
  - AWS.CloudFormation.Stack
Reports:
  MITRE ATT&CK:
    - TA0040:T1496
Tags:
  - AWS
  - Operations
  - Panther
  - Impact:Resource Hijacking
Severity: Info
Description: >
  Protects a CloudFormation stack from accidentally being deleted. If you attempt to delete a stack
  with termination protection enabled, the deletion fails and the stack, including its status,
  will remain unchanged.
Runbook: |
  This setting can only be enabled on stack creation. To add it to an existing stack, it must
  be re-created with the --enable-termination-protection flag.
Reference: https://amzn.to/2HAdfny

Stages and Predicates

Flags AWS.CloudFormation.Stack resources when all of the conditions below hold.

Condition

  • RootId is empty
  • EnableTerminationProtection is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EnableTerminationProtectioneqtrueexcludes:EnableTerminationProtection field:"EnableTerminationProtection" value:"true"
RootIdis_not_null(no value, null check)excludes:RootId

Indicators

These rows show field, operator, and value matches.

Response runbook

This setting can only be enabled on stack creation. To add it to an existing stack, it must

be re-created with the --enable-termination-protection flag.

AWS CloudTrail Account Discovery

#
Severity
informational
Entities
aws_account_ids, aws_arns, ip_addresses, trace_ids, usernames
Log types
AWS.CloudTrail
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

Adversaries may attempt to get a listing of accounts on a system or within an environment. This information can help adversaries determine which accounts exist to aid in follow-on behavior.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Detection logic

DISCOVERY_EVENTS = [
    "GetAlternateContact",
    "GetContactInformation",
    "PutAlternateContact",
    "PutContactInformation",
    "DescribeAccount",
]


def rule(event):
    return event.get("eventName") in DISCOVERY_EVENTS


def title(event):
    return (
        f"User [{event.deep_get('userIdentity', 'arn')}]"
        f"performed a [{event.get('eventName')}] "
        f"action in AWS account [{event.get('recipientAccountId')}]."
    )

Rule specification

AnalysisType: rule
Description: Adversaries may attempt to get a listing of accounts on a system or within an environment. This information can help adversaries determine which accounts exist to aid in follow-on behavior.
DisplayName: "AWS CloudTrail Account Discovery"
Enabled: true
Filename: aws_cloudtrail_account_discovery.py
Reference: https://attack.mitre.org/techniques/T1087/
Reports:
  MITRE ATT&CK:
    - TA0007:T1087
Severity: Info
CreateAlert: false
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.CloudTrail.Account.Discovery"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is one of GetAlternateContact, GetContactInformation, PutAlternateContact, PutContactInformation, DescribeAccount

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DescribeAccount
  • GetAlternateContact
  • GetContactInformation
  • PutAlternateContact
  • PutContactInformation
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
arnuserIdentity.arn
eventName
recipientAccountId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "0b51d284-19f7-42cf-a103-276602aeada5",
  "eventName": "DescribeAccount",
  "eventSource": "organizations.amazonaws.com",
  "eventTime": "2022-11-21 18:06:52",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123456789123"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123456789123:role/TestUser",
    "arn:aws:sts::123456789123:assumed-role/TestUser/test_123456789123"
  ],
  "p_any_ip_addresses": [
    "1.1.1.1"
  ],
  "p_any_trace_ids": [
    "ASIA3JHVJH35KB7LJHV2"
  ],
  "p_any_usernames": [
    "TestUser"
  ],
  "p_event_time": "2022-11-21 18:06:52",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-11-21 18:07:38.9",
  "p_row_id": "824956f0377f98908684d8de14d3d612",
  "p_source_id": "5f9f0f60-9c56-4027-b93a-8bab3019f0f1",
  "p_source_label": "Cloudtrail",
  "readOnly": true,
  "recipientAccountId": "123456789123",
  "requestID": "1c40241b-c59c-4d4a-8301-b612545f9c5c",
  "requestParameters": {
    "accountId": "123456789123"
  },
  "sourceIPAddress": "1.1.1.1",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "organizations.us-east-1.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "Boto3/1.26.2 Python/3.10.8 Linux/4.14.294-220.533.amzn2.x86_64 exec-env/AWS_ECS_FARGATE Botocore/1.29.2",
  "userIdentity": {
    "accessKeyId": "ASIA3JHVJH35KB7LJHV2",
    "accountId": "123456789123",
    "arn": "arn:aws:sts::123456789123:assumed-role/TestUser/test_123456789123",
    "principalId": "AR0A354LKJXC87G9XC89V:test_123456789123",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-11-21T18:06:36Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789123",
        "arn": "arn:aws:iam::123456789123:role/TestUser",
        "principalId": "AR0A354LKJXC87G9XC89V",
        "type": "Role",
        "userName": "TestUser"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail Attempt To Leave Org

#
Severity
informational
Compliance
Stratus Red Team aws.defense-evasion.organizations-leave
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, Defense Evasion, Impair Defenses, Disable or Modify Cloud Logs, Modify Cloud Resource Hierarchy
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when an actor attempts to remove an AWS account from an Organization. Security configurations are often defined at the organizational level. Leaving the organization can disrupt or totally shut down these controls.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    return event.get("eventName") == "LeaveOrganization"


def title(event: PantherEvent) -> str:
    account_name = event.get("recipientAccountId")
    actor = event.udm("actor_user")
    # Return a more informative message if the attempt was unsuccessful
    if not aws_cloudtrail_success(event):
        return f"Failed attempt to remove {account_name} from your AWS organization by {actor}"
    return f"Account {account_name} has been removed from your AWS organization by {actor}"


def severity(event: PantherEvent) -> str:
    # Downgrade to HIGH if attempt is unsuccessful
    if not aws_cloudtrail_success(event):
        return "HIGH"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_attempt_to_leave_org.py
RuleID: "AWS.CloudTrail.AttemptToLeaveOrg"
DisplayName: AWS CloudTrail Attempt To Leave Org
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info  
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable or Modify Cloud Logs
    - TA0005:T1666 # Defense Evasion: Modify Cloud Resource Hierarchy

  Stratus Red Team:
    - aws.defense-evasion.organizations-leave
Description: >
  Detects when an actor attempts to remove an AWS account from an Organization. Security
  configurations are often defined at the organizational level. Leaving the organization can
  disrupt or totally shut down these controls.
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.defense-evasion.organizations-leave/
Runbook: |
  Determine if the attempt was successful. Monitor and potentially suspect the user account which 
  attempted the action. Determine if the root account is compromised.
SummaryAttributes:
  - p_any_ip_addresses
  - p_any_aws_account_ids
Tags:
  - AWS CloudTrail
  - Defense Evasion
  - Impair Defenses
  - Disable or Modify Cloud Logs
  - Modify Cloud Resource Hierarchy

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is LeaveOrganization

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Determine if the attempt was successful. Monitor and potentially suspect the user account which

attempted the action. Determine if the root account is compromised.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:sts::111122223333:assumed-role/SampleRole/SampleSession is not authorized to perform: organizations:LeaveOrganization on resource: * because no identity-based policy allows the organizations:LeaveOrganization action",
  "eventCategory": "Management",
  "eventID": "f52c1358-4ddb-4453-a676-3f4dbc64d713",
  "eventName": "LeaveOrganization",
  "eventSource": "organizations.amazonaws.com",
  "eventTime": "2025-01-20 15:59:33.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.09",
  "managementEvent": true,
  "p_event_time": "2025-01-20 15:59:33.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-20 16:05:54.322564138",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "67dce4b9-c7d1-4c91-a686-d34bbd5365eb",
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "organizations.us-east-1.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/SampleSession",
    "principalId": "SAMPLE_PRINCIPAL_ID:SampleSession",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-20T15:59:30Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail CloudWatch Logs

#
Severity
low
Compliance
CIS 2.4
Resource types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

CloudTrail supports sending data and management events to CloudWatch Logs. This setup can be used for real-time processing of all CloudTrail data events.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

import datetime

from panther_base_helpers import deep_get, resolve_timestamp_string

MAX_TIME_BETWEEN_LOGS = datetime.timedelta(hours=24)


def policy(resource):
    # Check if a CloudWatch Logs Group has been set, and received at least one log
    if not (
        resource.get("CloudWatchLogsLogGroupArn")
        and deep_get(resource, "Status", "LatestCloudWatchLogsDeliveryTime")
    ):
        return False

    # Check if the last log sent is within the allowable timeframe
    last_log_time = resolve_timestamp_string(
        deep_get(resource, "Status", "LatestCloudWatchLogsDeliveryTime")
    )

    if not last_log_time:
        return True
    return (datetime.datetime.utcnow() - last_log_time) <= MAX_TIME_BETWEEN_LOGS

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_cloudwatch_logs.py
PolicyID: "AWS.CloudTrail.CloudWatchLogs"
DisplayName: "AWS CloudTrail CloudWatch Logs"
Enabled: false
ResourceTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 2.4
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Low
Description: >
  CloudTrail supports sending data and management events to CloudWatch Logs. This setup can be
  used for real-time processing of all CloudTrail data events.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-trails-integrated-with-cloudwatch-logs
Reference: >
  https://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html

Stages and Predicates

Flags AWS.CloudTrail resources when any of the conditions below holds.

Condition

  • any of:
    • CloudWatchLogsLogGroupArn is empty
    • Status.LatestCloudWatchLogsDeliveryTime is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CloudWatchLogsLogGroupArnis_not_null(no value, null check)excludes:CloudWatchLogsLogGroupArn
Status.LatestCloudWatchLogsDeliveryTimeis_not_null(no value, null check)excludes:Status.LatestCloudWatchLogsDeliveryTime

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-trails-integrated-with-cloudwatch-logs

AWS CloudTrail Least Privilege Access

#
Severity
medium
Resource types
AWS.IAM.Group
Tags
AWS, Configuration Required, IAM, Panther, Defense Evasion:Impair Defenses
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

Users with permissions to disable or reconfigure CloudTrail should be limited.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

CLOUDTRAIL_MANAGED_POLICY_ARN = "arn:aws:iam::aws:policy/AWSCloudTrailFullAccess"
MAX_ADMIN_USERS = 2


def policy(resource):
    return (
        CLOUDTRAIL_MANAGED_POLICY_ARN in resource["ManagedPolicyARNs"]
        and len(resource["Users"]) <= MAX_ADMIN_USERS
    )

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_least_privilege.py
PolicyID: "AWS.CloudTrail.LeastPrivilege"
DisplayName: "AWS CloudTrail Least Privilege Access"
Enabled: false
ResourceTypes:
  - AWS.IAM.Group
Tags:
  - AWS
  - Configuration Required
  - IAM
  - Panther
  - Defense Evasion:Impair Defenses
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Medium
Description: >
  Users with permissions to disable or reconfigure CloudTrail should be limited.
Runbook: >
  Remove the AWSCloudTrailFullAccess managed IAM policy from all but one user in the account.
Reference: https://amzn.to/2ZEUrKm

Stages and Predicates

Flags AWS.IAM.Group resources when any of the conditions below holds.

Condition

  • any of:
    • ManagedPolicyARNs does not contain arn:aws:iam::aws:policy/AWSCloudTrailFullAccess
    • Users has length greater than 2

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ManagedPolicyARNscontainsarn:aws:iam::aws:policy/AWSCloudTrailFullAccessexcludes:ManagedPolicyARNs field:"ManagedPolicyARNs" value:"arn:aws:iam::aws:policy/AWSCloudTrailFullAccess"
Userslength_compare2excludes:Users field:"Users" value:"2"

Response runbook

Remove the AWSCloudTrailFullAccess managed IAM policy from all but one user in the account.

AWS CloudTrail Log Encryption

#
Severity
medium
Compliance
CIS 2.7
Resource types
AWS.CloudTrail
Tags
AWS, Data Protection, Discovery:Cloud Service Discovery
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that CloudTrail Logs are encrypted at rest with customer managed KMS key.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

def policy(resource):
    return bool(resource["KmsKeyId"])

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_log_encryption.py
PolicyID: "AWS.CloudTrail.LogEncryption"
DisplayName: "AWS CloudTrail Log Encryption"
Enabled: true
ResourceTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Data Protection
  - Discovery:Cloud Service Discovery
Reports:
  CIS:
    - 2.7
  MITRE ATT&CK:
    - TA0007:T1526
Severity: Medium
Description: >
  This policy validates that CloudTrail Logs are encrypted at rest with customer managed KMS key.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-logs-encrypted-using-kms-cmk
Reference: >
  https://docs.aws.amazon.com/awscloudtrail/latest/userguide/encrypting-cloudtrail-log-files-with-aws-kms.html

Stages and Predicates

Flags AWS.CloudTrail resources when the condition below holds.

Condition

  • KmsKeyId is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
KmsKeyIdis_not_null(no value, null check)excludes:KmsKeyId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
KmsKeyIdis_null
  • (no value, null check)
field:"KmsKeyId" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-logs-encrypted-using-kms-cmk

AWS CloudTrail Log Validation

#
Severity
medium
Compliance
CIS 2.2; PCI 10.5.5
Resource types
AWS.CloudTrail
Tags
AWS, Data Protection, Defense Evasion:Impair Defenses
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

This policy ensures that CloudTrail logs have file integrity validation enabled.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def policy(resource):
    # Explicit check for True as the value may be None, and we want to return a bool not a NoneType
    return resource["LogFileValidationEnabled"] is True

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_log_validation.py
PolicyID: "AWS.CloudTrail.LogValidation"
DisplayName: "AWS CloudTrail Log Validation"
Enabled: true
ResourceTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Data Protection
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 2.2
  PCI:
    - 10.5.5
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Medium
Description: >
  This policy ensures that CloudTrail logs have file integrity validation enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-log-validation-enabled
Reference: >
  https://amzn.to/2MMgE6W

Stages and Predicates

Flags AWS.CloudTrail resources when the condition below holds.

Condition

  • LogFileValidationEnabled is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LogFileValidationEnabledeqtrueexcludes:LogFileValidationEnabled field:"LogFileValidationEnabled" value:"true"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-log-validation-enabled

AWS CloudTrail Management Events Enabled

#
Severity
high
Compliance
CIS 2.1; PCI 10.5.4
Resource types
AWS.CloudTrail.Meta
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that at least one CloudTrail has management (control plane) operations logged.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def policy(resource):
    # pylint: disable=R1260
    if not resource.get("Trails"):
        return False

    if not any(
        [resource.get("GlobalEventSelectors"), resource.get("GlobalAdvancedEventSelectors")]
    ):
        return False

    if resource.get("GlobalEventSelectors"):
        for selector in resource.get("GlobalEventSelectors", [{}]):
            if selector.get("IncludeManagementEvents") and selector.get("ReadWriteType") == "All":
                return True

    if resource.get("GlobalAdvancedEventSelectors"):
        for advanced_selector in resource.get("GlobalAdvancedEventSelectors", [{}]):
            management_present = False
            readonly_present = False
            for field_selector in advanced_selector.get("FieldSelectors", [{}]):
                if field_selector.get("Field") == "eventCategory":
                    event_categories = field_selector.get("Equals", [])
                    if "Management" in event_categories:
                        management_present = True
                if field_selector.get("Field") == "readOnly":
                    readonly_present = True
            if all([management_present, not readonly_present]):
                return True
    return False

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_enabled.py
PolicyID: "AWS.CloudTrail.Enabled"
DisplayName: "AWS CloudTrail Management Events Enabled"
Enabled: true
ResourceTypes:
  - AWS.CloudTrail.Meta
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 2.1
  PCI:
    - 10.5.4
  MITRE ATT&CK:
    - TA0005:T1562
Severity: High
Description: >
  This policy ensures that at least one CloudTrail has
  management (control plane) operations logged.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-enabled-in-all-regions
Reference: >
  https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html

Stages and Predicates

Flags AWS.CloudTrail.Meta resources when any of the conditions below holds.

Condition

  • any of:
    • Trails is empty
    • all of:
      • GlobalEventSelectors is empty
      • GlobalAdvancedEventSelectors is empty
    • all of:
      • any of:
        • GlobalEventSelectors is empty
        • no element of GlobalEventSelectors matches all of:
          • GlobalEventSelectors.IncludeManagementEvents is present
          • GlobalEventSelectors.ReadWriteType is All
      • GlobalAdvancedEventSelectors is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
GlobalEventSelectorsarray_any(no value, null check)excludes:GlobalEventSelectors
GlobalEventSelectorsis_not_null(no value, null check)excludes:GlobalEventSelectors
GlobalAdvancedEventSelectorsis_not_null(no value, null check)excludes:GlobalAdvancedEventSelectors
Trailsis_not_null(no value, null check)excludes:Trails

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-enabled-in-all-regions

AWS CloudTrail Password Policy Discovery

#
Severity
informational
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This detection looks for *AccountPasswordPolicy events in AWS CloudTrail logs. If these events occur in a short period of time from the same ARN, it could constitute Password Policy reconnaissance.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

PASSWORD_DISCOVERY_EVENTS = [
    "GetAccountPasswordPolicy",
    "UpdateAccountPasswordPolicy",
    "PutAccountPasswordPolicy",
]


def rule(event):
    service_event = event.get("eventType") == "AwsServiceEvent"
    return event.get("eventName") in PASSWORD_DISCOVERY_EVENTS and not service_event


def title(event):
    user_arn = event.deep_get("useridentity", "arn", default="<MISSING_ARN>")
    return f"Password Policy Discovery detected in AWS CloudTrail from [{user_arn}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: This detection looks for *AccountPasswordPolicy events in AWS CloudTrail logs. If these events occur in a short period of time from the same ARN, it could constitute Password Policy reconnaissance.
DisplayName: AWS CloudTrail Password Policy Discovery
Enabled: true
Filename: aws_cloudtrail_password_policy_discovery.py
Reports:
  MITRE ATT&CK:
    - TA0007:T1201
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html
Severity: Info
DedupPeriodMinutes: 30
LogTypes:
  - AWS.CloudTrail
RuleID: AWS.CloudTrail.Password.Policy.Discovery
Threshold: 2

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is one of GetAccountPasswordPolicy, UpdateAccountPasswordPolicy, PutAccountPasswordPolicy
  • eventType is not AwsServiceEvent
Alert cadence
alerts after 2 matches within 30m

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • GetAccountPasswordPolicy
  • PutAccountPasswordPolicy
  • UpdateAccountPasswordPolicy
field:"aws::eventName" kind:in
eventTypene
  • AwsServiceEvent
field:"eventType" kind:ne value:"AwsServiceEvent"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuseridentity.arn

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsregion": "us-east-1",
  "eventcategory": "Management",
  "eventid": "1808ca3b-4311-4b48-9d1f-21061acb2329",
  "eventname": "GetAccountPasswordPolicy",
  "eventsource": "iam.amazonaws.com",
  "eventtime": "2023-01-10 23:10:06",
  "eventtype": "AwsApiCall",
  "eventversion": "1.08",
  "managementevent": true,
  "useridentity": {
    "arn": "arn:aws:test_arn"
  }
}

AWS CloudTrail Password Spraying

#
Status
Experimental
Severity
medium
Group by
awsRegion, recipientAccountId
Log types
AWS.CloudTrail
Tags
Initial Access:Valid Accounts
Source
github.com/panther-labs/panther-analysis

Detects password spraying attacks by alerting when more than 9 distinct usernames fail to authenticate to the AWS console from the same account and region within 60 minutes.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventType") != "AwsConsoleSignIn":
        return False
    return event.deep_get("responseElements", "ConsoleLogin", default="") == "Failure"


def title(event):
    account = event.get("recipientAccountId", "Unknown Account")
    region = event.get("awsRegion", "Unknown Region")
    return f"Password Spraying Detected in AWS Account [{account}] Region [{region}]"


def dedup(event):
    account = event.get("recipientAccountId", "")
    region = event.get("awsRegion", "")
    return f"{account}:{region}"


def unique(event):
    return event.deep_get("userIdentity", "userName") or None


def severity(event):
    if event.deep_get("userIdentity", "type", default="") == "Root":
        return "HIGH"
    return "DEFAULT"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_password_spraying.py
RuleID: "AWS.CloudTrail.PasswordSpraying"
DisplayName: "AWS CloudTrail Password Spraying"
Status: Experimental
Enabled: false
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 10
LogTypes:
  - AWS.CloudTrail
Description: >
  Detects password spraying attacks by alerting when more than 9 distinct usernames
  fail to authenticate to the AWS console from the same account and region within 60 minutes.
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
Tags:
  - Initial Access:Valid Accounts
Runbook: |
  1. Query CloudTrail for all ConsoleLogin events in the 2 hours around this alert grouped by sourceIPAddress to identify the origin of the spray targeting recipientAccountId
  2. Check if any of the targeted usernames subsequently had a successful ConsoleLogin from any sourceIPAddress in the 6 hours after the alert
  3. Find other alerts for this recipientAccountId or any of the targeted usernames in the past 7 days to determine if this is part of a broader campaign

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventType is AwsConsoleSignIn
  • responseElements.ConsoleLogin is Failure
Alert cadence
alerts after 10 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

1. Query CloudTrail for all ConsoleLogin events in the 2 hours around this alert grouped by sourceIPAddress to identify the origin of the spray targeting recipientAccountId

2. Check if any of the targeted usernames subsequently had a successful ConsoleLogin from any sourceIPAddress in the 6 hours after the alert

3. Find other alerts for this recipientAccountId or any of the targeted usernames in the past 7 days to determine if this is part of a broader campaign

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventType": "AwsConsoleSignIn",
  "recipientAccountId": "111122223333",
  "responseElements": {
    "ConsoleLogin": "Failure"
  },
  "userIdentity": {
    "type": "IAMUser",
    "userName": "alice"
  }
}

AWS Cloudtrail Region Enabled

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, CloudTrail, Region
Reference
permiso.io
Source
github.com/panther-labs/panther-analysis

Threat actors who successfully compromise a victim's AWS account, whether through stolen credentials, exposed access keys, exploited IAM misconfigurations, vulnerabilities in third-party applications, or the absence of Multi-Factor Authentication (MFA), can exploit unused regions as safe zones for malicious activities. These regions are often overlooked in monitoring and security setups, making them an attractive target for attackers to operate undetected.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return event.get("eventName") == "EnableRegion"


def title(event):
    return (
        f"AWS CloudTrail region [{event.deep_get('requestParameters', 'RegionName')}] "
        f"enabled by user [{event.udm('actor_user')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: AWS Cloudtrail Region Enabled
Enabled: true
Filename: aws_cloudtrail_region_enabled.py
RuleID: "AWS.CloudTrail.EnableRegion"
Severity: Medium
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - CloudTrail
  - Region
Reports:
    MITRE ATT&CK:
        - TA0005:T1535  # Unused/Unsupported Cloud Regions
Description: >
  Threat actors who successfully compromise a victim's AWS account, whether through stolen credentials, 
  exposed access keys, exploited IAM misconfigurations, vulnerabilities in third-party applications, 
  or the absence of Multi-Factor Authentication (MFA), can exploit unused regions as safe zones 
  for malicious activities. These regions are often overlooked in monitoring and security setups, 
  making them an attractive target for attackers to operate undetected.
Runbook: |
  Validate whether enabling the new region was authorized.  
  Revoke user privileges, review the newly enabled region for malicious activity, and disable the region.
Reference: https://permiso.io/blog/how-threat-actors-leverage-unsupported-cloud-regions

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is EnableRegion

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
RegionNamerequestParameters.RegionName
actor_user

Response runbook

Validate whether enabling the new region was authorized.

Revoke user privileges, review the newly enabled region for malicious activity, and disable the region.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 0,
    "bytesTransferredOut": 0
  },
  "awsRegion": "us-east-1",
  "eventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "eventName": "EnableRegion",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2023-10-01T12:34:56Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "EXAMPLE123456789",
  "requestParameters": {
    "RegionName": "us-west-2"
  },
  "resources": [],
  "responseElements": null,
  "sharedEventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "aws-sdk-go/1.15.12 (go1.12.6; linux; amd64)",
  "userIdentity": {
    "accessKeyId": "EXAMPLEKEY",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/Alice",
    "principalId": "EXAMPLE",
    "type": "IAMUser",
    "userName": "Alice"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

AWS CloudTrail Retention Lifecycle Too Short

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS, Cloudtrail, Defense Evasion, Impair Defenses, Disable or Modify Cloud Logs, Defense Evasion:Impair Defenses, Security Control
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when an S3 bucket containing CloudTrail logs has been modified to delete data after a short period of time.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_base_helpers import deep_get

# Use this to record the names of your S3 buckets that have cloudtrail logs
#   If a bucket name isn't mentioned here, we still make a best guess as to whether or not it
#   contains CloudTrail data, but the confidence rating will be lower, and so will the severity
CLOUDTRAIL_BUCKETS = ("example_cloudtrail_bucket_name",)

# This is the minimum length fo time CloudTrail logs should remain in an S3 bucket.
#   We set this to 7 initially, since this is the recommended amount of time logs ingested by
#   Panther should remain available. You can modify this if you wish.
CLOUDTRAIL_MINIMUM_STORAGE_PERIOD_DAYS = 7


def rule(event):
    # Only alert for successful PutBucketLifecycle events
    if not (aws_cloudtrail_success(event) and event.get("eventName") == "PutBucketLifecycle"):
        return False

    # Exit out if the bucket doesn't have cloudtrail logs
    #   We check this be either comparing the bucket name to a list of buckets the user knows has
    #   CT logs, or by heuristically looking at the name and guessing whether it likely has CT logs
    bucket_name = event.deep_get("requestParameters", "bucketName")
    if not bucket_name or (
        not is_cloudtrail_bucket(bucket_name) and not guess_is_cloudtrail_bucket(bucket_name)
    ):
        return False

    # Don't alert if the Rule status is disabled
    lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule")
    if lifecycle.get("Status") != "Enabled":
        return False

    # Alert if the lifecycle period is short
    duration = deep_get(lifecycle, "Expiration", "Days", default=0)
    return duration < CLOUDTRAIL_MINIMUM_STORAGE_PERIOD_DAYS


def title(event):
    bucket_name = event.deep_get("requestParameters", "bucketName", default="<UNKNOWN S3 BUCKET>")
    lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule")
    duration = deep_get(lifecycle, "Expiration", "Days", default=0)
    rule_id = lifecycle.get("ID", "<UNKNOWN RULE ID>")
    account = event.deep_get("userIdentity", "accountId", default="<UNKNOWN_AWS_ACCOUNT>")
    return (
        f"S3 Bucket {bucket_name} in account {account} "
        f"has new rule {rule_id} set to delete CloudTrail logs after "
        f"{duration} day{'s' if duration != 1 else ''}"
    )


def severity(event):
    # Return lower severity if we aren't positive this bucket has cloudtrail logs.
    bucket_name = event.deep_get("requestParameters", "bucketName")
    if not is_cloudtrail_bucket(bucket_name):
        return "LOW"
    return "DEFAULT"


def alert_context(event):
    context = aws_rule_context(event)

    # Add name of S3 bucket, Rule ID, and expiration duration to context
    bucket_name = event.deep_get("requestParameters", "bucketName", default="<UNKNOWN S3 BUCKET>")
    lifecycle = event.deep_get("requestParameters", "LifecycleConfiguration", "Rule")
    duration = deep_get(lifecycle, "Expiration", "Days", default=0)
    rule_id = lifecycle.get("ID", "<UNKNOWN RULE ID>")
    context.update(
        {
            "bucketName": bucket_name,
            "lifecycleRuleID": rule_id,
            "lifecycleRuleDurationDays": duration,
        }
    )

    return context


def is_cloudtrail_bucket(bucket_name: str) -> bool:
    """Returns True if the bucket is known to contain CloudTrail logs."""
    return bucket_name in CLOUDTRAIL_BUCKETS


def guess_is_cloudtrail_bucket(bucket_name: str) -> bool:
    """Takes a best guess at whether a bucket contains CloudTrail logs or not."""
    # Maybe one day, this check will get more complex
    return "trail" in bucket_name.lower()

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_short_lifecycle.py
RuleID: "AWS.CloudTrail.ShortLifecycle"
DisplayName: "AWS CloudTrail Retention Lifecycle Too Short"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.008 # Defense Evasion: Disable or Modify Cloud Logs
Description: "Detects when an S3 bucket containing CloudTrail logs has been modified to delete data after a short period of time."
Reference: 
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.defense-evasion.cloudtrail-lifecycle-rule/
Runbook: Verify whether the bucket in question contains CloudTrail data, and if so, why the lifecycle was changed. Potentally add a filter for this bucket to prevent future false positives.
Tags:
  - AWS
  - Cloudtrail
  - Defense Evasion
  - Impair Defenses
  - Disable or Modify Cloud Logs
  - Defense Evasion:Impair Defenses
  - Security Control

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is PutBucketLifecycle
  • requestParameters.bucketName is present
  • any of:
    • requestParameters.bucketName is one of example_cloudtrail_bucket_name
    • requestParameters.bucketName contains trail (case-insensitive)
  • requestParameters.LifecycleConfiguration.Rule.Status is Enabled
  • requestParameters.LifecycleConfiguration.Rule.Expiration.Days is less than 7

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
requestParameters.bucketNamecontainstrailexcludes:requestParameters.bucketName field:"requestParameters.bucketName" value:"trail"
requestParameters.bucketNameeqexample_cloudtrail_bucket_nameexcludes:requestParameters.bucketName field:"requestParameters.bucketName" value:"example_cloudtrail_bucket_name"
requestParameters.bucketNameis_null(no value, null check)excludes:requestParameters.bucketName
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucketNamerequestParameters.bucketName
accountIduserIdentity.accountId
IDrequestParameters.LifecycleConfiguration.Rule.ID
DaysrequestParameters.LifecycleConfiguration.Rule.Expiration.Days

Response runbook

Verify whether the bucket in question contains CloudTrail data, and if so, why the lifecycle was changed. Potentally add a filter for this bucket to prevent future false positives.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "AuthenticationMethod": "AuthHeader",
    "CipherSuite": "TLS_AES_128_GCM_SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 249,
    "bytesTransferredOut": 0,
    "x-amz-id-2": "vf6Ehji6uE8ET3EJvRpIQva7eul9KSAUWVlf87sIKBmLQ0HgdGbswZiHYlVvSr1FdP5DiZze4DRZRAFppKpD4A=="
  },
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "e1ea136d-f372-4cd5-be5f-f317fc80214a",
  "eventName": "PutBucketLifecycle",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-11-25 22:00:58.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "p_event_time": "2024-11-25 22:00:58.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-11-25 22:05:54.357893092",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "4XRNRGRFH6RES629",
  "requestParameters": {
    "Host": "sample-cloudtrail-bucket-name.s3.us-west-2.amazonaws.com",
    "LifecycleConfiguration": {
      "Rule": {
        "Expiration": {
          "Days": 1
        },
        "Filter": {
          "Prefix": "*"
        },
        "ID": "nuke-cloudtrail-logs-after-1-day",
        "Status": "Enabled"
      },
      "xmlns": "http://s3.amazonaws.com/doc/2006-03-01/"
    },
    "bucketName": "sample-cloudtrail-bucket-name",
    "lifecycle": ""
  },
  "resources": [
    {
      "accountId": "111122223333",
      "arn": "arn:aws:s3:::sample-cloudtrail-bucket-name",
      "type": "AWS::S3::Bucket"
    }
  ],
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "sample-cloudtrail-bucket-name.s3.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "[sample-user-agent]",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins",
    "principalId": "SAMPLE_PRINCIPAL_ID:leroy.jenkins",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-11-25T16:53:42Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail S3 Bucket Access Logging

#
Severity
medium
Compliance
CIS 2.6
Resource types
AWS.CloudTrail
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the bucket receiving CloudTrail Logs is configured with S3 Access Logging. This audits all creation, modification, or deletion to CloudTrail audit logs.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_aws_helpers import BadLookup, resource_lookup


def policy(resource):
    bucket_arn = "arn:aws:s3:::" + resource["S3BucketName"]
    try:
        bucket = resource_lookup(bucket_arn)
    except BadLookup:
        return True

    return bucket["LoggingPolicy"] is not None

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_s3_bucket_access_logging.py
PolicyID: "AWS.CloudTrail.S3Bucket.AccessLogging"
DisplayName: "AWS CloudTrail S3 Bucket Access Logging"
Enabled: true
ResourceTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  CIS:
    - 2.6
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Medium
Description: >
  This policy validates that the bucket receiving CloudTrail Logs is configured with
  S3 Access Logging. This audits all creation, modification, or deletion to CloudTrail audit logs.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-s3-bucket-has-access-logging-enabled
Reference: >
  https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html

# Unit testing not supported for policies that might network calls

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-s3-bucket-has-access-logging-enabled

AWS CloudTrail S3 Bucket Public

#
Severity
high
Compliance
CIS 2.3
Resource types
AWS.CloudTrail
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that CloudTrail S3 buckets are not publicly accessible.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_aws_helpers import BadLookup, aws_regions, resource_lookup
from panther_base_helpers import deep_get

BAD_PERMISSIONS = {
    "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
    "http://acs.amazonaws.com/groups/global/AllUsers",
}

# docs.aws.amazon.com/codepipeline/latest/userguide/reference-ct-placeholder-buckets.html
EXCLUDED_BUCKET_NAMES = {
    f"codepipeline-cloudtrail-placeholder-bucket-{region}" for region in aws_regions()
}


def policy(resource):
    bucket_arn = "arn:aws:s3:::" + resource["S3BucketName"]

    try:
        bucket = resource_lookup(bucket_arn)
    except BadLookup:
        return True

    for grant in bucket["Grants"] or []:
        if deep_get(grant, "Grantee", "URI") in BAD_PERMISSIONS and not any(
            bucket_name in bucket_arn for bucket_name in EXCLUDED_BUCKET_NAMES
        ):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_cloudtrail_s3_bucket_public.py
PolicyID: "AWS.CloudTrail.S3Bucket.Public"
DisplayName: "AWS CloudTrail S3 Bucket Public"
Enabled: true
ResourceTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  CIS:
    - 2.3
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  This policy validates that CloudTrail S3 buckets are not publicly accessible.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-logs-s3-bucket-not-publicly-accessible
Reference: >
  https://docs.aws.amazon.com/AmazonS3/latest/user-guide/block-public-access.html

# Unit testing not supported for policies that might network calls

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-cloudtrail-logs-s3-bucket-not-publicly-accessible

AWS CloudTrail SES Check Identity Verifications

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS CloudTrail
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    return event.get("eventName") == "GetIdentityVerificationAttributes"


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["accountRegion"] = f"{event.get('recipientAccountId')}_{event.get('eventRegion')}"
    return context

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_ses_check_identity_verifications.py
RuleID: "AWS.CloudTrail.SES.CheckIdentityVerifications"
DisplayName: AWS CloudTrail SES Check Identity Verifications
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
CreateAlert: false
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.discovery.ses-enumerate/
Tags:
  - AWS CloudTrail

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is GetIdentityVerificationAttributes

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNameeq
  • GetIdentityVerificationAttributes
field:"aws::eventName" kind:eq value:"GetIdentityVerificationAttributes"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "SignatureVersion": "4"
  },
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "05197e93-992f-4476-899a-a6f53c9a462c",
  "eventName": "GetIdentityVerificationAttributes",
  "eventSource": "ses.amazonaws.com",
  "eventTime": "2025-01-20 16:52:14.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-01-20 16:52:14.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-20 17:00:54.142940079",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "e3b6e034-97ce-4d43-a7d2-1e718f3ebf32",
  "requestParameters": {
    "identities": [
      "acme.com",
      "bobson.dugnutt@acme.com",
      "sleve.mcdichael@yahoo.com"
    ]
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "email.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-20T15:58:59Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail SES Check Send Quota

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, SES
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect when someone checks how many emails can be delivered via SES. Excludes automated checks from AWS Trusted Advisor to reduce false positives.

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    if event.get("eventName") == "GetSendQuota":
        # Exclude AWS Trusted Advisor automated checks
        role_name = event.deep_get("userIdentity", "sessionContext", "sessionIssuer", "userName")
        if role_name == "AWSServiceRoleForTrustedAdvisor":
            return False
        return True
    return False


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["accountRegion"] = f"{event.get('recipientAccountId')}_{event.get('eventRegion')}"
    return context

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_ses_check_send_quota.py
RuleID: "AWS.CloudTrail.SES.CheckSendQuota"
DisplayName: AWS CloudTrail SES Check Send Quota
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
CreateAlert: false
Description: >
  Detect when someone checks how many emails can be delivered via SES.
  Excludes automated checks from AWS Trusted Advisor to reduce false positives. 
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.discovery.ses-enumerate/
Tags:
  - AWS CloudTrail
  - SES

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is GetSendQuota
  • userIdentity.sessionContext.sessionIssuer.userName is not AWSServiceRoleForTrustedAdvisor

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "SignatureVersion": "4"
  },
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "141c7b0f-3ec3-40bd-b551-5a33d1a794b4",
  "eventName": "GetSendQuota",
  "eventSource": "ses.amazonaws.com",
  "eventTime": "2025-01-20 16:52:14.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-01-20 16:52:14.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-20 17:00:54.217261818",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "6495a102-3900-47fc-a8b4-88e4b4e56442",
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "email.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-20T15:58:59Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail SES Check SES Sending Enabled

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, SES
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect when a user inquires whether SES Sending is enabled.

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    return event.get("eventName") == "GetAccountSendingEnabled"


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["accountRegion"] = f"{event.get('recipientAccountId')}_{event.get('eventRegion')}"
    return context

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_ses_check_ses_sending_enabled.py
RuleID: "AWS.CloudTrail.SES.CheckSESSendingEnabled"
DisplayName: AWS CloudTrail SES Check SES Sending Enabled
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
CreateAlert: false
Description: >
  Detect when a user inquires whether SES Sending is enabled.
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.discovery.ses-enumerate/
Tags:
  - AWS CloudTrail
  - SES

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is GetAccountSendingEnabled

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "910326f5-5c2c-49b4-a963-702280f29208",
  "eventName": "GetAccountSendingEnabled",
  "eventSource": "ses.amazonaws.com",
  "eventTime": "2025-01-20 16:52:14.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-01-20 16:52:14.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-20 17:00:54.143061055",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "b88b794d-b419-47b0-9805-5af1de78a1e7",
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "email.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-20T15:58:59Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudTrail SES Enumeration

#
Severity
medium
Time window
36h
Match by
p_alert_context.accountRegion
Tags
AWS, SES, Discovery, Cloud Service Discovery
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

TacticTechniques
Discovery

Rule specification

AnalysisType: correlation_rule
RuleID: AWS.CloudTrail.SES.SESEnumeration
DisplayName: AWS CloudTrail SES Enumeration
Enabled: true
Severity: Medium
Detection:
  - Group:
      - ID: CheckSendingEnabled
        RuleID: AWS.CloudTrail.SES.CheckSESSendingEnabled
      - ID: CheckSendQuota
        RuleID: AWS.CloudTrail.SES.CheckSendQuota
      - ID: ListIdentities
        RuleID: AWS.CloudTrail.SES.ListIdentities
      - ID: CheckVerifications
        RuleID: AWS.CloudTrail.SES.CheckIdentityVerifications
    MatchCriteria:
      accountRegion:
        - GroupID: CheckSendingEnabled
          Match: p_alert_context.accountRegion
        - GroupID: CheckSendQuota
          Match: p_alert_context.accountRegion
        - GroupID: ListIdentities
          Match: p_alert_context.accountRegion
        - GroupID: CheckVerifications
          Match: p_alert_context.accountRegion
    LookbackWindowMinutes: 2160
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 2
Reference: 
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.discovery.ses-enumerate/
Reports:
  MITRE ATT&CK:
    - TA0007:T1580
SummaryAttributes:
  - p_any_aws_arns
  - p_any_ip_addresses
  - p_any_emails
  - p_any_actor_ids
Tags:
  - AWS
  - SES
  - Discovery
  - Cloud Service Discovery

Stages and Predicates

Fires when the steps below all occur within 36h, correlated by p_alert_context.accountRegion. Each step needs one match unless a higher minimum is shown.

Stage 1: step CheckSendingEnabled

References detection AWS CloudTrail SES Check SES Sending Enabled.

Stage 2: step CheckSendQuota

References detection AWS CloudTrail SES Check Send Quota.

Stage 3: step ListIdentities

References detection AWS CloudTrail SES List Identities.

Stage 4: step CheckVerifications

References detection AWS CloudTrail SES Check Identity Verifications.

AWS CloudTrail SES List Identities

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, SES
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    return event.get("eventName") == "ListIdentities"


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["accountRegion"] = f"{event.get('recipientAccountId')}_{event.get('eventRegion')}"
    return context

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_ses_list_identities.py
RuleID: "AWS.CloudTrail.SES.ListIdentities"
DisplayName: AWS CloudTrail SES List Identities
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
CreateAlert: false
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.discovery.ses-enumerate/
Tags:
  - AWS CloudTrail
  - SES

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is ListIdentities

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "SignatureVersion": "4"
  },
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "7c41bbec-52c5-49cb-80aa-88f295d490fd",
  "eventName": "ListIdentities",
  "eventSource": "ses.amazonaws.com",
  "eventTime": "2025-01-20 16:52:14.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-01-20 16:52:14.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-20 17:00:54.217385551",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "7bdf32e1-6e53-4752-b745-2cb37788a23c",
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "email.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-20T15:58:59Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS CloudWatch Log Encryption

#
Severity
informational
Resource types
AWS.CloudWatch.LogGroup
Tags
AWS, Panther
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

AWS automatically performs server-side encryption of logs, but you can encrypt with your own CMK to protect extra sensitive log data.

Detection logic

def policy(resource):
    return resource["KmsKeyId"] is not None

Rule specification

AnalysisType: policy
Filename: aws_cloudwatch_loggroup_encrypted.py
PolicyID: "AWS.CloudWatchLogs.Encrypted"
DisplayName: "AWS CloudWatch Log Encryption"
Enabled: true
ResourceTypes:
  - AWS.CloudWatch.LogGroup
Tags:
  - AWS
  - Panther
Severity: Info
Description: >
  AWS automatically performs server-side encryption of logs, but you can encrypt with your own CMK
  to protect extra sensitive log data.
Runbook: >
  Encrypt the CloudWatch log group with a KMS key, or add this log group to the ignore list.
Reference: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html

Stages and Predicates

Flags AWS.CloudWatch.LogGroup resources when the condition below holds.

Condition

  • KmsKeyId is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
KmsKeyIdis_not_null(no value, null check)excludes:KmsKeyId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
KmsKeyIdis_null
  • (no value, null check)
field:"KmsKeyId" kind:is_null

Response runbook

Encrypt the CloudWatch log group with a KMS key, or add this log group to the ignore list.

AWS CloudWatch Logs Data Retention

#
Severity
low
Resource types
AWS.CloudWatch.LogGroup
Tags
AWS, Panther
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

By default, logs are kept indefinitely and never expire. You can adjust the retention policy for each log group, keeping the indefinite retention, or choosing a specific retention period.

Detection logic

MIN_DAYS_TO_RETAIN_LOGS = 365


def policy(resource):
    retention_in_days = resource["RetentionInDays"]
    # If retention is None, logs will never expire.
    return retention_in_days is None or retention_in_days >= MIN_DAYS_TO_RETAIN_LOGS

Rule specification

AnalysisType: policy
Filename: aws_cloudwatch_loggroup_data_retention.py
PolicyID: "AWS.CloudWatchLogs.DataRetention1Year"
DisplayName: "AWS CloudWatch Logs Data Retention"
Enabled: true
ResourceTypes:
  - AWS.CloudWatch.LogGroup
Tags:
  - AWS
  - Panther
Severity: Low
Description: >
  By default, logs are kept indefinitely and never expire. You can adjust the retention policy
  for each log group, keeping the indefinite retention, or choosing a specific retention period.
Runbook: |
  Change the CloudWatch log group retention from the CloudWatch Logs web console,
  SDK, CloudFormation, or any other supported method.
Reference: https://docs.aws.amazon.com/cli/latest/reference/logs/put-retention-policy.html

Stages and Predicates

Flags AWS.CloudWatch.LogGroup resources when all of the conditions below hold.

Condition

  • RetentionInDays is present
  • RetentionInDays is less than 365

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RetentionInDaysge365excludes:RetentionInDays field:"RetentionInDays" value:"365"
RetentionInDaysis_null(no value, null check)excludes:RetentionInDays

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RetentionInDaysis_not_null
  • (no value, null check)
field:"RetentionInDays" kind:is_not_null
RetentionInDayslt
  • 365 transforms: number
field:"RetentionInDays" kind:lt value:"365"

Response runbook

Change the CloudWatch log group retention from the CloudWatch Logs web console,

SDK, CloudFormation, or any other supported method.

AWS Compromised IAM Key Quarantine

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Identity and Access Management, Initial Access:Valid Accounts, Credential Access:Unsecured Credentials
Reference
unit42.paloaltonetworks.com
Source
github.com/panther-labs/panther-analysis

Detects when an IAM user has the AWSCompromisedKeyQuarantineV2 policy attached to their account.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

IAM_ACTIONS = {
    "AttachUserPolicy",
    "AttachGroupPolicy",
    "AttachRolePolicy",
}

QUARANTINE_MANAGED_POLICY = "arn:aws:iam::aws:policy/AWSCompromisedKeyQuarantineV2"


def rule(event):
    return all(
        [
            event.get("eventSource", "") == "iam.amazonaws.com",
            event.get("eventName", "") in IAM_ACTIONS,
            event.deep_get("requestParameters", "policyArn", default="")
            == QUARANTINE_MANAGED_POLICY,
        ]
    )


def title(event):
    account_id = event.get("recipientAccountId", "<ACCOUNT_ID_NOT_FOUND>")
    user_name = event.deep_get("requestParameters", "userName", default="<USER_NAME_NOT_FOUND>")
    return f"Compromised Key quarantined for [{user_name}] in AWS Account [{account_id}]"

Rule specification

AnalysisType: rule
LogTypes:
  - AWS.CloudTrail
Description: "Detects when an IAM user has the AWSCompromisedKeyQuarantineV2 policy attached to their account."
DisplayName: "AWS Compromised IAM Key Quarantine"
Enabled: true
RuleID: "AWS.CloudTrail.IAMCompromisedKeyQuarantine"
Filename: aws_iam_compromised_key_quarantine.py
Severity: High
Tags:
  - AWS
  - Identity and Access Management
  - Initial Access:Valid Accounts
  - Credential Access:Unsecured Credentials
Reports:
  MITRE ATT&CK:
    - TA0001:T1078.004
    - TA0006:T1552.001
Runbook: >
  Check the quarantined IAM entity's key usage for signs of compromise and follow the instructions outlined in the AWS support case opened regarding this event.
Reference: https://unit42.paloaltonetworks.com/malicious-operations-of-exposed-iam-keys-cryptojacking/
Threshold: 1
DedupPeriodMinutes: 60

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is iam.amazonaws.com
  • eventName is one of AttachUserPolicy, AttachGroupPolicy, AttachRolePolicy
  • requestParameters.policyArn is arn:aws:iam::aws:policy/AWSCompromisedKeyQuarantineV2

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
userNamerequestParameters.userName
recipientAccountId

Response runbook

Check the quarantined IAM entity's key usage for signs of compromise and follow the instructions outlined in the AWS support case opened regarding this event.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "e7bb4b23-66e1-4656-b607-f575fde3b790",
  "eventName": "AttachUserPolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2023-11-21T23:23:52Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a2468e00-2b3c-4696-8056-327a624b5887",
  "requestParameters": {
    "policyArn": "arn:aws:iam::aws:policy/AWSCompromisedKeyQuarantineV2",
    "userName": "test-user"
  },
  "responseElements": null,
  "sessionCredentialFromConsole": "true",
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "FAKE_ACCESS_KEY",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/a-role/user.name",
    "principalId": "FAKE_PRINCIPAL:user.name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2023-11-21T22:28:31Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/a-role",
        "principalId": "FAKE_PRINCIPAL",
        "type": "Role",
        "userName": "a-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Config Global Resources

#
Severity
low
Resource types
AWS.Config.Recorder.Meta
Tags
AWS, Security Control
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

You can have AWS Config record supported types of global resources, such as IAM users, groups, roles, and customer managed policies.

Detection logic

import json

from panther_aws_helpers import BadLookup, resource_lookup
from panther_base_helpers import deep_get

# TODO: Once Detection Pipelines are merged, implement downgraded (INFO) case for multiple
#       global resource recorders.


def policy(resource):
    if (
        resource.get("GlobalRecorderCount", 0) == 0
        or "Recorders" not in resource
        or not bool(resource.get("Recorders"))
    ):
        return False

    for recorder_name in resource.get("Recorders", []):
        try:
            recorder = resource_lookup(recorder_name)
        except BadLookup:
            continue
        if isinstance(recorder, str):
            recorder = json.loads(recorder)
        resource_records_global_resources = bool(
            deep_get(recorder, "RecordingGroup", "IncludeGlobalResourceTypes")
            and deep_get(recorder, "Status", "Recording")
        )
        if resource_records_global_resources:
            return True
    return False

Rule specification

AnalysisType: policy
Filename: aws_config_global_resources.py
PolicyID: "AWS.Config.GlobalResources"
DisplayName: "AWS Config Global Resources"
Enabled: true
ResourceTypes:
  - AWS.Config.Recorder.Meta
Tags:
  - AWS
  - Security Control
Severity: Low
Description: >
  You can have AWS Config record supported types of global resources, such as
  IAM users, groups, roles, and customer managed policies.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-config-is-enabled-for-global-resources
Reference: https://amzn.to/2MO8xXM

Stages and Predicates

Flags AWS.Config.Recorder.Meta resources when any of the conditions below holds.

Condition

  • any of:
    • GlobalRecorderCount is 0
    • Recorders is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
GlobalRecorderCounteq0excludes:GlobalRecorderCount field:"GlobalRecorderCount" value:"0"
Recordersis_null(no value, null check)excludes:Recorders

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
GlobalRecorderCounteq
  • 0 transforms: number
field:"GlobalRecorderCount" kind:eq value:"0"
Recordersis_null
  • (no value, null check)
field:"Recorders" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-config-is-enabled-for-global-resources

AWS Config Recording Status

#
Severity
medium
Resource types
AWS.Config.Recorder
Tags
AWS, Panther
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the config recorder is operational and capturing changes to your account without error.

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    return deep_get(resource, "Status", "LastErrorCode") is None

Rule specification

AnalysisType: policy
Filename: aws_config_recording_no_error.py
PolicyID: "AWS.Config.RecordingNoErrors"
DisplayName: "AWS Config Recording Status"
Enabled: true
ResourceTypes:
  - AWS.Config.Recorder
Tags:
  - AWS
  - Panther
Severity: Medium
Description: >
  This policy ensures that the config recorder is operational and capturing changes to
  your account without error.
Runbook: >
  Check the AWS Config console to understand and resolve the cause of the errors.
Reference: https://docs.aws.amazon.com/config/latest/developerguide/notification-delivery-failed.html

Stages and Predicates

Flags AWS.Config.Recorder resources when the condition below holds.

Condition

  • Status.LastErrorCode is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Status.LastErrorCodeis_null(no value, null check)excludes:Status.LastErrorCode

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Status.LastErrorCodeis_not_null
  • (no value, null check)
field:"Status.LastErrorCode" kind:is_not_null

Response runbook

Check the AWS Config console to understand and resolve the cause of the errors.

AWS Config Records All Resource Types

#
Severity
low
Resource types
AWS.Config.Recorder
Tags
AWS, Panther
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensurers that you have a comprehensive configuration audit in place for all resource types in AWS.

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    return bool(deep_get(resource, "RecordingGroup", "AllSupported"))

Rule specification

AnalysisType: policy
Filename: aws_config_all_resource_types.py
PolicyID: "AWS.Config.RecordAllResourceTypes"
DisplayName: "AWS Config Records All Resource Types"
Enabled: true
ResourceTypes:
  - AWS.Config.Recorder
Tags:
  - AWS
  - Panther
Severity: Low
Description: >
  This policy ensurers that you have a comprehensive configuration audit in place for
  all resource types in AWS.
Runbook: >
  Update AWS Config to record changes to all supported resource types.
Reference: https://aws.amazon.com/blogs/mt/aws-config-best-practices/

Stages and Predicates

Flags AWS.Config.Recorder resources when the condition below holds.

Condition

  • RecordingGroup.AllSupported is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RecordingGroup.AllSupportedis_not_null(no value, null check)excludes:RecordingGroup.AllSupported

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RecordingGroup.AllSupportedis_null
  • (no value, null check)
field:"RecordingGroup.AllSupported" kind:is_null

Response runbook

Update AWS Config to record changes to all supported resource types.

AWS Config Service Created

#
Severity
informational
Compliance
CIS 3.9
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Discovery:Cloud Service Discovery
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An AWS Config Recorder or Delivery Channel was created

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an AWS Config Service change
CONFIG_SERVICE_CREATE_EVENTS = {
    "PutDeliveryChannel",
    "PutConfigurationRecorder",
    "StartConfigurationRecorder",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in CONFIG_SERVICE_CREATE_EVENTS


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_config_service_created.py
RuleID: "AWS.ConfigService.Created"
DisplayName: "AWS Config Service Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Discovery:Cloud Service Discovery
Reports:
  CIS:
    - 3.9
  MITRE ATT&CK:
    - TA0007:T1526
Severity: Info
Description: >
  An AWS Config Recorder or Delivery Channel was created
Runbook: >
  Verify that the Config Service changes were authorized. If not, revert them and investigate who caused the change. Consider altering permissions to prevent this from happening again in the future.
Reference: https://aws.amazon.com/config/
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of PutDeliveryChannel, PutConfigurationRecorder, StartConfigurationRecorder

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • PutConfigurationRecorder
  • PutDeliveryChannel
  • StartConfigurationRecorder
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify that the Config Service changes were authorized. If not, revert them and investigate who caused the change. Consider altering permissions to prevent this from happening again in the future.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "PutDeliveryChannel",
  "eventSource": "config.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "configurationRecorderName": "default"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Config Service Disabled

#
Severity
medium
Compliance
CIS 3.9
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An AWS Config Recorder or Delivery Channel was disabled or deleted

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an AWS Config Service change
CONFIG_SERVICE_DISABLE_DELETE_EVENTS = {
    "StopConfigurationRecorder",
    "DeleteDeliveryChannel",
}


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventName") in CONFIG_SERVICE_DISABLE_DELETE_EVENTS
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_config_service_disabled_deleted.py
RuleID: "AWS.ConfigService.DisabledDeleted"
DisplayName: "AWS Config Service Disabled"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.9
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Medium
Description: >
  An AWS Config Recorder or Delivery Channel was disabled or deleted
Runbook: >
  Verify that the Config Service changes were authorized. If not, revert them and investigate who caused the change. Consider altering permissions to prevent this from happening again in the future.
Reference: https://aws.amazon.com/config/
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of StopConfigurationRecorder, DeleteDeliveryChannel

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DeleteDeliveryChannel
  • StopConfigurationRecorder
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify that the Config Service changes were authorized. If not, revert them and investigate who caused the change. Consider altering permissions to prevent this from happening again in the future.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "DeleteDeliveryChannel",
  "eventSource": "config.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "configurationRecorderName": "default"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Config Status

#
Severity
high
Resource types
AWS.Config.Recorder
Tags
AWS, Panther
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the config recorder is operational and capturing changes to your account.

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    return deep_get(resource, "Status", "Recording", default=False)

Rule specification

AnalysisType: policy
Filename: aws_config_recording_enabled.py
PolicyID: "AWS.Config.RecordingEnabled"
DisplayName: "AWS Config Status"
Enabled: true
ResourceTypes:
  - AWS.Config.Recorder
Tags:
  - AWS
  - Panther
Severity: High
Description: >
  This policy ensures that the config recorder is operational and capturing changes to
  your account.
Runbook: Enable AWS Config recording
Reference: https://docs.aws.amazon.com/config/latest/developerguide/stop-start-recorder.html

Stages and Predicates

Flags AWS.Config.Recorder resources when the condition below holds.

Condition

  • Status.Recording is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Status.Recordingis_not_null(no value, null check)excludes:Status.Recording

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Status.Recordingis_null
  • (no value, null check)
field:"Status.Recording" kind:is_null

Response runbook

Enable AWS Config recording

AWS Console GetSigninToken Potential Abuse

#
Status
Experimental
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, AWS STS, Lateral Movement:Remote Services, Defense Evasion:Use Alternate Authentication Material
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects GetSigninToken calls from non-SSO user agents. An adversary can use tools like aws_consoler to convert compromised CLI credentials into a federated console session, bypassing MFA requirements and obscuring the original access key. The GetSigninToken API creates temporary console access from STS temporary credentials.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

# User agents associated with legitimate AWS SSO portal sign-in token requests
SSO_USER_AGENTS = ["Jersey/${project.version}", "Go-http-client/2.0"]


def rule(event):
    if event.get("eventSource") != "signin.amazonaws.com":
        return False
    if event.get("eventName") != "GetSigninToken":
        return False
    # Exclude legitimate AWS SSO portal traffic
    user_agent = event.get("userAgent", "")
    for sso_ua in SSO_USER_AGENTS:
        if sso_ua in user_agent:
            return False
    return True


def title(event):
    arn = event.deep_get("userIdentity", "arn", default="<unknown>")
    ip_addr = event.get("sourceIPAddress", "<unknown>")
    user_agent = event.get("userAgent", "<unknown>")
    return (
        f"Suspicious GetSigninToken call from [{ip_addr}] "
        f"as [{arn}] with user agent [{user_agent}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_console_getsignintoken.py
RuleID: "AWS.Console.GetSigninToken.Abuse"
DisplayName: "AWS Console GetSigninToken Potential Abuse"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - AWS STS
  - Lateral Movement:Remote Services
  - Defense Evasion:Use Alternate Authentication Material
Reports:
  MITRE ATT&CK:
    - TA0008:T1021.007
    - TA0005:T1550.001
Status: Experimental
Severity: Medium
Description: >
  Detects GetSigninToken calls from non-SSO user agents. An adversary can use tools
  like aws_consoler to convert compromised CLI credentials into a federated console
  session, bypassing MFA requirements and obscuring the original access key. The
  GetSigninToken API creates temporary console access from STS temporary credentials.
Runbook: |
  1. Query CloudTrail for all API calls by userIdentity:arn in the 6 hours before and after this alert, focusing on ConsoleLogin and console-based actions that may indicate the federated session was used
  2. Check if sourceIPAddress and userAgent are associated with known internal tooling or if they match patterns of attacker tools like aws_consoler
  3. Find all other alerts from this userIdentity:accessKeyId in the past 7 days to determine if the underlying credentials are compromised
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is signin.amazonaws.com
  • eventName is GetSigninToken
  • userAgent does not contain Jersey/${project.version}
  • userAgent does not contain Go-http-client/2.0

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userAgentcontainsGo-http-client/2.0excludes:userAgent field:"userAgent" value:"Go-http-client/2.0"
userAgentcontainsJersey/${project.version}excludes:userAgent field:"userAgent" value:"Jersey/${project.version}"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

1. Query CloudTrail for all API calls by userIdentity:arn in the 6 hours before and after this alert, focusing on ConsoleLogin and console-based actions that may indicate the federated session was used

2. Check if sourceIPAddress and userAgent are associated with known internal tooling or if they match patterns of attacker tools like aws_consoler

3. Find all other alerts from this userIdentity:accessKeyId in the past 7 days to determine if the underlying credentials are compromised

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "GetSigninToken",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2024-01-15T10:30:00Z",
  "eventType": "AwsApiCall",
  "recipientAccountId": "123456789012",
  "sourceIPAddress": "203.0.113.50",
  "userAgent": "python-requests/2.28.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/MyRole/session",
    "type": "AssumedRole"
  }
}

AWS Console Login

#
Severity
informational
Log types
AWS.CloudTrail
Source
github.com/panther-labs/panther-analysis

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    return event.get("eventName") == "ConsoleLogin"


def alert_context(event):
    context = {}
    context["ip_and_username"] = event.get(
        "sourceIPAddress", "<MISSING_SOURCE_IP>"
    ) + event.deep_get("userIdentity", "userName", default="<MISSING_USER_NAME>")
    return context

Rule specification

AnalysisType: rule
Filename: aws_console_login.py
RuleID: "AWS.Console.Login"
DisplayName: "AWS Console Login"
Enabled: true
LogTypes:
    - AWS.CloudTrail
Severity: Info
DedupPeriodMinutes: 60
Threshold: 1
CreateAlert: false

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is ConsoleLogin

Indicators

These rows show field, operator, and value matches.

AWS Console Sign-In WITHOUT Okta Redirect

#
Severity
high
Time window
30h
Match by
p_alert_context.actor, userIdentity.userName
Tags
AWS, Configuration Required, Okta, Actor Profiles
Source
github.com/panther-labs/panther-analysis

A user has logged into the AWS console without authenticating via Okta. This rule requires AWS SSO via Okta and both log sources configured.

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.Console.Sign-In.WITHOUT.Okta"
DisplayName: "AWS Console Sign-In WITHOUT Okta Redirect"
Enabled: false
Tags:
    - AWS
    - Configuration Required
    - Okta
    - Actor Profiles
Severity: High
Description: A user has logged into the AWS console without authenticating via Okta.  This rule requires AWS SSO via Okta and both log sources configured.
Detection:
    - Group:
        - ID: Okta SSO to AWS
          RuleID: Okta.SSO.to.AWS
          Absence: true
        - ID: AWS Console Sign-In
          RuleID: AWS.Console.Sign-In
      MatchCriteria:
        field_name:
          - GroupID: Okta SSO to AWS
            Match: p_alert_context.actor
          - GroupID: AWS Console Sign-In
            Match: userIdentity.userName
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5
      LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.actor, userIdentity.userName. Each step needs one match unless a higher minimum is shown.

Stage 1: step Okta SSO to AWS (negated)

References detection SIGNAL - Okta SSO to AWS.

Stage 2: step AWS Console Sign-In

References detection SIGNAL - AWS Console SSO Sign-In.

AWS Decrypt SSM Parameters

#
Status
Experimental
Severity
medium
Compliance
Stratus Red Team aws.credential-access.ssm-retrieve-securestring-parameters
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, Credential Access: Credentials from Password Stores
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Identify principals retrieving a high number of SSM Parameters of type 'SecretString'. This rule filters out known administrative roles that legitimately need bulk parameter access.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import datetime as dt
import json

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent
from panther_detection_helpers.caching import get_string_set, put_string_set

# Determine how many secrets must be accessed in order to trigger an alert
PARAM_THRESHOLD = 25

# Whitelisted IAM role name patterns (case-insensitive)
WHITELISTED_ROLE_PATTERNS = [
    "AWSReservedSSO_DevAdmin_",  # SSO admin roles
    "AWSReservedSSO_Admin_",  # SSO admin roles
]

all_param_names = set()


def rule(event: PantherEvent) -> bool:
    # Exclude events of the wrong type
    if not (
        event.get("eventName") in ("GetParameter", "GetParameters")
        and event.deep_get("requestParameters", "withDecryption")
    ):
        return False

    # Check if the role is whitelisted
    role_name = event.deep_get(
        "userIdentity", "sessionContext", "sessionIssuer", "userName", default=""
    )
    if role_name and any(
        pattern.lower() in role_name.lower() for pattern in WHITELISTED_ROLE_PATTERNS
    ):
        return False

    # Determine if this actor accessed any other params in this account
    key = get_cache_key(event)
    cached_params = get_cached_param_names(key)
    accessed_params = get_param_names(event)

    # Determine if the cache needs updating with new entries
    global all_param_names  # pylint: disable=global-statement
    all_param_names = cached_params | accessed_params
    if all_param_names - cached_params:
        # Only set the TTL if this is the first time we're adding to the cache
        #   Otherwise we'll be perpetually extending the lifespan of the cached data every time we
        #   add more.
        put_string_set(key, all_param_names, epoch_seconds=(3600 if not cached_params else None))

    # Check combined number of params
    return len(all_param_names) > PARAM_THRESHOLD


def title(event: PantherEvent) -> str:
    actor = event.udm("actor_user")
    account_name = event.get("recipientAccountId")
    return f"Excessive SSM parameter decryption by [{actor}] in [{account_name}]"


def severity(event: PantherEvent) -> str:
    # Demote to LOW if attempt was denied
    if not aws_cloudtrail_success(event):
        return "LOW"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    global all_param_names
    context = aws_rule_context(event)
    context.update({"accessedParams": list(all_param_names)})
    return context


def get_cache_key(event) -> str:
    """Use the field values in the event to generate a cache key unique to this actor and
    account ID."""
    offset = (
        dt.datetime.fromisoformat(event.get("p_event_time", "1970-01-01T00:00:00")).timestamp()
        // 3600
        * 3600
    )
    actor = event.udm("actor_user")
    account = event.get("recipientAccountId")
    rule_id = "AWS.SSM.DecryptSSMParams"
    return f"{rule_id}-{account}-{actor}-{offset}"


def get_param_names(event) -> set[str]:
    """Returns the accessed SSM Param names."""
    # Params could be either a list or a single entry
    params = set(event.deep_get("requestParameters", "names", default=[]))
    if single_param := event.deep_get("requestParameters", "name"):
        params.add(single_param)

    return params


def get_cached_param_names(key: str) -> set[str]:
    """Get any previously cached parameter names. Included automatic converstion from string in
    the case of a unit test mock."""
    cached_params = get_string_set(key, force_ttl_check=True)
    if isinstance(cached_params, str):
        # This is a unit test
        cached_params = set(json.loads(cached_params))
    return cached_params

Rule specification

AnalysisType: rule
Filename: aws_ssm_decrypt_ssm_params.py
RuleID: "AWS.SSM.DecryptSSMParams"
DisplayName: AWS Decrypt SSM Parameters
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0006:T1555
  Stratus Red Team:
    - aws.credential-access.ssm-retrieve-securestring-parameters
Description: >
  Identify principals retrieving a high number of SSM Parameters of type 'SecretString'.
  This rule filters out known administrative roles that legitimately need bulk parameter access.
Threshold: 25
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.ssm-retrieve-securestring-parameters/
Runbook: |
  1. Query CloudTrail for all GetParameter and GetParameters events by userIdentity.arn with requestParameters.withDecryption=true in the 4 hours around this alert to identify the complete list of accessed SSM parameters
  2. Review the parameter names from the resources array to determine if they contain database credentials, API keys, or encryption keys, and assess the impact if those secrets are compromised
  3. Search CloudTrail for other suspicious API calls by the same userIdentity.arn and sourceIPAddress in the 24 hours before the first parameter access, looking for privilege escalation, IAM changes, or unusual resource access
SummaryAttributes:
  - sourceIpAddress
  - p_alert_context.accessedParams
Tags:
  - AWS CloudTrail
  - 'Credential Access: Credentials from Password Stores'
Status: Experimental

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is one of GetParameter, GetParameters
  • requestParameters.withDecryption is present
  • userIdentity.sessionContext.sessionIssuer.userName is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 25 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.sessionContext.sessionIssuer.userNameis_not_null(no value, null check)excludes:userIdentity.sessionContext.sessionIssuer.userName

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

1. Query CloudTrail for all GetParameter and GetParameters events by userIdentity.arn with requestParameters.withDecryption=true in the 4 hours around this alert to identify the complete list of accessed SSM parameters

2. Review the parameter names from the resources array to determine if they contain database credentials, API keys, or encryption keys, and assess the impact if those secrets are compromised

3. Search CloudTrail for other suspicious API calls by the same userIdentity.arn and sourceIPAddress in the 24 hours before the first parameter access, looking for privilege escalation, IAM changes, or unusual resource access

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "587e6d58-a653-4fd9-859f-367dc1bad98c",
  "eventName": "GetParameter",
  "eventSource": "ssm.amazonaws.com",
  "eventTime": "2025-02-14 19:43:09.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.11",
  "managementEvent": true,
  "p_event_time": "2025-02-14 19:43:09.000000000",
  "p_log_type": "AWS.CloudTrail",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "a1f28efd-9f5b-4a13-9878-86f57de594dc",
  "requestParameters": {
    "name": "/credentials/stratus-red-team/credentials-25",
    "withDecryption": true
  },
  "resources": [
    {
      "accountId": "111122223333",
      "arn": "arn:aws:ssm:us-west-2:111122223333:parameter/credentials/stratus-red-team/credentials-25"
    }
  ],
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "example-user-agent",
  "userIdentity": {
    "accessKeyId": "EXAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-02-14T19:42:05Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS DNS Crypto Domain

#
Severity
high
Group by
source_ip
Log types
AWS.VPCDns, OCSF.DnsActivity
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Identifies clients that may be performing DNS lookups associated with common currency mining pools.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_iocs import CRYPTO_MINING_DOMAINS


def rule(event):
    query_name = event.udm("dns_query")
    if not query_name:
        return False
    for domain in CRYPTO_MINING_DOMAINS:
        if query_name.rstrip(".").endswith(domain):
            return True
    return False


def title(event):
    return (
        f"[{event.udm('source_ip')}:{event.udm('source_port')}] "
        "made a DNS query for crypto mining domain: "
        f"[{event.udm('dns_query')}]."
    )


def dedup(event):
    return f"{event.udm('source_ip')}"

Rule specification

AnalysisType: rule
Description: Identifies clients that may be performing DNS lookups associated with common currency mining pools.
DisplayName: "AWS DNS Crypto Domain"
Enabled: true
Filename: aws_dns_crypto_domain.py
Reports:
  MITRE ATT&CK:
    - TA0040:T1496
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.VPCDns
  - OCSF.DnsActivity
RuleID: "AWS.DNS.Crypto.Domain"
Threshold: 1

Stages and Predicates

Fires on AWS.VPCDns, OCSF.DnsActivity events when all of the conditions below hold.

Condition

  • dns_query is present
  • any of:
    • dns_query ends with 1gh.com
    • dns_query ends with abcxyz.stream
    • dns_query ends with alimabi.cn
    • dns_query ends with ap.luckpool.net
    • dns_query ends with asiapool.io
    • dns_query ends with backup-pool.com
    • dns_query ends with baikalmine.com
    • dns_query ends with bcn.pool.minergate.com
    • dns_query ends with bcn.vip.pool.minergate.com
    • dns_query ends with bohemianpool.com
    • dns_query ends with ca.minexmr.com
    • dns_query ends with ca.monero.herominers.com
    • dns_query ends with cbd.monerpool.org
    • dns_query ends with cbdv2.monerpool.org
    • dns_query ends with coinfoundry.org
    • dns_query ends with coinpoolit.webhop.me
    • dns_query ends with coolmining.club
    • dns_query ends with cryptmonero.com
    • dns_query ends with crypto-pool.fr
    • dns_query ends with crypto-pool.info
    • dns_query ends with crypto-pools.org
    • dns_query ends with cryptoescrow.eu
    • dns_query ends with cryptoknight.cc
    • dns_query ends with cryptonight-hub.miningpoolhub.com
    • dns_query ends with cryptonight.net
    • dns_query ends with cryptonotepool.org.uk
    • dns_query ends with cryptonotepool.org
    • dns_query ends with d1pool.ddns.net
    • dns_query ends with d5pool.us
    • dns_query ends with daili01.monerpool.org
    • dns_query ends with de.minexmr.com
    • dns_query ends with dl.nbminer.com
    • dns_query ends with do-dear.com
    • dns_query ends with donate.graef.in
    • dns_query ends with donate.ssl.xmrig.com
    • dns_query ends with donate.v2.xmrig.com
    • dns_query ends with donate.xmrig.com
    • dns_query ends with donate2.graef.in
    • dns_query ends with drill.moneroworld.com
    • dns_query ends with dwarfpool.com
    • dns_query ends with emercoin.com
    • dns_query ends with emercoin.net
    • dns_query ends with emergate.net
    • dns_query ends with ethereumpool.co
    • dns_query ends with eu.luckpool.net
    • dns_query ends with eu.minerpool.pw
    • dns_query ends with extremehash.com
    • dns_query ends with extremepool.org
    • dns_query ends with extrmepool.org
    • dns_query ends with fairhash.org
    • dns_query ends with fairpool.cloud
    • dns_query ends with fairpool.xyz
    • dns_query ends with fcn-xmr.pool.minergate.com
    • dns_query ends with fee.xmrig.com
    • dns_query ends with fr.minexmr.com
    • dns_query ends with freeyy.me
    • dns_query ends with gntl.co.uk
    • dns_query ends with hash-to-coins.com
    • dns_query ends with hashanywhere.com
    • dns_query ends with hashfor.cash
    • dns_query ends with hashinvest.net
    • dns_query ends with hashinvest.ws
    • dns_query ends with hashvault.pro
    • dns_query ends with hellominer.com
    • dns_query ends with herominers.com
    • dns_query ends with huadong1-aeon.ppxxmr.com
    • dns_query ends with iwanttoearn.money
    • dns_query ends with jw-js1.ppxxmr.com
    • dns_query ends with kippo.eu
    • dns_query ends with koto-pool.work
    • dns_query ends with lhr.nbminer.com
    • dns_query ends with lhr3.nbminer.com
    • dns_query ends with linux-repository-updates.com
    • dns_query ends with linux.monerpool.org
    • dns_query ends with litecoinpool.org
    • dns_query ends with lokiturtle.herominers.com
    • dns_query ends with luckpool.net
    • dns_query ends with masari.miner.rocks
    • dns_query ends with mine.c3pool.com
    • dns_query ends with mine.moneropool.com
    • dns_query ends with mine.ppxxmr.com
    • dns_query ends with mine.zpool.ca
    • dns_query ends with mine1.ppxxmr.com
    • dns_query ends with minemonero.gq
    • dns_query ends with miner.center
    • dns_query ends with miner.ppxxmr.com
    • dns_query ends with miner.rocks
    • dns_query ends with minercircle.com
    • dns_query ends with minergate.com
    • dns_query ends with minerpool.pw
    • dns_query ends with minerrocks.com
    • dns_query ends with miners.pro
    • dns_query ends with minerxmr.ru
    • dns_query ends with mineshaft.ml
    • dns_query ends with minexmr.cn
    • dns_query ends with minexmr.com
    • dns_query ends with minexmr.org
    • dns_query ends with mining-help.ru
    • dns_query ends with mininglottery.eu
    • dns_query ends with miningpoolhub.com
    • dns_query ends with mixpools.org
    • dns_query ends with moner.monerpool.org
    • dns_query ends with moner1min.monerpool.org
    • dns_query ends with monero-master.crypto-pool.fr
    • dns_query ends with monero.crypto-pool.fr
    • dns_query ends with monero.farm
    • dns_query ends with monero.hashvault.pro
    • dns_query ends with monero.herominers.com
    • dns_query ends with monero.lindon-pool.win
    • dns_query ends with monero.miners.pro
    • dns_query ends with monero.net
    • dns_query ends with monero.riefly.id
    • dns_query ends with monero.us.to
    • dns_query ends with monerocean.stream
    • dns_query ends with monerogb.com
    • dns_query ends with monerohash.com
    • dns_query ends with monerominers.net
    • dns_query ends with moneroocean.stream
    • dns_query ends with moneropool.com
    • dns_query ends with moneropool.nl
    • dns_query ends with moneropool.ru
    • dns_query ends with moneropools.com
    • dns_query ends with monerorx.com
    • dns_query ends with monerpool.org
    • dns_query ends with mooo.com
    • dns_query ends with moriaxmr.com
    • dns_query ends with mro.pool.minergate.com
    • dns_query ends with multipool.us
    • dns_query ends with multipooler.com
    • dns_query ends with myxmr.pw
    • dns_query ends with na.luckpool.net
    • dns_query ends with nanopool.org
    • dns_query ends with nbminer.com
    • dns_query ends with node3.luckpool.net
    • dns_query ends with noobxmr.com
    • dns_query ends with pangolinminer.comgandalph3000.com
    • dns_query ends with pool-proxy.com
    • dns_query ends with pool.4i7i.com
    • dns_query ends with pool.armornetwork.org
    • dns_query ends with pool.cortins.tk
    • dns_query ends with pool.gntl.co.uk
    • dns_query ends with pool.hashvault.pro
    • dns_query ends with pool.minergate.com
    • dns_query ends with pool.minexmr.com
    • dns_query ends with pool.monero.hashvault.pro
    • dns_query ends with pool.ppxxmr.com
    • dns_query ends with pool.somec.cc
    • dns_query ends with pool.support
    • dns_query ends with pool.supportxmr.com
    • dns_query ends with pool.usa-138.com
    • dns_query ends with pool.xmr.pt
    • dns_query ends with pool.xmrfast.com
    • dns_query ends with pool2.armornetwork.org
    • dns_query ends with poolchange.ppxxmr.com
    • dns_query ends with pooldd.com
    • dns_query ends with poolmining.org
    • dns_query ends with poolto.be
    • dns_query ends with ppxvip1.ppxxmr.com
    • dns_query ends with ppxxmr.com
    • dns_query ends with prohash.net
    • dns_query ends with r.twotouchauthentication.online
    • dns_query ends with randomx.xmrig.com
    • dns_query ends with ratchetmining.com
    • dns_query ends with secumine.net
    • dns_query ends with seed.emercoin.com
    • dns_query ends with seed.emercoin.net
    • dns_query ends with seed.emergate.net
    • dns_query ends with seed1.joulecoin.org
    • dns_query ends with seed2.joulecoin.org
    • dns_query ends with seed3.joulecoin.org
    • dns_query ends with seed4.joulecoin.org
    • dns_query ends with seed5.joulecoin.org
    • dns_query ends with seed6.joulecoin.org
    • dns_query ends with seed7.joulecoin.org
    • dns_query ends with seed8.joulecoin.org
    • dns_query ends with semipool.com
    • dns_query ends with sg.minexmr.com
    • dns_query ends with sheepman.mine.bz
    • dns_query ends with shscrypto.net
    • dns_query ends with siamining.com
    • dns_query ends with sumokoin.minerrocks.com
    • dns_query ends with supportxmr.com
    • dns_query ends with suprnova.cc
    • dns_query ends with teracycle.net
    • dns_query ends with trtl.cnpool.cc
    • dns_query ends with trtl.pool.mine2gether.com
    • dns_query ends with tubepool.xyz
    • dns_query ends with turtle.miner.rocks
    • dns_query ends with unipool.pro
    • dns_query ends with us-west.minexmr.com
    • dns_query ends with usxmrpool.com
    • dns_query ends with viaxmr.com
    • dns_query ends with walpool.com
    • dns_query ends with webcoin.me
    • dns_query ends with webservicepag.webhop.net
    • dns_query ends with xiazai.monerpool.org
    • dns_query ends with xiazai1.monerpool.org
    • dns_query ends with xmc.pool.minergate.com
    • dns_query ends with xmo.pool.minergate.com
    • dns_query ends with xmr-asia1.nanopool.org
    • dns_query ends with xmr-au1.nanopool.org
    • dns_query ends with xmr-eu1.nanopool.org
    • dns_query ends with xmr-eu2.nanopool.org
    • dns_query ends with xmr-jp1.nanopool.org
    • dns_query ends with xmr-us-east1.nanopool.org
    • dns_query ends with xmr-us-west1.nanopool.org
    • dns_query ends with xmr-us.suprnova.cc
    • dns_query ends with xmr-usa.dwarfpool.com
    • dns_query ends with xmr.2miners.com
    • dns_query ends with xmr.5b6b7b.ru
    • dns_query ends with xmr.alimabi.cn
    • dns_query ends with xmr.bohemianpool.com
    • dns_query ends with xmr.crypto-pool.fr
    • dns_query ends with xmr.crypto-pool.info
    • dns_query ends with xmr.f2pool.com
    • dns_query ends with xmr.hashcity.org
    • dns_query ends with xmr.hex7e4.ru
    • dns_query ends with xmr.ip28.net
    • dns_query ends with xmr.monerpool.org
    • dns_query ends with xmr.mypool.online
    • dns_query ends with xmr.nanopool.org
    • dns_query ends with xmr.pool.gntl.co.uk
    • dns_query ends with xmr.pool.minergate.com
    • dns_query ends with xmr.poolto.be
    • dns_query ends with xmr.ppxxmr.com
    • dns_query ends with xmr.prohash.net
    • dns_query ends with xmr.pt
    • dns_query ends with xmr.simka.pw
    • dns_query ends with xmr.somec.cc
    • dns_query ends with xmr.suprnova.cc
    • dns_query ends with xmr.usa-138.com
    • dns_query ends with xmr.vip.pool.minergate.com
    • dns_query ends with xmr1min.monerpool.org
    • dns_query ends with xmrf.520fjh.org
    • dns_query ends with xmrf.fjhan.club
    • dns_query ends with xmrfast.com
    • dns_query ends with xmrget.com
    • dns_query ends with xmrigcc.graef.in
    • dns_query ends with xmrminer.cc
    • dns_query ends with xmrminerpro.com
    • dns_query ends with xmrpool.com
    • dns_query ends with xmrpool.de
    • dns_query ends with xmrpool.eu
    • dns_query ends with xmrpool.me
    • dns_query ends with xmrpool.net
    • dns_query ends with xmrpool.xyz
    • dns_query ends with xx11m.monerpool.org
    • dns_query ends with xx11mv2.monerpool.org
    • dns_query ends with xxx.hex7e4.ru
    • dns_query ends with zarabotaibitok.ru
    • dns_query ends with zer0day.ru

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
dns_queryends_with
  • 1gh.com
  • abcxyz.stream
  • alimabi.cn
  • ap.luckpool.net
  • asiapool.io
  • backup-pool.com
  • baikalmine.com
  • bcn.pool.minergate.com
  • bcn.vip.pool.minergate.com
  • bohemianpool.com
  • ca.minexmr.com
  • ca.monero.herominers.com
  • cbd.monerpool.org
  • cbdv2.monerpool.org
  • coinfoundry.org
  • coinpoolit.webhop.me
  • coolmining.club
  • cryptmonero.com
  • crypto-pool.fr
  • crypto-pool.info
  • crypto-pools.org
  • cryptoescrow.eu
  • cryptoknight.cc
  • cryptonight-hub.miningpoolhub.com
  • cryptonight.net
  • cryptonotepool.org
  • cryptonotepool.org.uk
  • d1pool.ddns.net
  • d5pool.us
  • daili01.monerpool.org
  • de.minexmr.com
  • dl.nbminer.com
  • do-dear.com
  • donate.graef.in
  • donate.ssl.xmrig.com
  • donate.v2.xmrig.com
  • donate.xmrig.com
  • donate2.graef.in
  • drill.moneroworld.com
  • dwarfpool.com
  • +211 more values (see full rule source)
field:"dns_query" kind:ends_with
dns_queryis_not_null
  • (no value, null check)
field:"dns_query" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
source_ip
source_port
dns_query

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "account_id": "0123456789",
  "answers": {
    "Class": "IN",
    "Rdata": "1.2.3.4",
    "Type": "A"
  },
  "p_log_type": "AWS.VPCDns",
  "query_class": "IN",
  "query_name": "moneropool.ru",
  "query_timestamp": "2022-06-25 00:27:53",
  "query_type": "A",
  "rcode": "NOERROR",
  "region": "us-west-2",
  "srcaddr": "5.6.7.8",
  "srcids": {
    "instance": "i-0abc234"
  },
  "srcport": "8888",
  "transport": "UDP",
  "version": "1.100000",
  "vpc_id": "vpc-abc123"
}

AWS DNS Logs Deleted

#
Severity
informational
Compliance
Stratus Red Team aws.defense-evasion.dns-delete-logs
Log types
AWS.CloudTrail
Tags
AWS, Cloudtrail, Defense Evasion, Impair Defenses, Disable or Modify Cloud Logs, Defense Evasion:Impair Defenses, Security Control
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when logs for a DNS Resolver have been removed.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event) and event.get("eventName") == "DeleteResolverQueryLogConfig"
    )


def title(event):
    account = event.deep_get("userIdentity", "accountId", default="<UNKNOWN ACCOUNT>")
    region = event.get("awsRegion", "<UNKNOWN REGION>")
    return f"DNS logs have been deleted in {account} in {region}"


def alert_context(event):
    log_id = event.deep_get("requestParameters", "resolverQueryLogConfigId", "<UNKNOWN LOG ID>")
    return aws_rule_context(event) | {"logId": log_id}

Rule specification

AnalysisType: rule
Filename: aws_dns_logs_deleted.py
RuleID: "AWS.CloudTrail.DNSLogsDeleted"
DisplayName: "AWS DNS Logs Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.008 # Defense Evasion: Disable or Modify Cloud Logs

  Stratus Red Team:
    - aws.defense-evasion.dns-delete-logs
Description: "Detects when logs for a DNS Resolver have been removed."
Reference: 
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.defense-evasion.dns-delete-logs/
Runbook: Determine if the log removal to is legitimate.
Tags:
  - AWS
  - Cloudtrail
  - Defense Evasion
  - Impair Defenses
  - Disable or Modify Cloud Logs
  - Defense Evasion:Impair Defenses
  - Security Control

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is DeleteResolverQueryLogConfig

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNameeq
  • DeleteResolverQueryLogConfig
field:"aws::eventName" kind:eq value:"DeleteResolverQueryLogConfig"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
accountIduserIdentity.accountId
awsRegion

Response runbook

Determine if the log removal to is legitimate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "27e6be30-7c86-4544-b0e0-a60b0c927887",
  "eventName": "DeleteResolverQueryLogConfig",
  "eventSource": "route53resolver.amazonaws.com",
  "eventTime": "2024-11-27 18:18:58.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2024-11-27 18:18:58.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-11-27 18:25:54.213480847",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "a45a0f04-8911-4c95-a9d7-3fead8a9bc45",
  "requestParameters": {
    "originSequenceNumber": 0,
    "resolverQueryLogConfigId": "rqlc-5aa596fe3bd84ec6"
  },
  "responseElements": {
    "resolverQueryLogConfig": {
      "arn": "arn:aws:route53resolver:us-west-2:111122223333:resolver-query-log-config/rqlc-5aa596fe3bd84ec6",
      "associationCount": 0,
      "creationTime": "2024-11-27T18:18:56.881520365Z",
      "creatorRequestId": "tf-r53-resolver-query-log-config-20241127181856499800000001",
      "destinationArn": "arn:aws:s3:::sample-bucket-name",
      "id": "rqlc-5aa596fe3bd84ec6",
      "name": "sample-config-name",
      "ownerId": "111122223333",
      "shareStatus": "NOT_SHARED",
      "status": "DELETING"
    }
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "route53resolver.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "stratus-red-team_dbac929e-ae11-4539-8753-35dbcbbc3256",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins",
    "principalId": "SAMPLE_PRINCIPAL_ID:leroy.jenkins",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-11-27T18:17:21Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS DynamoDB Table Autoscaling

#
Severity
low
Resource types
AWS.DynamoDB.Table
Tags
AWS, Availability
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

DynamoDB Auto Scaling can dynamically adjust provisioned throughput capacity in response to traffic patterns. This enables a table to increase its provisioned read and write capacity to handle sudden increases in traffic

Detection logic

from panther_base_helpers import deep_get

# If you do not wish to enforce application auto-scaling on your dynamo tables Global
# Secondary Indices, set this variable to False
CHECK_GSI = True


def policy(resource):
    # Check if this table has never had auto scaling configured
    if resource["BillingModeSummary"] is None and resource["AutoScalingDescriptions"] is None:
        return False

    # Check if this table is not on provisioned billing (and therefore auto scaling does not apply)
    if deep_get(resource, "BillingModeSummary", "BillingMode") != "PROVISIONED":
        return True

    # Check if application auto scaling is configured
    if resource["AutoScalingDescriptions"] is None:
        return False

    # Build a list of all the resources (the table and optionally the GSI's) to be checked
    table_id = "table/" + resource["Name"]
    resource_auto_scaling = {table_id: False}
    if CHECK_GSI:
        # We cannot use resource.get('GSI', []) here as the value is present, it is just a NoneType
        for gsi in resource["GlobalSecondaryIndexes"] or []:
            resource_auto_scaling[table_id + "/index/" + gsi["IndexName"]] = False

    # Check that each resource that requires application autoscaling has it enabled
    for auto_scale_target in resource["AutoScalingDescriptions"]:
        resource_auto_scaling[auto_scale_target["ResourceId"]] = True

    # Return True if all resources have autoscaling enabled
    return all(resource_auto_scaling.values())

Rule specification

AnalysisType: policy
Filename: aws_dynamodb_autoscaling.py
PolicyID: "AWS.DynamoDB.Autoscaling"
DisplayName: "AWS DynamoDB Table Autoscaling"
Enabled: true
ResourceTypes:
  - AWS.DynamoDB.Table
Tags:
  - AWS
  - Availability
Severity: Low
Description: >
  DynamoDB Auto Scaling can dynamically adjust provisioned throughput capacity in response to
  traffic patterns. This enables a table to increase its provisioned read and write capacity
  to handle sudden increases in traffic
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-dynamodb-table-has-autoscaling-enabled
Reference: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html

Stages and Predicates

Flags AWS.DynamoDB.Table resources when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • BillingModeSummary is empty
      • AutoScalingDescriptions is empty
    • all of:
      • BillingModeSummary.BillingMode is PROVISIONED
      • AutoScalingDescriptions is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AutoScalingDescriptionsis_not_null(no value, null check)excludes:AutoScalingDescriptions
BillingModeSummary.BillingModenePROVISIONEDexcludes:BillingModeSummary.BillingMode field:"BillingModeSummary.BillingMode" value:"PROVISIONED"
AutoScalingDescriptionsis_null(no value, null check)excludes:AutoScalingDescriptions
BillingModeSummaryis_null(no value, null check)excludes:BillingModeSummary

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-dynamodb-table-has-autoscaling-enabled

AWS DynamoDB Table Autoscaling Configuration

#
Severity
low
Resource types
AWS.DynamoDB.Table
Tags
AWS, Availability, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

DynamoDB Auto Scaling can dynamically adjust provisioned throughput capacity in response to traffic patterns. This enables a table to increase its provisioned read and write capacity to handle sudden increases in traffic

Detection logic

from panther_base_helpers import deep_get

# If you do not wish to enforce application auto-scaling on your dynamo tables'
# Global Secondary Indices, set this variable to False
CHECK_GSI = True
READ_CAP = {
    "MIN": 5,
    "MAX": 15,
    "TYPE": "READ",
}
WRITE_CAP = {
    "MIN": 5,
    "MAX": 50000,
    "TYPE": "WRITE",
}


def policy(resource):
    # Check if this table has never had auto scaling configured
    if resource["BillingModeSummary"] is None and resource["AutoScalingDescriptions"] is None:
        return False

    # Check if this table is not on provisioned billing (and therefore auto scaling does not apply)
    if deep_get(resource, "BillingModeSummary", "BillingMode") != "PROVISIONED":
        return True

    # Check if application auto scaling is configured at all
    if resource["AutoScalingDescriptions"] is None:
        return False

    # Build a list of all the resources (the table and optionally the GSI's) to be checked
    table_id = "table/" + resource["Name"]

    resource_auto_scaling = {
        table_id + "/READ": False,
        table_id + "/WRITE": False,
    }

    if CHECK_GSI:
        # We cannot use resource.get('GSI', []) here as the value is present, it is just a NoneType
        for gsi in resource["GlobalSecondaryIndexes"] or []:
            resource_auto_scaling[table_id + "/index/" + gsi["IndexName"] + "/READ"] = False
            resource_auto_scaling[table_id + "/index/" + gsi["IndexName"] + "/WRITE"] = False

    # Check that each resource that requires application autoscaling has it enabled
    for auto_scale_target in resource["AutoScalingDescriptions"]:
        # Determine if this is a target for reading capacity or writing capacity
        cap = (
            WRITE_CAP
            if "WriteCapacityUnits" in auto_scale_target["ScalableDimension"]
            else READ_CAP
        )

        # Verify that the minimum and maximum scalable targets are within the configured bounds
        resource_auto_scaling[auto_scale_target["ResourceId"] + "/" + cap["TYPE"]] = (
            auto_scale_target["MinCapacity"] > cap["MIN"]
            and auto_scale_target["MaxCapacity"] < cap["MAX"]
        )

    # Verify that each scalable target was within configured bounds
    return all(resource_auto_scaling.values())

Rule specification

AnalysisType: policy
Filename: aws_dynamodb_autoscaling_configuration.py
PolicyID: "AWS.DynamoDB.AutoscalingConfiguration"
DisplayName: "AWS DynamoDB Table Autoscaling Configuration"
Enabled: false
ResourceTypes:
  - AWS.DynamoDB.Table
Tags:
  - AWS
  - Availability
  - Configuration Required
Severity: Low
Description: >
  DynamoDB Auto Scaling can dynamically adjust provisioned throughput capacity in response to
  traffic patterns. This enables a table to increase its provisioned read and write capacity
  to handle sudden increases in traffic
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-dynamodb-table-has-autoscaling-targets-configured
Reference: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html

Stages and Predicates

Flags AWS.DynamoDB.Table resources when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • BillingModeSummary is empty
      • AutoScalingDescriptions is empty
    • all of:
      • BillingModeSummary.BillingMode is PROVISIONED
      • AutoScalingDescriptions is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AutoScalingDescriptionsis_not_null(no value, null check)excludes:AutoScalingDescriptions
BillingModeSummary.BillingModenePROVISIONEDexcludes:BillingModeSummary.BillingMode field:"BillingModeSummary.BillingMode" value:"PROVISIONED"
AutoScalingDescriptionsis_null(no value, null check)excludes:AutoScalingDescriptions
BillingModeSummaryis_null(no value, null check)excludes:BillingModeSummary

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-dynamodb-table-has-autoscaling-targets-configured

AWS DynamoDB Table TTL

#
Severity
low
Compliance
PCI 3.1
Resource types
AWS.DynamoDB.Table
Tags
AWS, Database, PCI, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all DynamoDB tables have a TTL field configured.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_base_helpers import deep_get


def policy(resource):

    if not resource["TimeToLiveDescription"]:
        return False

    return deep_get(resource, "TimeToLiveDescription", "TimeToLiveStatus") == "ENABLED"

Rule specification

AnalysisType: policy
Filename: aws_dynamodb_table_ttl_enabled.py
PolicyID: "AWS.DynamoDB.TableTTLEnabled"
DisplayName: "AWS DynamoDB Table TTL"
Enabled: false
ResourceTypes:
  - AWS.DynamoDB.Table
Tags:
  - AWS
  - Database
  - PCI
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 3.1
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: >
  This policy validates that all DynamoDB tables have a TTL field configured.
Runbook: >
  Enable table TTL.
Reference: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html

Stages and Predicates

Flags AWS.DynamoDB.Table resources when any of the conditions below holds.

Condition

  • any of:
    • TimeToLiveDescription is empty
    • TimeToLiveDescription.TimeToLiveStatus is not ENABLED

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TimeToLiveDescriptionis_not_null(no value, null check)excludes:TimeToLiveDescription
TimeToLiveDescription.TimeToLiveStatuseqENABLEDexcludes:TimeToLiveDescription.TimeToLiveStatus field:"TimeToLiveDescription.TimeToLiveStatus" value:"ENABLED"

Indicators

These rows show field, operator, and value matches.

Response runbook

Enable table TTL.

AWS EC2 AMI Approved Host

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks that AWS EC2 AMI's are only launched on approved dedicated hosts.

Detection logic

from panther_base_helpers import deep_get

# APPROVED_HOSTS maps AMI IDs to a list of approved dedicated hosts for that AMI.
APPROVED_HOSTS = {
    "EXAMPLE-AMI-ID": ["EXAMPLE-HOST-ID"],
}


def policy(resource):
    # Check if this Instance's AMI is restricted to certain hosts
    if resource.get("ImageId") not in APPROVED_HOSTS:
        return True

    return deep_get(resource, "Placement", "HostId") in APPROVED_HOSTS[resource.get("ImageId")]

Rule specification

AnalysisType: policy
Filename: aws_ec2_ami_approved_host.py
PolicyID: "AWS.EC2.AMI.ApprovedHost"
DisplayName: "AWS EC2 AMI Approved Host"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  Checks that AWS EC2 AMI's are only launched on approved dedicated hosts.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-on-approved-host
Reference: https://aws.amazon.com/ec2/dedicated-hosts/

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • ImageId is one of EXAMPLE-AMI-ID

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ImageIdeqEXAMPLE-AMI-IDexcludes:ImageId field:"ImageId" value:"EXAMPLE-AMI-ID"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ImageIdin
  • EXAMPLE-AMI-ID
field:"ImageId" kind:in value:"EXAMPLE-AMI-ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-on-approved-host

AWS EC2 AMI Approved Instance Type

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the EC2 instance is running with an instance type approved for its AMI.

Detection logic

# APPROVED_TYPES maps AMI IDs to a list of approved types for that AMI.
APPROVED_TYPES = {
    "EXAMPLE-AMI-ID": ["t2.small"],
}


def policy(resource):
    # Check if this Instance's AMI is restricted to certain instance types
    if resource["ImageId"] not in APPROVED_TYPES:
        return True

    return resource["InstanceType"] in APPROVED_TYPES[resource["ImageId"]]

Rule specification

AnalysisType: policy
Filename: aws_ec2_ami_approved_instance_type.py
PolicyID: "AWS.EC2.AMI.ApprovedInstanceType"
DisplayName: "AWS EC2 AMI Approved Instance Type"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  This policy ensures that the EC2 instance is running with an instance type approved for its AMI.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-on-approved-instance-type
Reference: https://aws.amazon.com/ec2/instance-types/

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • ImageId is one of EXAMPLE-AMI-ID

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ImageIdeqEXAMPLE-AMI-IDexcludes:ImageId field:"ImageId" value:"EXAMPLE-AMI-ID"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ImageIdin
  • EXAMPLE-AMI-ID
field:"ImageId" kind:in value:"EXAMPLE-AMI-ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-on-approved-instance-type

AWS EC2 AMI Approved Tenancy

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the EC2 instance was launched with a tenancy approved for its AMI.

Detection logic

from panther_base_helpers import deep_get

# APPROVED_TENANCIES maps AMI IDs to a list of approved tenancy states for that AMI.
# The possible tenancy states are dedicated, host, and default
APPROVED_TENANCIES = {
    "EXAMPLE-AMI-ID": ["default"],
}


def policy(resource):
    # Check if this Instance's AMI has a required tenancy setting
    if resource["ImageId"] not in APPROVED_TENANCIES:
        return True

    return deep_get(resource, "Placement", "Tenancy") in APPROVED_TENANCIES[resource["ImageId"]]

Rule specification

AnalysisType: policy
Filename: aws_ec2_ami_approved_tenancy.py
PolicyID: "AWS.EC2.AMI.ApprovedTenancy"
DisplayName: "AWS EC2 AMI Approved Tenancy"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  This policy ensures that the EC2 instance was launched with a tenancy approved for its AMI.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-with-approved-tenancy
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/dedicated-instance.html

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • ImageId is one of EXAMPLE-AMI-ID

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ImageIdeqEXAMPLE-AMI-IDexcludes:ImageId field:"ImageId" value:"EXAMPLE-AMI-ID"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ImageIdin
  • EXAMPLE-AMI-ID
field:"ImageId" kind:in value:"EXAMPLE-AMI-ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-ami-launched-with-approved-tenancy

AWS EC2 Download Instance User Data

#
Severity
medium
Compliance
Stratus Red Team aws.discovery.ec2-download-user-data
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, EC2, Discovery, Cloud Infrastructure Discovery, Discovery - Cloud Infrastructure Discovery
Reference
hackingthe.cloud
Source
github.com/panther-labs/panther-analysis

An entity has accessed the user data scripts of multiple EC2 instances.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_regions, aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    # Panther's Cloud Security Scanning feature triggers false positives
    actor = event.udm("actor_user")
    if (
        any(actor == "PantherAuditRole-" + region for region in aws_regions())
        and event.get("userAgent") == event.get("sourceIPAddress") == "cloudformation.amazonaws.com"
    ):
        return False

    return (
        event.get("eventName") == "DescribeInstanceAttribute"
        and event.deep_get("requestParameters", "attribute") == "userData"
    )


def title(event: PantherEvent) -> str:
    account_id = event.get("recipientAccountId", "UNKNOWN AWS ACCOUNT")
    actor_user = event.udm("actor_user")
    return (
        f"EC2 Instance User Data accessed in bulk by [{actor_user}] "
        f"in AWS Account [{account_id}]"
    )


def severity(event: PantherEvent) -> str:
    if not aws_cloudtrail_success(event):
        return "LOW"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_download_instance_user_data.py
RuleID: "AWS.EC2.DownloadInstanceUserData"
DisplayName: AWS EC2 Download Instance User Data
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0007:T1580 # Discovery: Cloud Infrastructure Discovery

  Stratus Red Team:
    - aws.discovery.ec2-download-user-data
Description: >
  An entity has accessed the user data scripts of multiple EC2 instances.
Threshold: 10
DedupPeriodMinutes: 1440
Reference: >
  https://hackingthe.cloud/aws/general-knowledge/introduction_user_data/
Runbook: |
  An entity has accessed the user data scripts of multiple EC2 instances. This is
  often an attempt to find unsecured credentials. Ensure the EC2 instances accessed
  do not have any sensitive information stored in the user data.

  Cloud security scanning tools may trigger false positives.  Add an exclude filter for
  the scanning tool's service account to prevent false positives.
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_instance_ids
Tags:
  - AWS CloudTrail
  - EC2
  - Discovery
  - Cloud Infrastructure Discovery
  - Discovery - Cloud Infrastructure Discovery

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is DescribeInstanceAttribute
  • requestParameters.attribute is userData

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 10 matches within 1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

An entity has accessed the user data scripts of multiple EC2 instances. This is

often an attempt to find unsecured credentials. Ensure the EC2 instances accessed

do not have any sensitive information stored in the user data.

Cloud security scanning tools may trigger false positives. Add an exclude filter for

the scanning tool's service account to prevent false positives.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "errorCode": "Client.UnauthorizedOperation",
  "errorMessage": "You are not authorized to perform this operation. User: arn:aws:sts::111122223333:assumed-role/SampleRole/sample-session is not authorized to perform: ec2:DescribeInstanceAttribute on resource: arn:aws:ec2:us-west-2:111122223333:instance/* because no identity-based policy allows the ec2:DescribeInstanceAttribute action. Encoded authorization failure message: 2RCBDBB0PcDpowyBFV7UpJobr-4iGb1MWqjKES--SAO_ca79pxBjKLQQ-sBGm2_m8jjcTAITBlNRa1WCVabq3VCghw9mbj0XZ3-2ZfsVyUrQWiTSUqYh9CUFVnWOmp710QzzKLj-Dy5Aup-bkSCbadoQe6HMevGPiSGeklgwBQc4jLO3oIbPTsujAijnaVr4CUIRBkD4KI8H2ZshEgJOujlpkg47FYqU8bVPNEIv9OwsQ9g4dRMB_cJ7C43kpURWIuiWkFpN8Q84RubskMNB3IfkHGR_Y63Z3LLzOCbPgT4S2zt97PBryCs-lUWSdA2ZCq9dcOtoHQq3Ed-3eB4o7FdZ0cJawa2i6oeUGzzPC2JtBQBcCqExMS8CXENosA0LY9cRUOSo4xc439wzlNpUbMI6K_y1AZWWN0f1QzLW1GkhtlUaLZFvLTwMqTVhClznln2ntmAJ_6iVVlvYco5x6Z4oNn8pMVPk76Iq8yM9o_2-tXmt0st7y_9B83eTSFOTbRnwgHabQXuMyzFKAA3xtuMcbcpB-Ij8c70lkv-eVQeJnfqRd_zbbnsMhxBybfjQyirKjY8bfLg",
  "eventCategory": "Management",
  "eventID": "fa67e0b9-9837-4c84-baa0-78a271821d3e",
  "eventName": "DescribeInstanceAttribute",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-12-11 21:49:07.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "p_event_time": "2024-12-11 21:49:07.000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-12-11 21:55:54.289594",
  "readOnly": true,
  "recipientAccountId": "111122223333",
  "requestID": "f2362cc7-4396-4be5-950c-16c36dddde76",
  "requestParameters": {
    "attribute": "userData",
    "instanceId": "i-12345678"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "ec2.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "sample-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/sample-session",
    "principalId": "SAMPLE_PRINCIPAL_ID:sample-session",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-12-11T21:49:04Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS EC2 EBS Encryption Disabled

#
Severity
medium
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Identifies disabling of default EBS encryption. Disabling default encryption does not change the encryption status of existing volumes.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return (
        event.get("eventSource") == "ec2.amazonaws.com"
        and event.get("eventName") == "DisableEbsEncryptionByDefault"
    )


def title(event):
    return (
        "EC2 EBS Default Encryption was disabled in "
        f"[{event.get('recipientAccountId')}] - "
        f"[{event.get('awsRegion')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: "Identifies disabling of default EBS encryption. Disabling default encryption does not change the encryption status of existing volumes. "
DisplayName: "AWS EC2 EBS Encryption Disabled"
Enabled: true
Filename: aws_ec2_ebs_encryption_disabled.py
Reports:
  MITRE ATT&CK:
    - TA0040:T1486
    - TA0040:T1565
Runbook: Verify this action was intended and if any EBS volumes were created after the change.
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.EC2.EBS.Encryption.Disabled"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is ec2.amazonaws.com
  • eventName is DisableEbsEncryptionByDefault

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify this action was intended and if any EBS volumes were created after the change.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "DisableEbsEncryptionByDefault",
  "eventSource": "ec2.amazonaws.com",
  "recipientAccountId": "123456789",
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "Chrome Browser"
}

AWS EC2 Image Monitoring

#
Severity
informational
Entities
aws_account_ids, aws_arns, aws_instance_ids, aws_tags, domain_names, ip_addresses, trace_ids, usernames
Log types
AWS.CloudTrail
Tags
ec2
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks CloudTrail for occurrences of EC2 Image Actions.

MITRE ATT&CK coverage

TacticTechniques
Execution

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

# AWS CloudTrail API eventNames for EC2 Image Actions
EC2_IMAGE_ACTIONS = [
    "CopyFpgaImage",
    "CopyImage",
    "CreateFpgaImage",
    "CreateImage",
    "CreateRestoreImageTask",
    "CreateStoreImageTask",
    "ImportImage",
]


def rule(event):
    # Disqualify any eventSource that is not ec2
    if event.get("eventSource", "") != "ec2.amazonaws.com":
        return False
    # Disqualify AWS Service-Service operations, which can appear in a variety of forms
    if (
        # FYI there is a weird quirk in the sourceIPAddress field of CloudTrail
        #  events with ec2.amazonaws.com as the source name where users of the
        #  web-console will have their sourceIPAddress recorded as "AWS Internal"
        #  though their userIdentity will be more normal.
        #  Example cloudtrail event in the "Terminate instance From WebUI with assumedRole" test
        event.get("sourceIPAddress", "").endswith(".amazonaws.com")
        or event.deep_get("userIdentity", "type", default="") == "AWSService"
        or event.deep_get("userIdentity", "invokedBy", default="") == "AWS Internal"
        or event.deep_get("userIdentity", "invokedBy", default="").endswith(".amazonaws.com")
    ):
        return False
    # Dry run operations get logged as SES Internal in the sourceIPAddress
    #  but not in the invokedBy field
    if event.get("errorCode", "") == "Client.DryRunOperation":
        return False
    # Disqualify any eventNames that do not Include Image Actions
    # and events that have readOnly set to false
    if event.get("eventName", "") in EC2_IMAGE_ACTIONS:
        return True

    return False


def title(event):
    return (
        f"[{event.deep_get('userIdentity', 'sessionContext', 'sessionIssuer', 'userName')}] "
        f"triggered a CloudTrail action [{event.get('eventName')}] "
        f"within AWS Account ID: [{event.get('recipientAccountId')}]"
    )

Rule specification

AnalysisType: rule
Description: Checks CloudTrail for occurrences of EC2 Image Actions.
DisplayName: "AWS EC2 Image Monitoring"
Enabled: true
Filename: aws_ec2_monitoring.py
Reports:
  MITRE ATT&CK:
    - TA0002:T1204
Runbook: Verify that the action was not taken by a malicious actor.
Reference: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2imagebuilder.html#amazonec2imagebuilder-actions-as-permissions
Severity: Info
Tags:
  - ec2
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.EC2.Monitoring"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is ec2.amazonaws.com
  • sourceIPAddress does not end with .amazonaws.com
  • userIdentity.type is not AWSService
  • userIdentity.invokedBy is not AWS Internal
  • userIdentity.invokedBy does not end with .amazonaws.com
  • errorCode is not Client.DryRunOperation
  • eventName is one of CopyFpgaImage, CopyImage, CreateFpgaImage, CreateImage, CreateRestoreImageTask (+2 more values, see Indicators below)

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodene
  • Client.DryRunOperation
field:"aws::errorCode" kind:ne value:"Client.DryRunOperation"
eventNamein
  • CopyFpgaImage
  • CopyImage
  • CreateFpgaImage
  • CreateImage
  • CreateRestoreImageTask
  • CreateStoreImageTask
  • ImportImage
field:"aws::eventName" kind:in
eventSourceeq
  • ec2.amazonaws.com
field:"aws::eventSource" kind:eq value:"ec2.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
userNameuserIdentity.sessionContext.sessionIssuer.userName
eventName
recipientAccountId

Response runbook

Verify that the action was not taken by a malicious actor.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "0ea3f05a-066c-43f9-8869-393ba67e7936",
  "eventName": "CreateImage",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2022-09-29 22:25:17",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123456789101"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123456789101:role/DevAdministrator",
    "arn:aws:sts::123456789101:assumed-role/DevAdministrator/test_user"
  ],
  "p_any_aws_instance_ids": [
    "i-0381a3817f72a949d"
  ],
  "p_any_domain_names": [
    "AWS Internal"
  ],
  "p_any_trace_ids": [
    "ASIA5PZQZ5QHE2FUNXHR"
  ],
  "p_event_time": "2022-09-29 22:25:17",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-09-29 22:27:25.748",
  "p_row_id": "66011977ec1fd0cf9dacf7d913f08d06",
  "p_source_id": "125a8146-e3ea-454b-aed7-9e08e735b670",
  "p_source_label": "CloudTrail Logs",
  "readOnly": false,
  "recipientAccountId": "123456789101",
  "requestID": "e686939a-a08a-4fd6-abf5-9ea34793cf25",
  "requestParameters": {
    "blockDeviceMapping": {
      "items": [
        {
          "deviceName": "/dev/xvda",
          "ebs": {
            "deleteOnTermination": true,
            "volumeSize": 8
          }
        }
      ]
    },
    "instanceId": "i-0381a3817f72a949d",
    "name": "testimage",
    "noReboot": false
  },
  "responseElements": {
    "imageId": "ami-06aaf5e4b77161786",
    "requestId": "e686939a-a08a-4fd6-abf5-9ea34793cf25"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ASIA5PZQZ5QHE2FUNXHR",
    "accountId": "123456789101",
    "arn": "arn:aws:sts::123456789101:assumed-role/DevAdministrator/test_user",
    "principalId": "AROA5PZQZ5QHBULW27VAC:test_user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-29T22:22:46Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789101",
        "arn": "arn:aws:iam::123456789101:role/DevAdministrator",
        "principalId": "AROA5PZQZ5QHBULW27VAC",
        "type": "Role",
        "userName": "DevAdministrator"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Instance Approved AMI

#
Severity
high
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required, Impact:Resource Hijacking
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures the given EC2 instance is running an AMI from the approved list of AMI's.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

# Tags: ['AWS Managed Rules - Compute']
APPROVED_AMIS = {
    "EXAMPLE-AMI-ID",
}


def policy(resource):
    return resource["ImageId"] in APPROVED_AMIS

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_approved_ami.py
PolicyID: "AWS.EC2.Instance.ApprovedAMI"
DisplayName: "AWS EC2 Instance Approved AMI"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
  - Impact:Resource Hijacking
Reports:
  MITRE ATT&CK:
    - TA0040:T1496
Severity: High
Description: >
  This policy ensures the given EC2 instance is running an AMI from the approved list of AMI's.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-ami
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • ImageId is not one of EXAMPLE-AMI-ID

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ImageIdeqEXAMPLE-AMI-IDexcludes:ImageId field:"ImageId" value:"EXAMPLE-AMI-ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-ami

AWS EC2 Instance Approved Host

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures the given EC2 Instance is running on an approved dedicated host.

Detection logic

from panther_base_helpers import deep_get

# Tags: ['AWS Managed Rules - Compute']
APPROVED_HOSTS = {
    "EXAMPLE-HOST-ID",
}


def policy(resource):
    return deep_get(resource, "Placement", "HostId") in APPROVED_HOSTS

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_approved_host.py
PolicyID: "AWS.EC2.Instance.ApprovedHost"
DisplayName: "AWS EC2 Instance Approved Host"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  This policy ensures the given EC2 Instance is running on an approved dedicated host.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-host
Reference: https://aws.amazon.com/ec2/dedicated-hosts/

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • Placement.HostId is not one of EXAMPLE-HOST-ID

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Placement.HostIdeqEXAMPLE-HOST-IDexcludes:Placement.HostId field:"Placement.HostId" value:"EXAMPLE-HOST-ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-host

AWS EC2 Instance Approved Instance Type

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the EC2 instance is running on one of the approved instance types.

Detection logic

# Tags: ['AWS Managed Rules - Compute']
APPROVED_INSTANCE_TYPES = {
    "t2.small",
}


def policy(resource):
    return resource["InstanceType"] in APPROVED_INSTANCE_TYPES

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_approved_instance_type.py
PolicyID: "AWS.EC2.Instance.ApprovedInstanceType"
DisplayName: "AWS EC2 Instance Approved Instance Type"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  This policy ensures that the EC2 instance is running on one of the approved instance types.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-instance-type
Reference: https://aws.amazon.com/ec2/instance-types/

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • InstanceType is not one of t2.small

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InstanceTypeeqt2.smallexcludes:InstanceType field:"InstanceType" value:"t2.small"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-on-approved-instance-type

AWS EC2 Instance Approved Tenancy

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures the given EC2 Instance is running with an approved tenancy option. The possible tenancy options are dedicated, host, and default.

Detection logic

from panther_base_helpers import deep_get

APPROVED_TENANCIES = {"default"}


def policy(resource):
    return deep_get(resource, "Placement", "Tenancy") in APPROVED_TENANCIES

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_approved_tenancy.py
PolicyID: "AWS.EC2.Instance.ApprovedTenancy"
DisplayName: "AWS EC2 Instance Approved Tenancy"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: Low
Description: >
  This policy ensures the given EC2 Instance is running with an approved tenancy option. The possible tenancy options are dedicated, host, and default.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-with-approved-tenancy
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/dedicated-instance.html

Stages and Predicates

Flags AWS.EC2.Instance resources when the condition below holds.

Condition

  • Placement.Tenancy is not one of default

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Placement.Tenancyeqdefaultexcludes:Placement.Tenancy field:"Placement.Tenancy" value:"default"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-with-approved-tenancy

AWS EC2 Instance Approved VPC

#
Severity
high
Resource types
AWS.EC2.Instance
Tags
AWS, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the given EC2 Instance is running in an approved VPC.

Detection logic

# Tags: ['AWS Managed Rules - Compute']
# This is a list of approved VPC IDs. All EC2 instances must exist in one of these VPCs.
APPROVED_VPCS = {
    "EXAMPLE-VPC-ID",
}

# IGNORED_INSTANCE_TAGS is to describe tags that, if present on an EC2 instance, indicate that the
# instance is to be exempted from this rule.
# Example: IGNORED_INSTANCE_TAGS = {'KeyOne': 'ValueOne', 'KeyTwo': 'ValueTwo'}
IGNORED_INSTANCE_TAGS = {
    "KeyOne": "ValueOne",
}


def policy(resource):
    # Check if any tags on this EC2 instance make it exempt from this rule
    if resource["Tags"] is not None:
        tags = resource.get("Tags", {})
        for tag in tags:
            if tag in IGNORED_INSTANCE_TAGS.keys():
                if isinstance(IGNORED_INSTANCE_TAGS[tag], str):
                    if tags[tag] == IGNORED_INSTANCE_TAGS[tag]:
                        return True
                elif isinstance(IGNORED_INSTANCE_TAGS[tag], list):
                    if tags[tag] in IGNORED_INSTANCE_TAGS[tag]:
                        return True

    return resource["VpcId"] in APPROVED_VPCS

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_approved_vpc.py
PolicyID: "AWS.EC2.Instance.ApprovedVPC"
DisplayName: "AWS EC2 Instance Approved VPC"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Configuration Required
Severity: High
Description: >
  This policy ensures that the given EC2 Instance is running in an approved VPC.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-in-approved-vpc
Reference: https://aws.amazon.com/vpc/

Stages and Predicates

Flags AWS.EC2.Instance resources when all of the conditions below hold.

Condition

  • Tags is empty
  • VpcId is not one of EXAMPLE-VPC-ID

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Tagsis_not_null(no value, null check)excludes:Tags
VpcIdeqEXAMPLE-VPC-IDexcludes:VpcId field:"VpcId" value:"EXAMPLE-VPC-ID"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Tagsis_null
  • (no value, null check)
field:"Tags" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-running-in-approved-vpc

AWS EC2 Instance Detailed Monitoring

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Security Control, Defense Evasion:Impair Defenses, Debug
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the AWS Instance has Detailed Monitoring Enabled

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    # per https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instance-status.html
    # > 16 Instance.State values indicate shutdown
    if deep_get(resource, "State", "Code", default=0) > 16:
        return True
    return deep_get(resource, "Monitoring", "State") != "disabled"

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_detailed_monitoring.py
PolicyID: "AWS.EC2.Instance.DetailedMonitoring"
DisplayName: "AWS EC2 Instance Detailed Monitoring"
Enabled: true
ResourceTypes:
  - AWS.EC2.Instance
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
  - Debug
Severity: Low
Description: >
  This policy ensures that the AWS Instance has Detailed Monitoring Enabled
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-has-detailed-monitoring-enabled
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html

Stages and Predicates

Flags AWS.EC2.Instance resources when all of the conditions below hold.

Condition

  • State.Code is at most 16
  • Monitoring.State is disabled

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-has-detailed-monitoring-enabled

AWS EC2 Instance EBS Optimization

#
Severity
low
Resource types
AWS.EC2.Instance
Tags
AWS, Security Control
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures EBS optimization is enabled for the given EC2 instance, if applicable.

Detection logic

OPTIMIZABLE_TYPES = {
    "c1.xlarge",
    "c3.xlarge",
    "c3.2xlarge",
    "c3.4xlarge",
    "g2.2xlarge",
    "i2.xlarge",
    "i2.2xlarge",
    "i2.4xlarge",
    "m1.large",
    "m1.xlarge",
    "m2.2xlarge",
    "m2.4xlarge",
    "m3.xlarge",
    "m3.2xlarge",
    "r3.xlarge",
    "r3.2xlarge",
    "r3.4xlarge",
}


def policy(resource):
    # Check if this Instance's instance type can be EBS optimized
    if resource["InstanceType"] not in OPTIMIZABLE_TYPES:
        return True

    # Explicit check for True to avoid returning NoneType
    return resource["EbsOptimized"] is True

Rule specification

AnalysisType: policy
Filename: aws_ec2_instance_ebs_optimization.py
PolicyID: "AWS.EC2.Instance.EBSOptimization"
DisplayName: "AWS EC2 Instance EBS Optimization"
Enabled: true
ResourceTypes:
  - AWS.EC2.Instance
Tags:
  - AWS
  - Security Control
Severity: Low
Description: >
  This policy ensures EBS optimization is enabled for the given EC2 instance, if applicable.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-is-ebs-optimized
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html

Stages and Predicates

Flags AWS.EC2.Instance resources when all of the conditions below hold.

Condition

  • InstanceType is one of c1.xlarge, c3.xlarge, c3.2xlarge, c3.4xlarge, g2.2xlarge (+12 more values, see Indicators below)
  • EbsOptimized is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InstanceTypeinc1.xlarge, c3.2xlarge, c3.4xlarge, c3.xlarge, g2.2xlarge, i2.2xlarge, i2.4xlarge, i2.xlarge, m1.large, m1.xlarge, m2.2xlarge, m2.4xlarge, m3.2xlarge, m3.xlarge, r3.2xlarge, r3.4xlarge, r3.xlargeexcludes:InstanceType
EbsOptimizedeqtrueexcludes:EbsOptimized field:"EbsOptimized" value:"true"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EbsOptimizedne
  • true transforms: boolean
field:"EbsOptimized" kind:ne value:"true"
InstanceTypein
  • c1.xlarge
  • c3.2xlarge
  • c3.4xlarge
  • c3.xlarge
  • g2.2xlarge
  • i2.2xlarge
  • i2.4xlarge
  • i2.xlarge
  • m1.large
  • m1.xlarge
  • m2.2xlarge
  • m2.4xlarge
  • m3.2xlarge
  • m3.xlarge
  • r3.2xlarge
  • r3.4xlarge
  • r3.xlarge
field:"InstanceType" kind:in

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-is-ebs-optimized

AWS EC2 Launch Unusual EC2 Instances

#
Severity
informational
Compliance
Stratus Red Team aws.execution.ec2-launch-unusual-instances
Log types
AWS.CloudTrail
Tags
CloudTrail, EC2, Execution, Deploy Container, Execution:Deploy Container
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect when an actor deploys an EC2 instance with an unusual profile based on your business needs.

MITRE ATT&CK coverage

TacticTechniques
Execution

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from collections.abc import Mapping

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent

# Configuration Required
#   Add/remove items from the set below as needed. It should contain instance types which aren't
#   expected to be used in your environment
UNUSUAL_INSTANCE_TYPES = {
    "p2.xlarge"  # Large GPU compute, but no graphics - could be used for crypt mining
}


def rule(event: PantherEvent) -> bool:
    return (
        event.get("eventSource") == "ec2.amazonaws.com"
        and event.get("eventName") == "RunInstances"
        and get_instance_type(event) in get_unusual_instance_types()
    )


def title(event: PantherEvent) -> str:
    # The actor in these events is always AutoScalingService
    account = event.get("recipientAccountId")
    instance_type = get_instance_type(event)
    return f"EC2 instance with a suspicious type '{instance_type}' was launched in in {account}"


def severity(event: PantherEvent) -> str:
    if not aws_cloudtrail_success(event):
        return "LOW"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["instanceType"] = get_instance_type(event)
    return context


def get_unusual_instance_types() -> set[str]:
    # Making this a separate function allows us to mock it during unit tests for reliable testing!
    return UNUSUAL_INSTANCE_TYPES


def get_instance_type(event: PantherEvent) -> str:
    # Return the type of the instance that was launch
    instance_type = event.deep_get(
        "requestParameters", "instanceType", default="<UNKNOWN INSTANCE TYPE>"
    )
    # instanceType could be a string or a dict
    if isinstance(instance_type, Mapping):
        instance_type = instance_type.get("value", "<UNKNOWN INSTANCE TYPE>")
    return instance_type

Rule specification

AnalysisType: rule
Filename: aws_ec2_launch_unusual_ec2_instances.py
RuleID: "AWS.EC2.LaunchUnusualEC2Instances"
DisplayName: AWS EC2 Launch Unusual EC2 Instances
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0002:T1610 # Execution - Deploy Container

  Stratus Red Team:
    - aws.execution.ec2-launch-unusual-instances
Description: >
  Detect when an actor deploys an EC2 instance with an unusual profile based on
  your business needs.
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ec2-launch-unusual-instances/
Runbook: |
  Follow up with the instance to identify whether the instance has a legitimate
  purpose. Reach out to the actor to ensure they performed the action.
SummaryAttributes:
  - p_any_aws_account_ids
  - p_any_instance_ids
  - p_any_arns
  - p_any_aws_tags
  - p_any_usernames
Tags:
  - CloudTrail
  - EC2
  - Execution
  - Deploy Container
  - Execution:Deploy Container

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is ec2.amazonaws.com
  • eventName is RunInstances

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Follow up with the instance to identify whether the instance has a legitimate

purpose. Reach out to the actor to ensure they performed the action.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "41fab871-150b-43ad-b42a-39fff3f2ca4e",
  "eventName": "RunInstances",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-12-16 18:41:07.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "95cdbe4d-8ff7-4111-8f08-44f510371035",
  "requestParameters": {
    "availabilityZone": "us-west-2a",
    "blockDeviceMapping": {},
    "clientToken": "fleet-180da986-0bb4-c936-0c9a-0e20a0c6d1aa-0",
    "disableApiStop": false,
    "disableApiTermination": false,
    "instanceType": "p2.xlarge",
    "instancesSet": {
      "items": [
        {
          "maxCount": 1,
          "minCount": 1
        }
      ]
    },
    "monitoring": {
      "enabled": false
    },
    "subnetId": "subnet-083e5906ef2809ac2"
  },
  "responseElements": {
    "groupSet": {},
    "instancesSet": {
      "items": [
        {
          "amiLaunchIndex": 0,
          "architecture": "arm64",
          "blockDeviceMapping": {},
          "bootMode": "uefi",
          "capacityReservationSpecification": {
            "capacityReservationPreference": "open"
          },
          "clientToken": "fleet-180da986-0bb4-c936-0c9a-0e20a0c6d1aa-0",
          "cpuOptions": {
            "coreCount": 2,
            "threadsPerCore": 1
          },
          "currentInstanceBootMode": "uefi",
          "ebsOptimized": false,
          "enaSupport": true,
          "enclaveOptions": {
            "enabled": false
          },
          "groupSet": {
            "items": [
              {
                "groupId": "sg-03d704b35372e74e8",
                "groupName": "my-group"
              }
            ]
          },
          "hypervisor": "xen",
          "iamInstanceProfile": {
            "arn": "arn:aws:iam::111122223333:instance-profile/profile-id",
            "id": "PROFILE_ID"
          },
          "imageId": "ami-013e7d3a6659f358d",
          "instanceId": "i-07d06021b0da55115",
          "instanceState": {
            "code": 0,
            "name": "pending"
          },
          "instanceType": "p2.xlarge",
          "launchTime": 1734374467000,
          "maintenanceOptions": {
            "autoRecovery": "default"
          },
          "metadataOptions": {
            "httpEndpoint": "enabled",
            "httpProtocolIpv4": "enabled",
            "httpProtocolIpv6": "disabled",
            "httpPutResponseHopLimit": 2,
            "httpTokens": "required",
            "instanceMetadataTags": "disabled",
            "state": "pending"
          },
          "monitoring": {
            "state": "disabled"
          },
          "networkInterfaceSet": {
            "items": [
              {
                "attachment": {
                  "attachTime": 1734374467000,
                  "attachmentId": "eni-attach-022e4a3077e096442",
                  "deleteOnTermination": true,
                  "deviceIndex": 0,
                  "networkCardIndex": 0,
                  "status": "attaching"
                },
                "groupSet": {
                  "items": [
                    {
                      "groupId": "sg-03d704b35372e74e8",
                      "groupName": "eks-cluster-sg-k8s-goat-cluster-816437967"
                    }
                  ]
                },
                "interfaceType": "interface",
                "ipv6AddressesSet": {},
                "macAddress": "02:fc:9a:8a:db:c3",
                "networkInterfaceId": "eni-03ac9043f76fab96c",
                "operator": {
                  "managed": false
                },
                "ownerId": "111122223333",
                "privateDnsName": "ip-192-168-1-95.us-west-2.compute.internal",
                "privateIpAddress": "192.168.1.95",
                "privateIpAddressesSet": {
                  "item": [
                    {
                      "primary": true,
                      "privateDnsName": "ip-192-168-1-95.us-west-2.compute.internal",
                      "privateIpAddress": "192.168.1.95"
                    }
                  ]
                },
                "sourceDestCheck": true,
                "status": "in-use",
                "subnetId": "subnet-083e5906ef2809ac2",
                "tagSet": {},
                "vpcId": "vpc-0330bfd33da75b36e"
              }
            ]
          },
          "operator": {
            "managed": false
          },
          "placement": {
            "availabilityZone": "us-west-2a",
            "tenancy": "default"
          },
          "privateDnsName": "ip-192-168-1-95.us-west-2.compute.internal",
          "privateDnsNameOptions": {
            "enableResourceNameDnsAAAARecord": false,
            "enableResourceNameDnsARecord": false,
            "hostnameType": "ip-name"
          },
          "privateIpAddress": "192.168.1.95",
          "productCodes": {},
          "rootDeviceName": "/dev/xvda",
          "rootDeviceType": "ebs",
          "sourceDestCheck": true,
          "stateReason": {
            "code": "pending",
            "message": "pending"
          },
          "subnetId": "subnet-083e5906ef2809ac2",
          "tagSet": {
            "items": [
              {
                "key": "k8s.io/cluster-autoscaler/enabled",
                "value": "true"
              },
              {
                "key": "aws:autoscaling:groupName",
                "value": "eks-ng-0ca246e9-cac9e862-bfd9-a821-c9fd-9916df5654eb"
              },
              {
                "key": "aws:ec2:fleet-id",
                "value": "fleet-180da986-0bb4-c936-0c9a-0e20a0c6d1aa"
              },
              {
                "key": "eks:cluster-name",
                "value": "k8s-goat-cluster"
              },
              {
                "key": "eks:nodegroup-name",
                "value": "ng-0ca246e9"
              },
              {
                "key": "alpha.eksctl.io/nodegroup-name",
                "value": "ng-0ca246e9"
              },
              {
                "key": "alpha.eksctl.io/nodegroup-type",
                "value": "managed"
              },
              {
                "key": "k8s.io/cluster-autoscaler/k8s-goat-cluster",
                "value": "owned"
              },
              {
                "key": "aws:ec2launchtemplate:id",
                "value": "lt-07a0b5cea4ece8ffd"
              },
              {
                "key": "aws:ec2launchtemplate:version",
                "value": "1"
              },
              {
                "key": "Name",
                "value": "k8s-goat-cluster-ng-0ca246e9-Node"
              },
              {
                "key": "kubernetes.io/cluster/k8s-goat-cluster",
                "value": "owned"
              }
            ]
          },
          "virtualizationType": "hvm",
          "vpcId": "vpc-0330bfd33da75b36e"
        }
      ]
    },
    "ownerId": "111122223333",
    "requestId": "95cdbe4d-8ff7-4111-8f08-44f510371035",
    "requesterId": "414886084714",
    "reservationId": "r-0ff0b006325a10345"
  },
  "sourceIPAddress": "autoscaling.amazonaws.com",
  "userAgent": "autoscaling.amazonaws.com",
  "userIdentity": {
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/AWSServiceRoleForAutoScaling/AutoScaling",
    "invokedBy": "autoscaling.amazonaws.com",
    "principalId": "PRINCIPAL_ID:AutoScaling",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-12-16T18:41:05Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
        "principalId": "PRINCIPAL_ID",
        "type": "Role",
        "userName": "AWSServiceRoleForAutoScaling"
      }
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Manual Security Group Change

#
Severity
medium
Group by
requestParameters
Entities
aws_account_ids, aws_arns, ip_addresses
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Configuration Required, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 security group was manually updated without abiding by the organization's accepted processes. This rule expects organizations to either use the Console, CloudFormation, or Terraform, configurable in the rule's ALLOWED_USER_AGENTS.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_base_helpers import pattern_match_list

PROD_ACCOUNT_IDS = {"11111111111111", "112233445566"}
SG_CHANGE_EVENTS = {
    "CreateSecurityGroup": {
        "fields": ["groupName", "vpcId"],
        "title": "New security group [{groupName}] created by {actor}",
    },
    "AuthorizeSecurityGroupIngress": {
        "fields": ["groupId"],
        "title": "User {actor} has updated security group [{groupId}]",
    },
    "AuthorizeSecurityGroupEgress": {
        "fields": ["groupId"],
        "title": "User {actor} has updated security group [{groupId}]",
    },
}
ALLOWED_USER_AGENTS = {
    "* HashiCorp/?.0 Terraform/*",
    # 'console.ec2.amazonaws.com',
    # 'cloudformation.amazonaws.com',
}
ALLOWED_ROLE_NAMES = {
    "Operator",
    "ContinousDeployment",
}


def rule(event):
    return aws_cloudtrail_success(event) and (
        event.get("eventName") in SG_CHANGE_EVENTS.keys()
        and event.get("recipientAccountId") in PROD_ACCOUNT_IDS
        and
        # Validate the deployment mechanism (Console, CloudFormation, or Terraform)
        not (
            pattern_match_list(event.get("userAgent"), ALLOWED_USER_AGENTS)
            and
            # Validate the IAM Role used is in our acceptable list
            any(role in event.deep_get("userIdentity", "arn") for role in ALLOWED_ROLE_NAMES)
        )
    )


def dedup(event):
    return ":".join(
        event.deep_get("requestParameters", field, default="<UNKNOWN_FIELD>")
        for field in SG_CHANGE_EVENTS[event.get("eventName")]["fields"]
    )


def title(event):
    title_fields = {
        field: event.deep_get("requestParameters", field, default="<UNKNOWN_FIELD>")
        for field in SG_CHANGE_EVENTS[event.get("eventName")]["fields"]
    }
    user = event.deep_get("userIdentity", "arn", default="<UNKNOWN_USER>").split("/")[-1]
    title_template = SG_CHANGE_EVENTS[event.get("eventName")]["title"]
    title_fields["actor"] = user
    return title_template.format(**title_fields)


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_manual_security_group_changes.py
RuleID: "AWS.EC2.ManualSecurityGroupChange"
DisplayName: "AWS EC2 Manual Security Group Change"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Tags:
  - AWS
  - Security Control
  - Configuration Required
  - Defense Evasion:Impair Defenses
Severity: Medium
Description: >
  An EC2 security group was manually updated without abiding by the organization's accepted processes. This rule expects organizations to either use the Console, CloudFormation, or Terraform, configurable in the rule's ALLOWED_USER_AGENTS.
Runbook: Identify the actor who changed the security group and validate it was legitimate
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/working-with-security-groups.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateSecurityGroup, AuthorizeSecurityGroupIngress, AuthorizeSecurityGroupEgress
  • recipientAccountId is one of 11111111111111, 112233445566
  • any of:
    • userAgent does not match the pattern * HashiCorp/?.0 Terraform/*
    • all of:
      • userIdentity.arn does not contain Operator
      • userIdentity.arn does not contain ContinousDeployment

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.arncontainsContinousDeploymentexcludes:userIdentity.arn field:"userIdentity.arn" value:"ContinousDeployment"
userIdentity.arncontainsOperatorexcludes:userIdentity.arn field:"userIdentity.arn" value:"Operator"
userAgentmatch HashiCorp/?.0 Terraform/excludes:userAgent field:"userAgent" value:" HashiCorp/?.0 Terraform/"
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AuthorizeSecurityGroupEgress
  • AuthorizeSecurityGroupIngress
  • CreateSecurityGroup
field:"aws::eventName" kind:in
recipientAccountIdin
  • 11111111111111
  • 112233445566
field:"recipientAccountId" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Identify the actor who changed the security group and validate it was legitimate

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "504b492f-7832-406b-a4fd-45a13e48adc4",
  "eventName": "AuthorizeSecurityGroupIngress",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2021-01-24 04:55:45.000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "112233445566"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::112233445566:role/TestAdmin",
    " arn:aws:sts::112233445566:assumed-role/TestAdmin/alan"
  ],
  "p_any_ip_addresses": [
    "136.25.37.134"
  ],
  "p_event_time": "2021-01-24 04:55:45.000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2021-01-24 05:02:58.358",
  "p_row_id": "1a57ff7ade26aaf5a1a4d7d20775",
  "p_source_id": "e55677c6-7ef5-4541-a443-0d0f17eec19f",
  "p_source_label": "CloudTrail Test",
  "readOnly": false,
  "recipientAccountId": "112233445566",
  "requestID": "91f34d65-513d-4e9f-a3de-e8d27f7ee4b2",
  "requestParameters": {
    "groupId": "sg-04f0b44316f7d2471",
    "ipPermissions": {
      "items": [
        {
          "fromPort": "443",
          "groups": {},
          "ipProtocol": "tcp",
          "ipRanges": {
            "items": [
              {
                "cidrIp": "0.0.0.0/16"
              }
            ]
          },
          "ipv6Ranges": {},
          "prefixListIds": {},
          "toPort": "443"
        }
      ]
    }
  },
  "responseElements": {
    "_return": true,
    "requestId": "91f34d65-513d-4e9f-a3de-e8d27f7ee4b2"
  },
  "sourceIPAddress": "136.25.37.134",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accesskeyid": "ASIASWJRT64Z7ZLFLJNI",
    "accountid": "112233445566",
    "arn": "arn:aws:sts::112233445566:assumed-role/TestAdmin/alan",
    "principalid": "ARORJ4ULULLE0EEJAAKDO:alan",
    "sessioncontext": {
      "attributes": {
        "creationdate": "2021-01-24T04:55:10Z",
        "mfaauthenticated": "true"
      },
      "sessionissuer": {
        "accountid": "112233445566",
        "arn": "arn:aws:iam::112233445566:role/TestAdmin",
        "principalid": "ARORJ4ULULLE0EEJAAKDO",
        "type": "Role",
        "username": "TestAdmin"
      }
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Many Password Read Attempts

#
Severity
informational
Group by
actor_user
Compliance
Stratus Red Team aws.credential-access.ec2-get-password-data
Log types
AWS.CloudTrail
Tags
AWS, CloudTrail, EC2, Credential Access:Credentials from Password Stores, Credential Access, Credentials from Password Stores
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

An actor in AWS has made many attempts to retrieve EC2 passwords. It is typically not necessary to retrieve EC2 passwords more than a few times an hour.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    # Raise alert if this is a GetPasswordData event for an EC2 service
    return (
        event.get("eventName") == "GetPasswordData"
        and event.get("eventSource") == "ec2.amazonaws.com"
    )


def title(event: PantherEvent) -> str:
    actor = event.udm("actor_user")
    return f"{actor} has made multiple requests for EC2 password data in the last hour"


def dedup(event: PantherEvent) -> str:
    # Dedup events based on the principal ID
    return event.udm("actor_user")


def severity(event: PantherEvent) -> str:
    # Return "INFO" severity if the password read attempts are unsuccessful
    if not aws_cloudtrail_success(event):
        return "INFO"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_many_passwors_read_attempts.py
RuleID: "AWS.EC2.ManyPasswordReadAttempts"
DisplayName: "AWS EC2 Many Password Read Attempts"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0006:T1555 # Credential Access: Credentials from Password Stores
  Stratus Red Team:
    - aws.credential-access.ec2-get-password-data
Description: >
  An actor in AWS has made many attempts to retrieve EC2 passwords. It is typically
  not necessary to retrieve EC2 passwords more than a few times an hour.
DedupPeriodMinutes: 60
Threshold: 30
Reference: 
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.ec2-get-password-data/
Runbook: Identify the actor and the EC2 instances for which the credential access
  attempts were made. Determine if the attempts have a valid reason.
Tags:
  - AWS
  - CloudTrail
  - EC2
  - Credential Access:Credentials from Password Stores
  - Credential Access
  - Credentials from Password Stores

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is GetPasswordData
  • eventSource is ec2.amazonaws.com
Alert cadence
alerts after 30 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Identify the actor and the EC2 instances for which the credential access attempts were made. Determine if the attempts have a valid reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "errorCode": "Client.UnauthorizedOperation",
  "errorMessage": "You are not authorized to perform this operation. User: arn:aws:sts::111111111111:assumed-role/sample-role-nostalgic-merkle/sample-role-quizzical-goldstine.jenkins is not authorized to perform: ec2:GetPasswordData on resource: arn:aws:ec2:us-west-2:111111111111:instance/* because no identity-based policy allows the ec2:GetPasswordData action. Encoded authorization failure message: VbdJrM7j1Czq6HEj1lZpnU-AWGICaaPZQs1K_-9U3hQcFpXimBHYHwvg4SOc36ACu0GUYqWoSeX6_wI7ke3QxC0d1_Wv2XYLI96rYdMY2aWzdzwkIp__hUQQi-XqaHmp-QHOHiiJ31xqEkDZvyZXaO0BHhCpf8m7mIMeMaAB2CPtKhPj5NPGGkPc1f6rNFx0grhDkKZ3MrWBo65U4nRjzJrThuyK3146B1k1tWuQfI2_H-QMCuOl_aTIZ93xIeWFoIqKUWnD6-F68V8hhxHHXl0EhiFL9p7LAGvYTPXtJ1wEH1ve8iOW1S9ptI8CuFVP-Q7E7-NS45tIaheVJusaq3JtS03XAnKYC2NuVXnXBwbPNbNiQWH8LfSdgl43MZ5Q9Kin-tqCoA_Yskz0F_JNokjmIB2PKegJ5kANzHcb09u9sSqvgqKqpVHpfIDtjcI8LPzWjZyUNExaymEWOkE4HhtF19t1zyBvuoO6xgZtCyAx-6fsDSO8jpBZbLz9MsPmjhJLfp_yQPOF9ROIrBhvNCY_2tC7hyDDwdl11iNzHQvBaCjiLjE5PcoEchYWTHqlVHZ-yMA3",
  "eventCategory": "Management",
  "eventID": "9e2b3bf0-8f58-4d50-83af-b3175b68c2f8",
  "eventName": "GetPasswordData",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-12-11 21:45:56.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "p_event_time": "2024-12-11 21:45:56.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-12-11 21:50:54.495464364",
  "readOnly": true,
  "recipientAccountId": "111111111111",
  "requestID": "b22ce7ac-297f-41b4-a92e-f4aa38668c6d",
  "requestParameters": {
    "instanceId": "i-abcdef1234567890"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "ec2.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "SampleUserAgent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-nostalgic-merkle/sample-role-quizzical-goldstine.jenkins",
    "principalId": "SAMPLE_PRINCIPAL_ID:leroy.jenkins",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-12-11T21:45:52Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-nostalgic-merkle",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Multi Instance Connect

#
Status
Experimental
Severity
informational
Group by
requestParameters.sSHPublicKey
Compliance
Stratus Red Team aws.lateral-movement.ec2-instance-connect
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, Lateral Movement, Remote Services, SSH, Lateral Movement:Remote Services
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect when an attacker pushes an SSH public key to multiple EC2 instances.

MITRE ATT&CK coverage

TacticTechniques
Lateral Movement

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent


def rule(event: PantherEvent) -> bool:
    return aws_cloudtrail_success(event) and event.get("eventName") == "SendSSHPublicKey"


def unique(event: PantherEvent) -> str:
    return event.deep_get("requestParameters", "instanceId", default="")


def dedup(event: PantherEvent) -> str:
    return event.deep_get("requestParameters", "sSHPublicKey", default="")


def title(event: PantherEvent) -> str:
    actor = event.udm("actor_user")
    account_name = event.get("recipientAccountId")
    return f"{actor} uploaded an SSH Key to multiple instances in {account_name}"


def alert_context(event: PantherEvent) -> dict:
    context = aws_rule_context(event)
    context["instanceId"] = event.deep_get(
        "requestParameters", "instanceId", default="<UNKNOWN EC2 INSTANCE ID>"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_ec2_multi_instance_connect.py
RuleID: "AWS.EC2.MultiInstanceConnect"
DisplayName: AWS EC2 Multi Instance Connect
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0008:T1021.005 # Lateral Movement: Remote Services: SSH

  Stratus Red Team:
    - aws.lateral-movement.ec2-instance-connect
Description: >
  Detect when an attacker pushes an SSH public key to multiple EC2 instances.
DedupPeriodMinutes: 60
Threshold: 2
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.lateral-movement.ec2-instance-connect/
Runbook: |
  Followup with the actor to determine if the SSH key is genuine. Consider using a different SSH key for each instance.
SummaryAttributes:
  - p_any_actor_ids
  - p_any_aws_account_ids
  - p_any_aws_instance_ids
  - p_any_usernames
Tags:
  - AWS CloudTrail
  - Lateral Movement
  - Remote Services
  - SSH
  - Lateral Movement:Remote Services
Status: Experimental

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is SendSSHPublicKey
Alert cadence
alerts after 2 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Followup with the actor to determine if the SSH key is genuine. Consider using a different SSH key for each instance.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "d6d05dd2-d03d-4dce-a88c-02b6a567d889",
  "eventName": "SendSSHPublicKey",
  "eventSource": "ec2-instance-connect.amazonaws.com",
  "eventTime": "2025-01-13 19:58:59.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-01-13 19:58:59.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-01-13 20:05:54.575569351",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "25eac5d1-cd24-4156-a49b-f2bf3a20ec9d",
  "requestParameters": {
    "instanceId": "i-abcdef01234567890",
    "instanceOSUser": "ec2-user",
    "sSHPublicKey": "ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  },
  "responseElements": {
    "requestId": "25eac5d1-cd24-4156-a49b-f2bf3a20ec9d",
    "success": true
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "ec2-instance-connect.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "stratus-red-team_8b255a24-d33d-4750-bd4b-4007124741df",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-01-13T19:21:25Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Startup Script Change

#
Severity
high
Group by
requestParameters.instanceId
Entities
aws_instance_ids
Compliance
Stratus Red Team aws.execution.ec2-user-data
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects changes to the EC2 instance startup script. The shell script will be executed as root/SYSTEM every time the specific instances are booted up.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventName") == "ModifyInstanceAttribute" and event.deep_get(
        "requestParameters", "userData"
    ):
        return True
    return False


def title(event):
    return (
        f"[{event.deep_get('userIdentity','arn')}] "
        "modified the startup script for "
        f" [{event.deep_get('requestParameters', 'instanceId')}] "
        f"in [{event.get('recipientAccountId')}] - [{event.get('awsRegion')}]"
    )


def dedup(event):
    return event.deep_get("requestParameters", "instanceId")


def alert_context(event):
    context = aws_rule_context(event)
    context["instance_ids"] = [event.deep_get("requestParameters", "instanceId"), "no_instance_id"]
    return context

Rule specification

AnalysisType: rule
Description: Detects changes to the EC2 instance startup script. The shell script will be executed as root/SYSTEM every time the specific instances are booted up.
DisplayName: "AWS EC2 Startup Script Change"
Enabled: true
Filename: aws_ec2_startup_script_change.py
Reports:
  MITRE ATT&CK:
    - TA0002:T1059

  Stratus Red Team:
    - aws.execution.ec2-user-data
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html#user-data-shell-scripts
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.EC2.Startup.Script.Change"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ModifyInstanceAttribute
  • requestParameters.userData is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn
instanceIdrequestParameters.instanceId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-2",
  "eventCategory": "Management",
  "eventName": "ModifyInstanceAttribute",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2022-09-30 15:11:25.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "0123456789",
  "requestID": "example-id",
  "requestParameters": {
    "instanceId": "i-012345abcde",
    "userData": "<sensitiveDataRemoved>"
  },
  "responseElements": {
    "_return": true,
    "requestId": "012345abcdef"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "ec2.us-east-2.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "aws sdk",
  "userIdentity": {
    "accessKeyId": "ABCDEXAMPLE123",
    "accountId": "0123456789",
    "arn": "arn:aws:sts::0123456789:assumed-role/Role-us-1/CodeBuild",
    "principalId": "ABCDEXAMPLE:CodeBuild",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-30T14:52:26Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "0123456789",
        "arn": "arn:aws:iam::0123456789:role/CodeBuild-US-East",
        "principalId": "AROAQUW22FGSKBTQ7R5HP",
        "type": "Role",
        "userName": "CodeBuild-US-East"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Traffic Mirroring

#
Severity
medium
Group by
userIdentity.arn
Entities
aws_account_ids, aws_arns, domain_names, trace_ids, usernames
Log types
AWS.CloudTrail
Tags
AWS, Cloudtrail, MITRE
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

This rule captures multiple traffic mirroring events in AWS Cloudtrail.

MITRE ATT&CK coverage

TacticTechniques
Credential Access
Discovery

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Return True to match the log event and trigger an alert.
    event_names = [
        "CreateTrafficMirrorFilter",
        "CreateTrafficMirrorFilterRule",
        "CreateTrafficMirrorSession",
        "CreateTrafficMirrorTarget",
        "DeleteTrafficMirrorFilter",
        "DeleteTrafficMirrorFilterRule",
        "DeleteTrafficMirrorSession",
        "DeleteTrafficMirrorTarget",
        # "DescribeTrafficMirrorFilters",
        # "DescribeTrafficMirrorSessions",
        # "DescribeTrafficMirrorTargets",
        "ModifyTrafficMirrorFilterNetworkServices",
        "ModifyTrafficMirrorFilterRule",
        "ModifyTrafficMirrorSession",
    ]
    if event.deep_get("userIdentity", "invokedBy", default="").endswith(".amazonaws.com"):
        return False
    return (
        event.get("eventSource", "") == "ec2.amazonaws.com"
        and event.get("eventName", "") in event_names
    )


def title(event):
    return (
        f"{event.get('userIdentity',{}).get('arn','no-type')} ec2 activity found for "
        f"{event.get('eventName')} in account {event.get('recipientAccountId')} "
        f"in region {event.get('awsRegion')}."
    )


def dedup(event):
    return f"{event.get('userIdentity',{}).get('arn','no-user-identity-provided')}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: This rule captures multiple traffic mirroring events in AWS Cloudtrail.
DisplayName: "AWS EC2 Traffic Mirroring"
Enabled: true
Filename: aws_ec2_traffic_mirroring.py
Reference: https://attack.mitre.org/techniques/T1040/
Runbook: Examine other activities done by this user to determine whether or not activity is suspicious. If your network traffic is not encrypted, we recommend changing the severity to high or critical.
Severity: Medium
Tags:
  - AWS
  - Cloudtrail
  - MITRE
DedupPeriodMinutes: 1440
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.EC2.Traffic.Mirroring"
SummaryAttributes:
  - userIdentity.type
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • userIdentity.invokedBy does not end with .amazonaws.com
  • eventSource is ec2.amazonaws.com
  • eventName is one of CreateTrafficMirrorFilter, CreateTrafficMirrorFilterRule, CreateTrafficMirrorSession, CreateTrafficMirrorTarget, DeleteTrafficMirrorFilter (+6 more values, see Indicators below)
Alert deduplication
repeat matches within 1d group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.invokedByends_with.amazonaws.comexcludes:userIdentity.invokedBy field:"userIdentity.invokedBy" value:".amazonaws.com"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CreateTrafficMirrorFilter
  • CreateTrafficMirrorFilterRule
  • CreateTrafficMirrorSession
  • CreateTrafficMirrorTarget
  • DeleteTrafficMirrorFilter
  • DeleteTrafficMirrorFilterRule
  • DeleteTrafficMirrorSession
  • DeleteTrafficMirrorTarget
  • ModifyTrafficMirrorFilterNetworkServices
  • ModifyTrafficMirrorFilterRule
  • ModifyTrafficMirrorSession
field:"aws::eventName" kind:in
eventSourceeq
  • ec2.amazonaws.com
field:"aws::eventSource" kind:eq value:"ec2.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

Examine other activities done by this user to determine whether or not activity is suspicious. If your network traffic is not encrypted, we recommend changing the severity to high or critical.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "a3c6297b-3320-4d32-b224-cc45ee75d561",
  "eventName": "CreateTrafficMirrorFilter",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2022-11-15 22:58:13",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123451234513"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123451234513:role/MakeStuffPublic",
    "arn:aws:sts::123451234513:assumed-role/MakeStuffPublic"
  ],
  "p_any_domain_names": [
    "AWS Internal"
  ],
  "p_any_trace_ids": [
    "ASIA57JLR4M2ZZDJUXY3"
  ],
  "p_any_usernames": [
    "MakeStuffPublic"
  ],
  "p_event_time": "2022-11-15 22:58:13",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-11-15 23:00:37.355",
  "p_row_id": "82670d2e7575bbd0e8fc97d014b5a80c",
  "p_source_id": "125a8146-e3ea-454b-aed7-9e08e735b670",
  "p_source_label": "Panther Identity Org CloudTrail",
  "readOnly": false,
  "recipientAccountId": "123451234515",
  "requestID": "200a9157-dff7-4578-87d6-205b01d90a56",
  "requestParameters": {
    "CreateTrafficMirrorFilterRequest": {
      "ClientToken": "5b7eff74-2b70-4f92-8aa1-9c716bf151aa"
    }
  },
  "responseElements": {
    "CreateTrafficMirrorFilterResponse": {
      "clientToken": "5b7eff74-2b70-4f92-8aa1-9c716bf151aa",
      "requestId": "200a9157-dff7-4578-87d6-205b01d90a56",
      "trafficMirrorFilter": {
        "egressFilterRuleSet": "",
        "ingressFilterRuleSet": "",
        "networkServiceSet": "",
        "tagSet": "",
        "trafficMirrorFilterId": "tmf-010db9a7d8056cc2d"
      },
      "xmlns": "http://ec2.amazonaws.com/doc/2016-11-15/"
    }
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ASIA57JLR4M2ZZDJUXY3",
    "accountId": "123451234516",
    "arn": "arn:aws:sts::123123123123:assumed-role/MakeStuffPublic",
    "principalId": "AROA57JLR4M2SBAPVC4BO",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-11-15T22:38:17Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123123123123",
        "arn": "arn:aws:iam::123123123123:role/MakeStuffPublic",
        "principalId": "AROA57JLR4M2SBAPVC4BO",
        "type": "Role",
        "userName": "MakeStuffPublic"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS EC2 Volume Encryption

#
Severity
high
Resource types
AWS.EC2.Volume
Tags
AWS, Data Protection, Collection:Data From Local System
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

You can encrypt both the boot and data volumes of an EC2 instance.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):
    # Only check the volumes that are "in-use" and ignore the rest
    if resource["State"] != "in-use":
        return True

    return bool(resource["Encrypted"])

Rule specification

AnalysisType: policy
Filename: aws_ec2_volume_encryption.py
PolicyID: "AWS.EC2.Volume.Encryption"
DisplayName: "AWS EC2 Volume Encryption"
Enabled: true
ResourceTypes:
  - AWS.EC2.Volume
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Local System
Reports:
  MITRE ATT&CK:
    - TA0009:T1005
Severity: High
Description: >
  You can encrypt both the boot and data volumes of an EC2 instance.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-volumes-are-encrypted
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html

Stages and Predicates

Flags AWS.EC2.Volume resources when all of the conditions below hold.

Condition

  • State is in-use
  • Encrypted is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Encryptedis_not_null(no value, null check)excludes:Encrypted
Statenein-useexcludes:State field:"State" value:"in-use"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Encryptedis_null
  • (no value, null check)
field:"Encrypted" kind:is_null
Stateeq
  • in-use
field:"State" kind:eq value:"in-use"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-instance-volumes-are-encrypted

AWS EC2 Volume Snapshot Encryption

#
Severity
high
Resource types
AWS.EC2.Volume
Tags
AWS, Panther, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

You can encrypt the snapshot of an EC2 volume to protect against accidental data loss

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):
    for snapshot in resource["Snapshots"] or []:
        if snapshot["State"] != "completed":
            continue
        if not bool(snapshot["Encrypted"]):
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_ec2_volume_snapshot_encrypted.py
PolicyID: "AWS.EC2.Volume.Snapshot.Encrypted"
DisplayName: "AWS EC2 Volume Snapshot Encryption"
Enabled: true
ResourceTypes:
  - AWS.EC2.Volume
Tags:
  - AWS
  - Panther
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  You can encrypt the snapshot of an EC2 volume to protect against accidental data loss
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-volume-is-encrypted
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html

Stages and Predicates

Flags AWS.EC2.Volume resources when the condition below holds.

Condition

  • any element of Snapshots matches all of:
    • Snapshots.State is completed
    • Snapshots.Encrypted is empty

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-ec2-volume-is-encrypted

AWS EC2 Vulnerable XZ Image Launched

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Linux, Emerging Threats, Supply Chain Compromise
Reference
cve.mitre.org
Source
github.com/panther-labs/panther-analysis

Detects EC2 instances launched using Amazon Machine Images containing vulnerable XZ Utils library versions (5.6.0 or 5.6.1) affected by CVE-2024-3094. This critical supply chain vulnerability introduced a backdoor allowing remote attackers to bypass SSH authentication on affected Linux systems. The rule monitors CloudTrail RunInstances events and checks AMI IDs against known vulnerable images.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_iocs import XZ_AMIS

# AMIs published by Fedora between 2024-03-26 and 2024-04-02
# OpenSUSE and Kali do not have any recent [public] AMIs that would be affected


def rule(event):
    if not aws_cloudtrail_success(event) or event.get("eventName") != "RunInstances":
        return False

    amis_launched = event.deep_walk(
        "responseElements",
        "instancesSet",
        "items",
        "imageId",
        default="<AMI ID not found>",
        return_val="all",
    )
    # convert to a list if only one item is returned
    if not isinstance(amis_launched, list):
        amis_launched = [amis_launched]
    if any(ami in XZ_AMIS for ami in amis_launched):
        return True

    return False


def title(event):
    amis_launched = event.deep_walk(
        "responseElements",
        "instancesSet",
        "items",
        "imageId",
        default="<AMI ID not found>",
        return_val="all",
    )
    instance_ids = event.deep_walk(
        "responseElements",
        "instancesSet",
        "items",
        "instanceId",
        default="<Instance ID not found>",
        return_val="all",
    )
    return f"Instance {instance_ids} launched with vulnerable AMI: {amis_launched}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: >
  Detects EC2 instances launched using Amazon Machine Images containing vulnerable XZ Utils library versions (5.6.0 or 5.6.1) affected by CVE-2024-3094. This critical supply chain vulnerability introduced a backdoor allowing remote attackers to bypass SSH authentication on affected Linux systems. The rule monitors CloudTrail RunInstances events and checks AMI IDs against known vulnerable images.
DisplayName: "AWS EC2 Vulnerable XZ Image Launched"
Enabled: true
Filename: aws_ec2_vulnerable_xz_image_launched.py
Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3094
Severity: High
Tags:
  - AWS
  - Linux
  - Emerging Threats
  - Supply Chain Compromise
Reports:
  MITRE ATT&CK:
    - TA0001:T1195.001
Runbook: |
  1. Query CloudTrail for all RunInstances events using the same responseElements.instancesSet.items.imageId across all regions in the 30 days before this alert to identify other instances launched with vulnerable XZ AMIs
  2. For the launched instance ID in responseElements.instancesSet.items.instanceId, verify the XZ Utils version using AWS Systems Manager Session Manager by running `xz --version` to confirm if version 5.6.0 or 5.6.1 is installed
  3. Review CloudTrail logs for userIdentity.arn in the 24 hours before the launch to determine if this was automated infrastructure deployment or manual execution, and check for other suspicious API calls from the same principal
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.EC2.Vulnerable.XZ.Image.Launched"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is RunInstances

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneRunInstancesexcludes:eventName field:"eventName" value:"RunInstances"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
instanceIdresponseElements.instancesSet.items.instanceId
imageIdresponseElements.instancesSet.items.imageId

Response runbook

1. Query CloudTrail for all RunInstances events using the same responseElements.instancesSet.items.imageId across all regions in the 30 days before this alert to identify other instances launched with vulnerable XZ AMIs

2. For the launched instance ID in responseElements.instancesSet.items.instanceId, verify the XZ Utils version using AWS Systems Manager Session Manager by running xz --version to confirm if version 5.6.0 or 5.6.1 is installed

3. Review CloudTrail logs for userIdentity.arn in the 24 hours before the launch to determine if this was automated infrastructure deployment or manual execution, and check for other suspicious API calls from the same principal

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "cd7919fe-34a2-4d26-b038-23a2556a79fb",
  "eventName": "RunInstances",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-04-02 15:13:06.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.09",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "439a7e66-d8b6-4bad-98d9-214c20161939",
  "requestParameters": {
    "blockDeviceMapping": {
      "items": [
        {
          "deviceName": "/dev/sda1",
          "ebs": {
            "deleteOnTermination": true,
            "encrypted": false,
            "iops": 3000,
            "snapshotId": "snap-00000000000000000",
            "throughput": 125,
            "volumeSize": 10,
            "volumeType": "gp3"
          }
        }
      ]
    },
    "clientToken": "00000000-0000-0000-0000-000000000000",
    "disableApiStop": false,
    "disableApiTermination": false,
    "ebsOptimized": true,
    "instanceType": "t3a.micro",
    "instancesSet": {
      "items": [
        {
          "imageId": "ami-020a359780bc6f835",
          "keyName": "a-key",
          "maxCount": 1,
          "minCount": 1
        }
      ]
    },
    "monitoring": {
      "enabled": false
    },
    "networkInterfaceSet": {
      "items": [
        {
          "associatePublicIpAddress": true,
          "deviceIndex": 0,
          "groupSet": {
            "items": [
              {
                "groupId": "sg-00000000000000000"
              }
            ]
          },
          "subnetId": "subnet-00000000000000000"
        }
      ]
    },
    "privateDnsNameOptions": {
      "enableResourceNameDnsAAAARecord": false,
      "enableResourceNameDnsARecord": false,
      "hostnameType": "ip-name"
    },
    "tagSpecificationSet": {
      "items": [
        {
          "resourceType": "instance",
          "tags": [
            {
              "key": "Name",
              "value": "test"
            }
          ]
        }
      ]
    }
  },
  "responseElements": {
    "groupSet": {},
    "instancesSet": {
      "items": [
        {
          "amiLaunchIndex": 0,
          "architecture": "x86_64",
          "blockDeviceMapping": {},
          "capacityReservationSpecification": {
            "capacityReservationPreference": "open"
          },
          "clientToken": "8cda61da-eea9-495c-b178-e7014d9bc212",
          "cpuOptions": {
            "coreCount": 1,
            "threadsPerCore": 2
          },
          "currentInstanceBootMode": "legacy-bios",
          "ebsOptimized": true,
          "enaSupport": true,
          "enclaveOptions": {
            "enabled": false
          },
          "groupSet": {
            "items": [
              {
                "groupId": "sg-00000000000000000",
                "groupName": "ssh"
              }
            ]
          },
          "hypervisor": "xen",
          "imageId": "ami-020a359780bc6f835",
          "instanceId": "i-00000000000000000",
          "instanceState": {
            "code": 0,
            "name": "pending"
          },
          "instanceType": "t3a.micro",
          "keyName": "a-key",
          "launchTime": 1712070786000,
          "maintenanceOptions": {
            "autoRecovery": "default"
          },
          "metadataOptions": {
            "httpEndpoint": "enabled",
            "httpProtocolIpv4": "enabled",
            "httpProtocolIpv6": "disabled",
            "httpPutResponseHopLimit": 1,
            "httpTokens": "optional",
            "instanceMetadataTags": "disabled",
            "state": "pending"
          },
          "monitoring": {
            "state": "disabled"
          },
          "networkInterfaceSet": {
            "items": [
              {
                "attachment": {
                  "attachTime": 1712070786000,
                  "attachmentId": "eni-attach-00000000000000000",
                  "deleteOnTermination": true,
                  "deviceIndex": 0,
                  "networkCardIndex": 0,
                  "status": "attaching"
                },
                "groupSet": {
                  "items": [
                    {
                      "groupId": "sg-00000000000000000",
                      "groupName": "ssh"
                    }
                  ]
                },
                "interfaceType": "interface",
                "ipv6AddressesSet": {},
                "macAddress": "06:71:c7:76:de:4f",
                "networkInterfaceId": "eni-00000000000000000",
                "ownerId": "123456789012",
                "privateDnsName": "ip-10-0-0-3.us-west-2.compute.internal",
                "privateIpAddress": "10.0.0.3",
                "privateIpAddressesSet": {
                  "item": [
                    {
                      "primary": true,
                      "privateDnsName": "ip-10-0-0-3.us-west-2.compute.internal",
                      "privateIpAddress": "10.0.0.3"
                    }
                  ]
                },
                "sourceDestCheck": true,
                "status": "in-use",
                "subnetId": "subnet-00000000000000000",
                "tagSet": {},
                "vpcId": "vpc-00000000000000000"
              }
            ]
          },
          "placement": {
            "availabilityZone": "us-west-2b",
            "tenancy": "default"
          },
          "privateDnsName": "ip-10-0-0-3.us-west-2.compute.internal",
          "privateDnsNameOptions": {
            "enableResourceNameDnsAAAARecord": false,
            "enableResourceNameDnsARecord": false,
            "hostnameType": "ip-name"
          },
          "privateIpAddress": "10.0.0.3",
          "productCodes": {},
          "rootDeviceName": "/dev/sda1",
          "rootDeviceType": "ebs",
          "sourceDestCheck": true,
          "stateReason": {
            "code": "pending",
            "message": "pending"
          },
          "subnetId": "subnet-00000000000000000",
          "tagSet": {
            "items": [
              {
                "key": "Name",
                "value": "test"
              }
            ]
          },
          "virtualizationType": "hvm",
          "vpcId": "vpc-00000000000000000"
        }
      ]
    },
    "ownerId": "123456789012",
    "requestId": "439a7e66-d8b6-4bad-98d9-214c20161939",
    "reservationId": "r-00000000000000000"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "ec2.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0",
  "userIdentity": {
    "accessKeyId": "ASIASXP6SDP2MXGX4MYC",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/ARole/user.name",
    "principalId": "AROAVKVYIOO7JN7TN7NSA:user.name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-04-02T14:45:31Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/us-west-2/ARole",
        "principalId": "AROAVKVYIOO7JN7TN7NSA",
        "type": "Role",
        "userName": "ARole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS ECR Events

#
Severity
informational
Group by
recipientAccountId
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An ECR event occurred outside of an expected account or region

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context

# CONFIGURATION REQUIRED: Update with your expected AWS Accounts/Regions
AWS_ACCOUNTS_AND_REGIONS = {
    "123456789012": {"us-west-1", "us-west-2"},
    "103456789012": {"us-east-1", "us-east-2"},
}


def rule(event):
    if event.get("eventSource") == "ecr.amazonaws.com":
        aws_account_id = event.deep_get("userIdentity", "accountId")
        if aws_account_id in AWS_ACCOUNTS_AND_REGIONS:
            if event.get("awsRegion") not in AWS_ACCOUNTS_AND_REGIONS[aws_account_id]:
                return True
        else:
            return True
    return False


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ecr_events.py
RuleID: "AWS.ECR.EVENTS"
DisplayName: "AWS ECR Events"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0005:T1535
Severity: Info
CreateAlert: false
Description: An ECR event occurred outside of an expected account or region
Runbook: https://docs.aws.amazon.com/AmazonECR/latest/userguide/logging-using-cloudtrail.html
Reference: https://aws.amazon.com/blogs/containers/amazon-ecr-in-multi-account-and-multi-region-architectures/
SummaryAttributes:
  - eventSource
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventSource is ecr.amazonaws.com

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.aws.amazon.com/AmazonECR/latest/userguide/logging-using-cloudtrail.html

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-2",
  "eventID": "2bfd4ee2-2178-4a82-a27d-b12939923f0f",
  "eventName": "PutImage",
  "eventSource": "ecr.amazonaws.com",
  "eventTime": "2019-04-15T16:45:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.04",
  "recipientAccountId": "123456789012",
  "requestID": "cf044b7d-5f9d-11e9-9b2a-95983139cc57",
  "requestParameters": {
    "imageManifest": "{\n   \"schemaVersion\": 2,\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"config\": {\n      \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n      \"size\": 5543,\n      \"digest\": \"sha256:000b9b805af1cdb60628898c9f411996301a1c13afd3dbef1d8a16ac6dbf503a\"\n   },\n   \"layers\": [\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 43252507,\n         \"digest\": \"sha256:3b37166ec61459e76e33282dda08f2a9cd698ca7e3d6bc44e6a6e7580cdeff8e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 846,\n         \"digest\": \"sha256:504facff238fde83f1ca8f9f54520b4219c5b8f80be9616ddc52d31448a044bd\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 615,\n         \"digest\": \"sha256:ebbcacd28e101968415b0c812b2d2dc60f969e36b0b08c073bf796e12b1bb449\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 850,\n         \"digest\": \"sha256:c7fb3351ecad291a88b92b600037e2435c84a347683d540042086fe72c902b8a\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 168,\n         \"digest\": \"sha256:2e3debadcbf7e542e2aefbce1b64a358b1931fb403b3e4aeca27cb4d809d56c2\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 37720774,\n         \"digest\": \"sha256:f8c9f51ad524d8ae9bf4db69cd3e720ba92373ec265f5c390ffb21bb0c277941\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 30432107,\n         \"digest\": \"sha256:813a50b13f61cf1f8d25f19fa96ad3aa5b552896c83e86ce413b48b091d7f01b\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 197,\n         \"digest\": \"sha256:7ab043301a6187ea3293d80b30ba06c7bf1a0c3cd4c43d10353b31bc0cecfe7d\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 154,\n         \"digest\": \"sha256:67012cca8f31dc3b8ee2305e7762fee20c250513effdedb38a1c37784a5a2e71\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 176,\n         \"digest\": \"sha256:3bc892145603fffc9b1c97c94e2985b4cb19ca508750b15845a5d97becbd1a0e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 183,\n         \"digest\": \"sha256:6f1c79518f18251d35977e7e46bfa6c6b9cf50df2a79d4194941d95c54258d18\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:b7bcfbc2e2888afebede4dd1cd5eebf029bb6315feeaf0b56e425e11a50afe42\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:2b220f8b0f32b7c2ed8eaafe1c802633bbd94849b9ab73926f0ba46cdae91629\"\n      }\n   ]\n}",
    "imageTag": "latest",
    "registryId": "123456789012",
    "repositoryName": "testrepo"
  },
  "resources": [
    {
      "ARN": "arn:aws:ecr:us-east-2:123456789012:repository/testrepo",
      "accountId": "123456789012"
    }
  ],
  "responseElements": {
    "image": {
      "imageId": {
        "imageDigest": "sha256:98c8b060c21d9adbb6b8c41b916e95e6307102786973ab93a41e8b86d1fc6d3e",
        "imageTag": "latest"
      },
      "imageManifest": "{\n   \"schemaVersion\": 2,\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"config\": {\n      \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n      \"size\": 5543,\n      \"digest\": \"sha256:000b9b805af1cdb60628898c9f411996301a1c13afd3dbef1d8a16ac6dbf503a\"\n   },\n   \"layers\": [\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 43252507,\n         \"digest\": \"sha256:3b37166ec61459e76e33282dda08f2a9cd698ca7e3d6bc44e6a6e7580cdeff8e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 846,\n         \"digest\": \"sha256:504facff238fde83f1ca8f9f54520b4219c5b8f80be9616ddc52d31448a044bd\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 615,\n         \"digest\": \"sha256:ebbcacd28e101968415b0c812b2d2dc60f969e36b0b08c073bf796e12b1bb449\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 850,\n         \"digest\": \"sha256:c7fb3351ecad291a88b92b600037e2435c84a347683d540042086fe72c902b8a\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 168,\n         \"digest\": \"sha256:2e3debadcbf7e542e2aefbce1b64a358b1931fb403b3e4aeca27cb4d809d56c2\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 37720774,\n         \"digest\": \"sha256:f8c9f51ad524d8ae9bf4db69cd3e720ba92373ec265f5c390ffb21bb0c277941\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 30432107,\n         \"digest\": \"sha256:813a50b13f61cf1f8d25f19fa96ad3aa5b552896c83e86ce413b48b091d7f01b\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 197,\n         \"digest\": \"sha256:7ab043301a6187ea3293d80b30ba06c7bf1a0c3cd4c43d10353b31bc0cecfe7d\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 154,\n         \"digest\": \"sha256:67012cca8f31dc3b8ee2305e7762fee20c250513effdedb38a1c37784a5a2e71\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 176,\n         \"digest\": \"sha256:3bc892145603fffc9b1c97c94e2985b4cb19ca508750b15845a5d97becbd1a0e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 183,\n         \"digest\": \"sha256:6f1c79518f18251d35977e7e46bfa6c6b9cf50df2a79d4194941d95c54258d18\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:b7bcfbc2e2888afebede4dd1cd5eebf029bb6315feeaf0b56e425e11a50afe42\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:2b220f8b0f32b7c2ed8eaafe1c802633bbd94849b9ab73926f0ba46cdae91629\"\n      }\n   ]\n}",
      "registryId": "123456789012",
      "repositoryName": "testrepo"
    }
  },
  "sourceIPAddress": "203.0.113.12",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:user/Mary_Major",
    "principalId": "AIDACKCEVSQ6C2EXAMPLE:account_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-04-15T16:42:14Z",
        "mfaAuthenticated": "false"
      }
    },
    "type": "IAMUser",
    "userName": "Mary_Major"
  }
}

AWS ELB SSL Policies

#
Severity
medium
Compliance
PCI 2.2.3, 4.1
Resource types
AWS.ELBV2.ApplicationLoadBalancer
Tags
AWS, Panther, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that deprecated TLS versions are not supported in internet-facing load balancers

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

# https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html#describe-ssl-policies
# Requirements to be "safe": 1) TLSv1.2+ only; 2) Forward Secrecy

TLS_SAFE_POLICIES = {
    "ELBSecurityPolicy-FS-1-2-2019-08",
    "ELBSecurityPolicy-FS-1-2-Res-2019-08",
    "ELBSecurityPolicy-FS-1-2-Res-2020-10",
    "ELBSecurityPolicy-TLS13-1-2-2021-06",
    "ELBSecurityPolicy-TLS13-1-2-Res-2021-06",
    "ELBSecurityPolicy-TLS13-1-3-2021-06",
}


def policy(resource):
    # Ignore load balancers that aren't serving internet traffic
    if resource.get("Scheme") == "internal":
        return True

    return len(resource.get("Listeners") if resource.get("Listeners") else []) >= 1 and all(
        (each_policy in TLS_SAFE_POLICIES for each_policy in resource.get("SSLPolicies", {}).keys())
    )

Rule specification

AnalysisType: policy
Filename: aws_alb_ssl_policy.py
PolicyID: "AWS.ELBv2.SSLPolicy"
DisplayName: "AWS ELB SSL Policies"
Enabled: true
ResourceTypes:
  - AWS.ELBV2.ApplicationLoadBalancer
Tags:
  - AWS
  - Panther
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 2.2.3
    - 4.1
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: Ensures that deprecated TLS versions are not supported in internet-facing load balancers
Runbook: >
  Update your load balancer's SSLPolicies to only support modern TLS versions
Reference: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html

Stages and Predicates

Flags AWS.ELBV2.ApplicationLoadBalancer resources when the condition below holds.

Condition

  • Scheme is not internal

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Schemeeqinternalexcludes:Scheme field:"Scheme" value:"internal"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Schemene
  • internal
field:"Scheme" kind:ne value:"internal"

Response runbook

Update your load balancer's SSLPolicies to only support modern TLS versions

AWS Enforces SSL Policies

#
Severity
medium
Compliance
PCI 2.2.3, 4.1
Resource types
AWS.ELBV2.ApplicationLoadBalancer
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that ELBV2 load balancer listeners are using an SSL policy.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

def policy(resource):

    # Casting to Bool as this may be None and we cannot return a NoneType
    return bool(resource["SSLPolicies"])

Rule specification

AnalysisType: policy
Filename: aws_elbv2_load_balancer_has_ssl_policy.py
PolicyID: "AWS.ELBV2.LoadBalancer.HasSSLPolicy"
DisplayName: "AWS Enforces SSL Policies"
Enabled: false
ResourceTypes:
  - AWS.ELBV2.ApplicationLoadBalancer
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 2.2.3
    - 4.1
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: This policy validates that ELBV2 load balancer listeners are using an SSL policy.
Runbook: Apply an SSL policy to the listener to force HTTPS.
Reference: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-update-certificates.html#update-security-policy

Stages and Predicates

Flags AWS.ELBV2.ApplicationLoadBalancer resources when the condition below holds.

Condition

  • SSLPolicies is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SSLPoliciesis_not_null(no value, null check)excludes:SSLPolicies

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SSLPoliciesis_null
  • (no value, null check)
field:"SSLPolicies" kind:is_null

Response runbook

Apply an SSL policy to the listener to force HTTPS.

AWS GuardDuty Critical Severity Finding

#
Severity
critical
Log types
AWS.GuardDuty
Tags
AWS, Threat Detection, Automated Security, Anomaly Detection, GuardDuty, Machine Learning
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects critical-severity findings (9.0/9.0) from AWS GuardDuty indicating active compromise, imminent data loss, or ongoing attacks. GuardDuty uses machine learning and threat intelligence to identify compromised credentials, cryptocurrency mining, data exfiltration, and connections to malicious infrastructure. This rule filters out sample data and alerts only on genuine critical threats requiring immediate investigation.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import aws_guardduty_context


def rule(event):
    if event.deep_get("service", "additionalInfo", "sample"):
        # in case of sample data
        # https://docs.aws.amazon.com/guardduty/latest/ug/sample_findings.html
        return False
    return 9.0 <= float(event.get("severity", 0)) <= 10.0


def title(event):
    return event.get("title")


def alert_context(event):
    return aws_guardduty_context(event)

Rule specification

AnalysisType: rule
Filename: aws_guardduty_critical_sev_findings.py
RuleID: "AWS.GuardDuty.CriticalSeverityFinding"
DisplayName: "AWS GuardDuty Critical Severity Finding"
Enabled: true
LogTypes:
  - AWS.GuardDuty
Tags:
  - AWS
  - Threat Detection
  - Automated Security
  - Anomaly Detection
  - GuardDuty
  - Machine Learning
Severity: Critical
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
DedupPeriodMinutes: 15
Description: >
  Detects critical-severity findings (9.0/9.0) from AWS GuardDuty indicating active compromise, imminent data loss, or ongoing attacks. GuardDuty uses machine learning and threat intelligence to identify compromised credentials, cryptocurrency mining, data exfiltration, and connections to malicious infrastructure. This rule filters out sample data and alerts only on genuine critical threats requiring immediate investigation.
Runbook: |
  1. Look up the finding type in AWS GuardDuty documentation at https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html, then query CloudTrail for all API calls by the affected principal or resource between service.eventFirstSeen and service.eventLastSeen
  2. For AttackSequence findings, review the description field to identify all MITRE ATT&CK tactics, techniques, and sensitive APIs called, then search for each of these API calls in CloudTrail to understand the full attack chain
  3. Search VPC Flow Logs and S3 access logs for connections from the source IPs mentioned in the GuardDuty finding during the timeframe to identify data exfiltration, lateral movement, or persistence mechanisms
Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
SummaryAttributes:
  - severity
  - type
  - title
  - p_any_domain_names
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.GuardDuty events when all of the conditions below hold.

Condition

  • service.additionalInfo.sample is empty
  • severity is at least 9.0
  • severity is at most 10.0
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
description
severity
id
type
resource
service
accountId
title

Response runbook

1. Look up the finding type in AWS GuardDuty documentation at https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html, then query CloudTrail for all API calls by the affected principal or resource between service.eventFirstSeen and service.eventLastSeen

2. For AttackSequence findings, review the description field to identify all MITRE ATT&CK tactics, techniques, and sensitive APIs called, then search for each of these API calls in CloudTrail to understand the full attack chain

3. Search VPC Flow Logs and S3 access logs for connections from the source IPs mentioned in the GuardDuty finding during the timeframe to identify data exfiltration, lateral movement, or persistence mechanisms

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "123456789012",
  "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/8c258d654378414b9bc26f61a93eb816",
  "createdAt": "2025-02-25 20:40:06.721",
  "description": "A sequence of actions involving 4 signals indicating a possible credential compromise was observed for IAMUser/john_doe with principalId AIDA3UBBJ2K3TVEXAMPLE in account 111122223333\nbetween eventFirstSeen and eventLastSeen with the following behaviors:\n  - 5 MITRE ATT&CK tactics observed: Persistence, Privilege Escalation, Defense Evasion, Discovery, Initial Access\n  - 5 MITRE ATT&CK techniques observed: T1562.008 - Impair Defenses: Disable or Modify Cloud Logs, T1098.003 - Account Manipulation: Additional Cloud Roles, T1078.004 - Valid Accounts: Cloud Accounts, T1087.004 - Account Discovery: Cloud Account, T1098 - Account Manipulation\n  - Connected from a known Tor Exit Node: 10.0.0.1\n  - 4 sensitive APIs called: cloudtrail:DeleteTrail, iam:AttachRolePolicy, iam:CreateRole, iam:ListUsers\n",
  "id": "8c258d654378414b9bc26f61a93eb816",
  "partition": "aws",
  "region": "us-east-1",
  "resource": {
    "resourceType": "AttackSequence"
  },
  "schemaVersion": "2.0",
  "service": {
    "additionalInfo": {},
    "archived": false,
    "count": 1,
    "detectorId": "111111bbbbbbbbbb5555555551111111",
    "eventFirstSeen": "2025-02-25 20:40:06.000000000",
    "eventLastSeen": "2025-02-25 20:40:06.000000000",
    "featureName": "Correlation",
    "resourceRole": "TARGET",
    "serviceName": "guardduty"
  },
  "severity": 9,
  "title": "Potential credential compromise of IAMUser/john_doe indicated by a sequence of actions.",
  "type": "AttackSequence:IAM/CompromisedCredentials",
  "updatedAt": "2025-02-25 20:40:06.721"
}

AWS GuardDuty Enabled

#
Severity
high
Resource types
AWS.GuardDuty.Detector.Meta
Tags
AWS, Security Control, Defense Evasion:Impair Defenses, Configuration Required
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

GuardDuty is a threat detection service that continuously monitors for malicious activity and unauthorized behavior.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

# Add/remove regions as desired
REGIONS_REQUIRED = {
    "us-west-2",
}


def policy(resource):
    # Detector IDs are in the following format:
    # [AccountId]:[Region]:AWS.GuardDuty.Detector
    # so we grab the middle part to determine what regions have GuardDuty enabled
    regions_enabled = [detector.split(":")[1] for detector in resource["Detectors"]]
    for region in REGIONS_REQUIRED:
        if region not in regions_enabled:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_guardduty_enabled.py
PolicyID: "AWS.GuardDuty.Enabled"
DisplayName: "AWS GuardDuty Enabled"
Enabled: true
ResourceTypes:
  - AWS.GuardDuty.Detector.Meta
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Severity: High
Description: >
  GuardDuty is a threat detection service that continuously monitors for malicious activity and unauthorized behavior.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-guardduty-is-enabled
Reference: https://aws.amazon.com/guardduty/

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-guardduty-is-enabled

AWS GuardDuty High Severity Finding

#
Severity
high
Log types
AWS.GuardDuty
Tags
AWS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A high-severity GuardDuty finding has been identified.

Detection logic

from panther_aws_helpers import aws_guardduty_context


def rule(event):
    if event.deep_get("service", "additionalInfo", "sample"):
        # in case of sample data
        # https://docs.aws.amazon.com/guardduty/latest/ug/sample_findings.html
        return False
    return 7.0 <= float(event.get("severity", 0)) <= 8.9


def title(event):
    return event.get("title")


def alert_context(event):
    return aws_guardduty_context(event)

Rule specification

AnalysisType: rule
Filename: aws_guardduty_high_sev_findings.py
RuleID: "AWS.GuardDuty.HighSeverityFinding"
DisplayName: "AWS GuardDuty High Severity Finding"
Enabled: true
LogTypes:
  - AWS.GuardDuty
Tags:
  - AWS
Severity: High
DedupPeriodMinutes: 60
Description: >
  A high-severity GuardDuty finding has been identified.
Runbook: >
  Search related logs to understand the root cause of the activity.
  Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.
Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
SummaryAttributes:
  - severity
  - type
  - title
  - p_any_domain_names
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.GuardDuty events when all of the conditions below hold.

Condition

  • service.additionalInfo.sample is empty
  • severity is at least 7.0
  • severity is at most 8.9

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
description
severity
id
type
resource
service
accountId
title

Response runbook

Search related logs to understand the root cause of the activity. Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "123456789012",
  "arn": "arn:aws:guardduty:us-west-2:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/90b82273685661b9318f078d0851fe9a",
  "createdAt": "2020-02-14T18:12:22.316Z",
  "description": "Principal AssumedRole:IAMRole attempted to add a highly permissive policy to themselves.",
  "id": "eeb88ab56556eb7771b266670dddee5a",
  "partition": "aws",
  "region": "us-east-1",
  "schemaVersion": "2.0",
  "service": {
    "action": {
      "actionType": "AWS_API_CALL",
      "awsApiCallAction": {
        "affectedResources": {
          "AWS::IAM::Role": "arn:aws:iam::123456789012:role/IAMRole"
        },
        "api": "PutRolePolicy",
        "callerType": "Domain",
        "domainDetails": {
          "domain": "cloudformation.amazonaws.com"
        },
        "serviceName": "iam.amazonaws.com"
      }
    },
    "additionalInfo": {},
    "archived": false,
    "count": 1,
    "detectorId": "111111bbbbbbbbbb5555555551111111",
    "eventFirstSeen": "2020-02-14T17:59:17Z",
    "eventLastSeen": "2020-02-14T17:59:17Z",
    "evidence": null,
    "resourceRole": "TARGET",
    "serviceName": "guardduty"
  },
  "severity": 8,
  "title": "Principal AssumedRole:IAMRole attempted to add a policy to themselves that is highly permissive.",
  "type": "PrivilegeEscalation:IAMUser/AdministrativePermissions",
  "updatedAt": "2020-02-14T18:12:22.316Z"
}

AWS GuardDuty Low Severity Finding

#
Severity
low
Log types
AWS.GuardDuty
Tags
AWS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A low-severity GuardDuty finding has been identified.

Detection logic

from panther_aws_helpers import aws_guardduty_context


def rule(event):
    if event.deep_get("service", "additionalInfo", "sample"):
        # in case of sample data
        # https://docs.aws.amazon.com/guardduty/latest/ug/sample_findings.html
        return False
    return 0.1 <= float(event.get("severity", 0)) <= 3.9


def title(event):
    return event.get("title")


def alert_context(event):
    return aws_guardduty_context(event)

Rule specification

AnalysisType: rule
Filename: aws_guardduty_low_sev_findings.py
RuleID: "AWS.GuardDuty.LowSeverityFinding"
DisplayName: "AWS GuardDuty Low Severity Finding"
Enabled: true
LogTypes:
  - AWS.GuardDuty
Tags:
  - AWS
Severity: Low
DedupPeriodMinutes: 1440 # 24 hours
Description: >
  A low-severity GuardDuty finding has been identified.
Runbook: >
  Search related logs to understand the root cause of the activity.
  Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.
Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
SummaryAttributes:
  - severity
  - type
  - title
  - p_any_domain_names
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.GuardDuty events when all of the conditions below hold.

Condition

  • service.additionalInfo.sample is empty
  • severity is at least 0.1
  • severity is at most 3.9
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
description
severity
id
type
resource
service
accountId
title

Response runbook

Search related logs to understand the root cause of the activity. Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "123456789012",
  "arn": "arn:aws:guardduty:us-west-2:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/90b82273685661b9318f078d0851fe9a",
  "createdAt": "2020-02-14T18:12:22.316Z",
  "description": "Principal AssumedRole:IAMRole attempted to add a highly permissive policy to themselves.",
  "id": "eeb88ab56556eb7771b266670dddee5a",
  "partition": "aws",
  "region": "us-east-1",
  "schemaVersion": "2.0",
  "service": {
    "action": {
      "actionType": "AWS_API_CALL",
      "awsApiCallAction": {
        "affectedResources": {
          "AWS::IAM::Role": "arn:aws:iam::123456789012:role/IAMRole"
        },
        "api": "PutRolePolicy",
        "callerType": "Domain",
        "domainDetails": {
          "domain": "cloudformation.amazonaws.com"
        },
        "serviceName": "iam.amazonaws.com"
      }
    },
    "additionalInfo": {},
    "archived": false,
    "count": 1,
    "detectorId": "111111bbbbbbbbbb5555555551111111",
    "eventFirstSeen": "2020-02-14T17:59:17Z",
    "eventLastSeen": "2020-02-14T17:59:17Z",
    "evidence": null,
    "resourceRole": "TARGET",
    "serviceName": "guardduty"
  },
  "severity": 1,
  "title": "Principal AssumedRole:IAMRole attempted to add a policy to themselves that is highly permissive.",
  "type": "PrivilegeEscalation:IAMUser/AdministrativePermissions",
  "updatedAt": "2020-02-14T18:12:22.316Z"
}

AWS GuardDuty Master Account

#
Severity
high
Resource types
AWS.GuardDuty.Detector
Tags
AWS, Configuration Required, Security Control, Defense Evasion:Impair Defenses
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensure that all GuardDuty logs are sending into a single Master account. This is a best practice for centralizing detection logic and useful data during an investigation.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

from panther_base_helpers import deep_get

# Set the MASTER_ACCOUNT_ID variable to a string
# of the AWS Master Account receiving GuardDuty logs.
MASTER_ACCOUNT_ID = None


def policy(resource):
    if MASTER_ACCOUNT_ID is None:
        return True

    if resource["Master"] is None:
        return False

    return MASTER_ACCOUNT_ID == deep_get(resource, "Master", "AccountId")

Rule specification

AnalysisType: policy
Filename: aws_guardduty_master_account.py
PolicyID: "AWS.GuardDuty.MasterAccount"
DisplayName: "AWS GuardDuty Master Account"
Enabled: false
ResourceTypes:
  - AWS.GuardDuty.Detector
Tags:
  - AWS
  - Configuration Required
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Severity: High
Description: >
  Ensure that all GuardDuty logs are sending into a single Master account. This is a best practice for centralizing detection logic and useful data during an investigation.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-guardduty-is-logging-to-a-master-account
Reference: https://aws.amazon.com/guardduty/

Stages and Predicates

Flags AWS.GuardDuty.Detector resources when the condition below holds.

Condition

  • Master is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Masteris_not_null(no value, null check)excludes:Master

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Masteris_null
  • (no value, null check)
field:"Master" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-guardduty-is-logging-to-a-master-account

AWS GuardDuty Medium Severity Finding

#
Severity
medium
Log types
AWS.GuardDuty
Tags
AWS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A medium-severity GuardDuty finding has been identified.

Detection logic

from panther_aws_helpers import aws_guardduty_context


def rule(event):
    if event.deep_get("service", "additionalInfo", "sample"):
        # in case of sample data
        # https://docs.aws.amazon.com/guardduty/latest/ug/sample_findings.html
        return False
    return 4.0 <= float(event.get("severity", 0)) <= 6.9


def title(event):
    return event.get("title")


def alert_context(event):
    return aws_guardduty_context(event)

Rule specification

AnalysisType: rule
Filename: aws_guardduty_med_sev_findings.py
RuleID: "AWS.GuardDuty.MediumSeverityFinding"
DisplayName: "AWS GuardDuty Medium Severity Finding"
Enabled: true
LogTypes:
  - AWS.GuardDuty
Tags:
  - AWS
Severity: Medium
DedupPeriodMinutes: 480 # 8 hours
Description: >
  A medium-severity GuardDuty finding has been identified.
Runbook: >
  Search related logs to understand the root cause of the activity.
  Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.
Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
SummaryAttributes:
  - severity
  - type
  - title
  - p_any_domain_names
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.GuardDuty events when all of the conditions below hold.

Condition

  • service.additionalInfo.sample is empty
  • severity is at least 4.0
  • severity is at most 6.9
Alert deduplication
repeat matches within 8h group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
description
severity
id
type
resource
service
accountId
title

Response runbook

Search related logs to understand the root cause of the activity. Search the Panther Summary Attribute type value in https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html for additional details.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "accountId": "123456789012",
  "arn": "arn:aws:guardduty:us-west-2:123456789012:detector/111111bbbbbbbbbb5555555551111111/finding/90b82273685661b9318f078d0851fe9a",
  "createdAt": "2020-02-14T18:12:22.316Z",
  "description": "Principal AssumedRole:IAMRole attempted to add a highly permissive policy to themselves.",
  "id": "eeb88ab56556eb7771b266670dddee5a",
  "partition": "aws",
  "region": "us-east-1",
  "schemaVersion": "2.0",
  "service": {
    "action": {
      "actionType": "AWS_API_CALL",
      "awsApiCallAction": {
        "affectedResources": {
          "AWS::IAM::Role": "arn:aws:iam::123456789012:role/IAMRole"
        },
        "api": "PutRolePolicy",
        "callerType": "Domain",
        "domainDetails": {
          "domain": "cloudformation.amazonaws.com"
        },
        "serviceName": "iam.amazonaws.com"
      }
    },
    "additionalInfo": {},
    "archived": false,
    "count": 1,
    "detectorId": "111111bbbbbbbbbb5555555551111111",
    "eventFirstSeen": "2020-02-14T17:59:17Z",
    "eventLastSeen": "2020-02-14T17:59:17Z",
    "evidence": null,
    "resourceRole": "TARGET",
    "serviceName": "guardduty"
  },
  "severity": 5,
  "title": "Principal AssumedRole:IAMRole attempted to add a policy to themselves that is highly permissive.",
  "type": "PrivilegeEscalation:IAMUser/AdministrativePermissions",
  "updatedAt": "2020-02-14T18:12:22.316Z"
}

AWS IAM Access Key Compromise Detection

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Credential Access:Unsecured Credentials
Reference
docs.github.com
Source
github.com/panther-labs/panther-analysis

This alert occurs when AWS has detected exposed credentials. It attaches a policy to deny certain actions, effectively quarantining those credentials, and is accompanied by a support case with instructions for detaching the policy.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

EXPOSED_CRED_POLICIES = {
    "AWSExposedCredentialPolicy_DO_NOT_REMOVE",
    "AWSCompromisedKeyQuarantine",
    "AWSCompromisedKeyQuarantineV2",
    "AWSCompromisedKeyQuarantineV3",
}


def rule(event):
    if event.get("eventName") != "PutUserPolicy":
        return False

    request_params = event.get("requestParameters") or {}
    if request_params.get("policyName") not in EXPOSED_CRED_POLICIES:
        return False
    return True


def title(event):
    user_name = event.deep_get("userIdentity", "userName")
    access_key_id = event.deep_get("userIdentity", "accessKeyId")
    return (
        f"[{user_name}]'s AWS IAM Access Key ID [{access_key_id}]"
        f" was exposed and quarantined by AWS"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_key_compromised.py
RuleID: "AWS.IAM.AccessKeyCompromised"
DisplayName: "AWS IAM Access Key Compromise Detection"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0006:T1552
Tags:
  - AWS
  - Credential Access:Unsecured Credentials
Severity: High
Description: This alert occurs when AWS has detected exposed credentials. It attaches a policy to deny certain actions, effectively quarantining those credentials, and is accompanied by a support case with instructions for detaching the policy.
Runbook: Determine the IAM user who owns the key, rotate/disable/delete the key, and then investigate which actions were taken by the compromised key to determine scope and if any remediation is needed.
Reference: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is PutUserPolicy
  • requestParameters.policyName is one of AWSExposedCredentialPolicy_DO_NOT_REMOVE, AWSCompromisedKeyQuarantine, AWSCompromisedKeyQuarantineV2, AWSCompromisedKeyQuarantineV3

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNameeq
  • PutUserPolicy
field:"aws::eventName" kind:eq value:"PutUserPolicy"
requestParameters.policyNamein
  • AWSCompromisedKeyQuarantine
  • AWSCompromisedKeyQuarantineV2
  • AWSCompromisedKeyQuarantineV3
  • AWSExposedCredentialPolicy_DO_NOT_REMOVE
field:"requestParameters.policyName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
userNameuserIdentity.userName
accessKeyIduserIdentity.accessKeyId

Response runbook

Determine the IAM user who owns the key, rotate/disable/delete the key, and then investigate which actions were taken by the compromised key to determine scope and if any remediation is needed.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1c2a53d1-58cc-41b3-85b8-bd7565370e0d",
  "eventName": "PutUserPolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2020-04-10T06:22:08Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "27ca92a5-61cc-44aa-b875-042a25310064",
  "requestParameters": {
    "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"Stmt1538161409\",\"Effect\":\"Deny\",\"Action\":[\"lambda:CreateFunction\",\"iam:AttachUserPolicy\",\"iam:PutUserPolicy\",\"organizations:InviteAccountToOrganization\",\"ec2:RunInstances\",\"iam:DetachUserPolicy\",\"iam:CreateUser\",\"lightsail:Create*\",\"lightsail:Update*\",\"ec2:StartInstances\",\"ec2:RequestSpotInstances\",\"iam:ChangePassword\",\"iam:CreateLoginProfile\",\"organizations:CreateOrganization\",\"organizations:CreateAccount\",\"lightsail:Delete*\",\"iam:AttachGroupPolicy\",\"iam:CreateAccessKey\",\"iam:UpdateUser\",\"iam:UpdateAccountPasswordPolicy\",\"iam:DeleteUserPolicy\",\"iam:PutUserPermissionsBoundary\",\"iam:UpdateAccessKey\",\"lightsail:DownloadDefaultKeyPair\",\"iam:CreateInstanceProfile\",\"lightsail:Start*\",\"lightsail:GetInstanceAccessDetails\",\"iam:CreateRole\",\"iam:PutGroupPolicy\",\"iam:AttachRolePolicy\"],\"Resource\":[\"*\"]}]}",
    "policyName": "AWSCompromisedKeyQuarantineV3",
    "userName": "compromised_user"
  },
  "responseElements": null,
  "sourceIPAddress": "72.21.217.97",
  "userAgent": "aws-internal/3 aws-sdk-java/1.11.706 Linux/4.9.184-0.1.ac.235.83.329.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.242-b08 java/1.8.0_242 vendor/Oracle_Corporation",
  "userIdentity": {
    "accessKeyId": "XXXXXXXXXXXXXXXXXXXXX",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/compromised_user",
    "principalId": "XXXXXXXXXXXXXXXXXXX",
    "type": "IAMUser",
    "userName": "compromised_user"
  }
}

AWS IAM Group Read Only Events

#
Severity
informational
Group by
userIdentity.arn
Log types
AWS.CloudTrail
Tags
AWS, Cloudtrail, Configuration Required, IAM, MITRE
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

This rule captures multiple read/list events related to IAM group management in AWS Cloudtrail.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

# arn allow list to suppress alerts
ARN_ALLOW_LIST = []

GROUP_ACTIONS = [
    "GetGroup",
    "GetGroupPolicy",
    "ListAttachedGroupPolicies",
    "ListGroupPolicies",
    "ListGroups",
    "ListGroupsForUser",
]


def rule(event):
    event_arn = event.get("userIdentity", {}).get("arn", "<NO_ARN_FOUND>")
    # Return True if arn not in whitelist and event source is iam and event name is
    # present in read/list event_name list.
    if (
        event_arn not in ARN_ALLOW_LIST
        and event.get("eventSource", "<NO_EVENT_SOURCE_FOUND>") == "iam.amazonaws.com"
        and event.get("eventName", "<NO_EVENT_NAME_FOUND>") in GROUP_ACTIONS
    ):
        # continue on with analysis
        return True
    return False


def title(event):
    return (
        f"{event.get('userIdentity',{}).get('arn','<NO_ARN_FOUND>')} "
        f"IAM user group activity event found: {event.get('eventName', '<NO_EVENT_NAME_FOUND>')} "
        f"in account {event.get('recipientAccountId', '<NO_RECIPIENT_ACCT_ID_FOUND>')} "
        f"in region {event.get('awsRegion', '<NO_AWS_REGION_FOUND>')}."
    )


def dedup(event):
    # dedup via arn value
    return f"{event.get('userIdentity',{}).get('arn','<NO_ARN_FOUND>')}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: This rule captures multiple read/list events related to IAM group management in AWS Cloudtrail.
DisplayName: "AWS IAM Group Read Only Events"
Enabled: false
Filename: aws_iam_group_read_only_events.py
Reference: https://attack.mitre.org/techniques/T1069/
Runbook: Examine other activities done by this user to determine whether or not activity is suspicious.
Severity: Info
CreateAlert: false
Tags:
  - AWS
  - Cloudtrail
  - Configuration Required
  - IAM
  - MITRE
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.IAM.Group.Read.Only.Events"
Threshold: 2

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • userIdentity.arn is not one of
  • eventSource is iam.amazonaws.com
  • eventName is one of GetGroup, GetGroupPolicy, ListAttachedGroupPolicies, ListGroupPolicies, ListGroups (+1 more values, see Indicators below)
Alert cadence
alerts after 2 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.arnin(no value, null check)excludes:userIdentity.arn

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • GetGroup
  • GetGroupPolicy
  • ListAttachedGroupPolicies
  • ListGroupPolicies
  • ListGroups
  • ListGroupsForUser
field:"aws::eventName" kind:in
eventSourceeq
  • iam.amazonaws.com
field:"aws::eventSource" kind:eq value:"iam.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

Examine other activities done by this user to determine whether or not activity is suspicious.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "883efb94-aa58-4512-beb7-10a5fffa33e4",
  "eventName": "GetGroup",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2022-12-11 19:42:55",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "1231231234",
  "requestID": "f92dd1a7-ad07-4fef-9511-1081d2dd3585",
  "requestParameters": {
    "maxItems": 1000,
    "userName": "user-name"
  },
  "sourceIPAddress": "cloudformation.amazonaws.com",
  "userAgent": "cloudformation.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "ASIAVKVYIOO7BDL4T5NG",
    "accountId": "1231231234",
    "arn": "arn:aws:sts::1231231234:assumed-role/AssumedRole-us-east-2/123123123456",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "AROAVKVYIOO7JN7TN7NSA:123123123456",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-12-11T19:42:54Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "1231231234",
        "arn": "arn:aws:iam::1231231234:role/PAssumedRole-us-east-2",
        "principalId": "AROAVKVYIOO7JN7TN7NSA",
        "type": "Role",
        "userName": "AssumedRole-us-east-2"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS IAM Group Users

#
Severity
low
Resource types
AWS.IAM.Group
Tags
AWS, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This Policy ensures that all IAM groups have at least one IAM user. If they are vacant, they should be deleted.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

def policy(resource):
    return resource["Users"] is not None

Rule specification

AnalysisType: policy
Filename: aws_iam_group_users.py
PolicyID: "AWS.IAM.Group.Users"
DisplayName: "AWS IAM Group Users"
Enabled: true
ResourceTypes:
  - AWS.IAM.Group
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Low
Description: >
  This Policy ensures that all IAM groups have at least one IAM user. If they are vacant,
  they should be deleted.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-group-has-users
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.IAM.Group resources when the condition below holds.

Condition

  • Users is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Usersis_not_null(no value, null check)excludes:Users

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Usersis_null
  • (no value, null check)
field:"Users" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-group-has-users

AWS IAM Password Unused

#
Severity
low
Compliance
CIS 1.3; PCI 8.1.4
Resource types
AWS.IAM.User
Tags
AWS, Identity & Access Management
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates IAM users with console passwords have logged in within the past 90 days.

Detection logic

import datetime

from panther_base_helpers import resolve_timestamp_string

TIMEOUT_DAYS = datetime.timedelta(days=90)
DEFAULT_TIME = "0001-01-01T00:00:00Z"


def aged_out(timestamp):
    datetime_ts = resolve_timestamp_string(timestamp)
    if not datetime_ts:
        return False
    return (datetime.datetime.now() - datetime_ts) > TIMEOUT_DAYS


def policy(resource):
    # If a user is less than 4 hours old, it may not have a credential report generated yet.
    # It will be re-scanned periodically until a credential report is found, at which point this
    # policy will be properly evaluated.
    report = resource.get("CredentialReport")
    if not report:
        return True

    if report.get("PasswordEnabled"):
        if report.get("PasswordLastUsed") != DEFAULT_TIME and aged_out(
            report.get("PasswordLastUsed")
        ):
            return False
        if report.get("PasswordLastUsed") == DEFAULT_TIME and aged_out(
            report.get("PasswordLastChanged")
        ):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_password_unused.py
PolicyID: "AWS.Password.Unused"
DisplayName: "AWS IAM Password Unused"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
Tags:
  - AWS
  - Identity & Access Management
Reports:
  CIS:
    - 1.3
  PCI:
    - 8.1.4
Severity: Low
Description: >
  This policy validates IAM users with console passwords have logged in within the past 90 days.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-password-used-every-90-days
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.IAM.User resources when the condition below holds.

Condition

  • CredentialReport is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReportis_null(no value, null check)excludes:CredentialReport

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CredentialReportis_not_null
  • (no value, null check)
field:"CredentialReport" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-password-used-every-90-days

AWS IAM Policy Administrative Privileges

#
Severity
high
Compliance
CIS 1.22
Resource types
AWS.IAM.Policy
Tags
AWS, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that there are no IAM policies that grant full administrative privileges to IAM users or groups.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

import json

from panther_base_helpers import listify


def policy(resource):
    iam_policy = json.loads(resource["PolicyDocument"])
    statements = listify(iam_policy["Statement"])
    for state in statements:
        actions = listify(state.get("Action", []))
        resources = listify(state.get("Resource", []))
        if state["Effect"] == "Allow" and "*" in actions and "*" in resources:
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_administrative_privileges.py
PolicyID: "AWS.IAM.Policy.AdministrativePrivileges"
DisplayName: "AWS IAM Policy Administrative Privileges"
Enabled: true
ResourceTypes:
  - AWS.IAM.Policy
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 1.22
  MITRE ATT&CK:
    - TA0004:T1078
Severity: High
Description: >
  This policy validates that there are no IAM policies that grant full administrative
  privileges to IAM users or groups.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-does-not-grant-full-administrative-privileges
Reference: >
  https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-does-not-grant-full-administrative-privileges

AWS IAM Policy Assigned to User

#
Severity
low
Compliance
CIS 1.16; PCI 2.2.4, 7.1.1
Resource types
AWS.IAM.User
Tags
AWS, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

This policy validates that there are no IAM policies assigned directly to users. Best practice suggests assigning to an IAM group and placing users within that group.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

def policy(resource):
    return resource["InlinePolicies"] is None and resource["ManagedPolicyNames"] is None

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_assigned_to_user.py
PolicyID: "AWS.IAM.Policy.AssignedToUser"
DisplayName: "AWS IAM Policy Assigned to User"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 1.16
  PCI:
    - 2.2.4
    - 7.1.1
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Low
Description: >
  This policy validates that there are no IAM policies assigned directly to users.
  Best practice suggests assigning to an IAM group and placing users within that group.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-not-assigned-directly-to-user
Reference: https://amzn.to/2Pss7Ln

Stages and Predicates

Flags AWS.IAM.User resources when any of the conditions below holds.

Condition

  • any of:
    • InlinePolicies is present
    • ManagedPolicyNames is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InlinePoliciesis_null(no value, null check)excludes:InlinePolicies
ManagedPolicyNamesis_null(no value, null check)excludes:ManagedPolicyNames

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InlinePoliciesis_not_null
  • (no value, null check)
field:"InlinePolicies" kind:is_not_null
ManagedPolicyNamesis_not_null
  • (no value, null check)
field:"ManagedPolicyNames" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-not-assigned-directly-to-user

AWS IAM Policy Blocklist

#
Severity
medium
Resource types
AWS.IAM.Group, AWS.IAM.Role, AWS.IAM.User
Tags
AWS, Configuration Required, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This detects the usage of highly permissive IAM Policies that should only be assigned to a small number of users, roles, or groups.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

# IAM policy ARN's list in this variable will be checked against the policies assigned to all IAM
# resources (users, groups, roles) and this rule will fail if any policy ARN in the blocklist is
# assigned to a user
IAM_POLICY_ARN_BLOCKLIST = [
    "TEST_BLOCKLISTED_ARN",
]


def policy(resource):
    # Check if the IAM resource has any managed policies
    if resource["ManagedPolicyNames"] is None:
        return True

    # Iterate through the blocklist and return true if the resource is in violation
    for iam_policy in IAM_POLICY_ARN_BLOCKLIST:
        if iam_policy in resource["ManagedPolicyNames"]:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_blocklist.py
PolicyID: "AWS.IAM.Policy.Blacklist"
DisplayName: "AWS IAM Policy Blocklist"
Enabled: false
ResourceTypes:
  - AWS.IAM.Group
  - AWS.IAM.Role
  - AWS.IAM.User
Reports:
  MITRE ATT&CK:
    - TA0004:T1078
Tags:
  - AWS
  - Configuration Required
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Severity: Medium
Description: >
  This detects the usage of highly permissive IAM Policies that should only be assigned to a small number of users, roles, or groups.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-blacklist-is-respected
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Flags AWS.IAM.Group, AWS.IAM.Role, AWS.IAM.User resources when the condition below holds.

Condition

  • ManagedPolicyNames is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ManagedPolicyNamesis_null(no value, null check)excludes:ManagedPolicyNames

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ManagedPolicyNamesis_not_null
  • (no value, null check)
field:"ManagedPolicyNames" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-blacklist-is-respected

AWS IAM Policy Does Not Grant Any Administrative Access

#
Severity
medium
Compliance
PCI 2.2.4, 7.1.2
Resource types
AWS.IAM.Policy
Tags
AWS, PCI, Security, Identity & Compliance, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that no IAM policies grant admin access. This should be combined with suppressions on the legitimate IAM admin policies in your account so that it only fires when new and unexpected policies granting admin access are created.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

import json

from policyuniverse.expander_minimizer import expand_policy
from policyuniverse.policy import Policy

# White listed policies (e.g. the approved admin policies) can be specified here, or as an
# exception to this policy.

ADMIN_ACTIONS = {
    "Permissions",
}


def policy(resource):
    iam_policy = Policy(expand_policy(json.loads(resource["PolicyDocument"])))
    action_summary = iam_policy.action_summary()

    # Sometimes AWS Service Linked Roles+Policies violate the
    # expectation as expressed in policyuniverse.
    # service-linked roles have a path of /aws-service-role/
    #   service-linked roles and their policies are not editable
    # service-role is editable, and so not included
    if resource.get("Path", "") == "/aws-service-role/":
        return True

    # Check if the policy grants any administrative privileges
    return not any(
        ADMIN_ACTIONS.intersection(action_summary[service]) for service in action_summary
    )

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_does_not_grant_admin_access.py
PolicyID: "AWS.IAM.Policy.DoesNotGrantAdminAccess"
DisplayName: "AWS IAM Policy Does Not Grant Any Administrative Access"
Enabled: true
ResourceTypes:
  - AWS.IAM.Policy
Tags:
  - AWS
  - PCI
  - Security, Identity & Compliance
  - Privilege Escalation:Valid Accounts
Reports:
  PCI:
    - 2.2.4
    - 7.1.2
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Medium
Description: >
  This policy validates that no IAM policies grant admin access. This should be combined with suppressions on the legitimate IAM admin policies in your account so that it only fires when new and unexpected policies granting admin access are created.
Runbook: Delete the unapproved IAM admin policy.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Flags AWS.IAM.Policy resources when the condition below holds.

Condition

  • Path is not /aws-service-role/

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Patheq/aws-service-role/excludes:Path field:"Path" value:"/aws-service-role/"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Pathne
  • /aws-service-role/
field:"Path" kind:ne value:"/aws-service-role/"

Response runbook

Delete the unapproved IAM admin policy.

AWS IAM Policy Does Not Grant Network Admin Access

#
Severity
medium
Compliance
PCI 1.1.5
Resource types
AWS.IAM.Policy
Tags
AWS, PCI, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that no IAM policies grant admin privileges on network resources. This should be used in conjunction with suppressions for the legitimate network admin policies in your account.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

import json

from policyuniverse.action_categories import categories_for_actions
from policyuniverse.expander_minimizer import expand_policy
from policyuniverse.policy import Policy

# White listed policies (e.g. the approved network admin policy) can be specified here, or as an
# exception to this policy.

ADMIN_ACTIONS = {
    "Tagging",
    "Write",
}
NETWORK_RESOURCES = {
    "vpc",
    "securitygroup",
    "networkacl",
    "routetable",
}


def policy(resource):
    iam_policy = Policy(expand_policy(json.loads(resource["PolicyDocument"])))
    action_summary = iam_policy.action_summary()

    #
    # These first two checks can technically be skipped and this policy will still return correct
    # results, but they prevent the more computationally expensive check the majority of the time.
    #

    # Check if the policy applies to EC2 resources
    if "ec2" not in action_summary:
        return True

    # Sometimes AWS Service Linked Roles+Policies violate the
    # expectation as expressed in policyuniverse.
    # service-linked roles have a path of /aws-service-role/
    #   service-linked roles and their policies are not editable
    # service-role is editable, and so not included
    if resource.get("Path", "") == "/aws-service-role/":
        return True

    # Check if the policy grants administrative privileges
    if not ADMIN_ACTIONS.intersection(action_summary["ec2"]):
        return True

    # Get the EC2 actions pertaining specifically to network resources
    network_actions = set()
    for statement in iam_policy.statements:
        # Only check statements granting access
        if statement.effect != "Allow":
            continue
        # Only check actions that are granted on network resources
        for action in statement.actions:
            if any(resource in action for resource in NETWORK_RESOURCES):
                network_actions.add(action)

    # For all actions that have been granted on network resources, ensure none grant admin access
    network_actions_summary = categories_for_actions(network_actions)
    return not any(action in ADMIN_ACTIONS for action in network_actions_summary["ec2"])

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_does_not_grant_network_admin_access.py
PolicyID: "AWS.IAM.Policy.DoesNotGrantNetworkAdminAccess"
DisplayName: "AWS IAM Policy Does Not Grant Network Admin Access"
Enabled: true
ResourceTypes:
  - AWS.IAM.Policy
Tags:
  - AWS
  - PCI
  - Privilege Escalation:Valid Accounts
Reports:
  PCI:
    - 1.1.5
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Medium
Description: >
  This policy validates that no IAM policies grant admin privileges on network resources. This should be used in conjunction with suppressions for the legitimate network admin policies in your account.
Runbook: >
  Delete the unapproved network admin policy.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Flags AWS.IAM.Policy resources when the condition below holds.

Condition

  • Path is not /aws-service-role/

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Patheq/aws-service-role/excludes:Path field:"Path" value:"/aws-service-role/"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Pathne
  • /aws-service-role/
field:"Path" kind:ne value:"/aws-service-role/"

Response runbook

Delete the unapproved network admin policy.

AWS IAM Policy Role Mapping

#
Severity
high
Resource types
AWS.IAM.Policy
Tags
AWS, Configuration Required, Identity & Access Management
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that policies that have been explicitly configured to be set to certain roles are still attached to those roles.

Detection logic

from panther_base_helpers import deep_get

# This is a mapping of what policies must be attached to what roles in the account.
# The mapping is keyed by string policy names to tuples of role names. Ordering doesn't matter.
# Example:
# POLICY_ROLE_MAPPINGS = {
#   'ExamplePolicyName1': ('ExampleRoleName1', 'ExampleRoleName2', 'ExampleRoleName3'),
#   'ExamplePolicyName2': ('ExampleRoleName1', 'ExampleRoleName3', 'ExampleRoleName4'),
# }
POLICY_ROLE_MAPPINGS = {
    "TestPolicyName": ("TestRole1", "TestRole2"),
}


def policy(resource):
    # Check if there are any required roles for this policy to be attached to
    if resource["PolicyName"] not in POLICY_ROLE_MAPPINGS:
        return True

    # Check if this policy is attached to any roles
    if deep_get(resource, "Entities", "PolicyRoles") is None:
        return False

    # Build the list of role names this policy is actually attached to
    roles_attached = [
        role["RoleName"] for role in deep_get(resource, "Entities", "PolicyRoles", default=[])
    ]

    # For each required role, ensure that role has the policy attached
    for role_needed in POLICY_ROLE_MAPPINGS[resource["PolicyName"]]:
        if role_needed not in roles_attached:
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_policy_role_mapping.py
PolicyID: "AWS.IAM.Policy.RoleMapping"
DisplayName: "AWS IAM Policy Role Mapping"
Enabled: false
ResourceTypes:
  - AWS.IAM.Policy
Tags:
  - AWS
  - Configuration Required
  - Identity & Access Management
Severity: High
Description: >
  This policy validates that policies that have been explicitly configured to be set to certain roles are still attached to those roles.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-role-mapping-is-respected
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html

Stages and Predicates

Flags AWS.IAM.Policy resources when the condition below holds.

Condition

  • PolicyName is one of TestPolicyName

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PolicyNameeqTestPolicyNameexcludes:PolicyName field:"PolicyName" value:"TestPolicyName"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-policy-role-mapping-is-respected

AWS IAM Resource Does Not Have Inline Policy

#
Severity
medium
Compliance
PCI 2.2.4, 7.1.1
Resource types
AWS.IAM.User, AWS.IAM.Group
Tags
AWS, PCI, Persistence:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that no IAM entities have inline policies assigned. Inline policies are more difficult to administer and audit, and may lead to access that lasts longer than intended.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

def policy(resource):
    return not resource["InlinePolicies"]

Rule specification

AnalysisType: policy
Filename: aws_iam_resource_does_not_have_inline_policy.py
PolicyID: "AWS.IAM.Resource.DoesNotHaveInlinePolicy"
DisplayName: "AWS IAM Resource Does Not Have Inline Policy"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
  - AWS.IAM.Group
Tags:
  - AWS
  - PCI
  - Persistence:Valid Accounts
Reports:
  PCI:
    - 2.2.4
    - 7.1.1
  MITRE ATT&CK:
    - TA0003:T1078
Severity: Medium
Description: >
  This policy validates that no IAM entities have inline policies assigned. Inline policies are more difficult to administer and audit, and may lead to access that lasts longer than intended.
Runbook: >
  Delete the inline policy, and if the permissions are required add an appropriate IAM managed policy to the entity.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html

Stages and Predicates

Flags AWS.IAM.User, AWS.IAM.Group resources when the condition below holds.

Condition

  • InlinePolicies is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InlinePoliciesis_null(no value, null check)excludes:InlinePolicies

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InlinePoliciesis_not_null
  • (no value, null check)
field:"InlinePolicies" kind:is_not_null

Response runbook

Delete the inline policy, and if the permissions are required add an appropriate IAM managed policy to the entity.

AWS IAM Role Grants (permission) to Non-organizational Account

#
Severity
critical
Resource types
AWS.IAM.Role
Tags
AWS, Identity & Access Management, Configuration Required
Source
github.com/panther-labs/panther-analysis

This policy validates that IAM roles that grant the (specified) permission do not allow accounts outside the organization to assume them.

Detection logic

import json

from botocore.exceptions import NoCredentialsError
from panther_aws_helpers import BadLookup, resource_lookup

# This is a list of the account numbers included in the organization
# Example:
# accounts = [
#   '123456789012',
#   '123456789013'
# ]

# account *12 is used as an organizational account in built-in unit tests
# account *13 is used as a non-organizational account
accounts = [
    "123456789012",
]

# The specific permission that the policy checks
# CONFIGURATION_REQUIRED: replace default "lambda:AddPermission" in unit tests
#  with specified permission
PERMISSION = "lambda:AddPermission"

# CONFIGURATION_REQUIRED: modify policy to contain specified permission, above
mock_policy_has_permission = json.loads("""
{
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "lambda:AddPermission",
                    "lambda:GetPolicy"
                ],
                "Resource": "*"
            }
        ]
    }
}
""")

mock_policy_no_permission = json.loads("""
{
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "lambda:ListAliases",
                "Resource": "*"
            }
        ]
    }
}
""")


# check to see if the account that is granted permission is third party
def check_account(resource):
    content_assumerole = resource.get("AssumeRolePolicyDocument")
    if isinstance(content_assumerole, str):
        content_assumerole = json.loads(content_assumerole)
    principal = content_assumerole["Statement"][0]["Principal"]
    if "AWS" in principal.keys():
        if isinstance(principal["AWS"], list):
            for principal_aws in principal["AWS"]:
                if not check_account_number(principal_aws):
                    return False
        else:
            return check_account_number(principal["AWS"])
    return True


def check_account_number(principal_aws):
    if principal_aws.split(":")[4] not in accounts:
        return False
    return True


def check_policy(policy_text):
    if isinstance(policy_text, str):
        policy_text = json.loads(policy_text)
    for statement in policy_text.get("Statement", []):
        if PERMISSION in statement.get("Action", []):
            return False
    return True


def policy(resource):
    # pylint: disable=too-complex
    if not check_account(resource):
        for policy_text in (resource.get("InlinePolicies") or {}).values():
            if not check_policy(policy_text):
                return False

        for managed_policy_name in resource.get("ManagedPolicyNames") or []:
            managed_policy_id = f"arn:aws:iam::{resource['AccountId']}:policy/{managed_policy_name}"
            try:
                # CONFIGURATION REQUIRED
                # to mock a Managed Policy
                #   - create mock policy, above
                #   - add '“IsUnitTest": true,' to test resource in .yml
                #   - add an additional 'key: value' pair and 'if/elif' block for each
                #     mocked case to .yml
                # Note: all Managed Policies in the test resource will be mocked
                # comment out 'if, else' block to optimize for production use
                if not resource.get("IsUnitTest"):
                    managed_policy = resource_lookup(managed_policy_id)
                else:
                    if resource.get("HasPermission"):
                        managed_policy = mock_policy_has_permission
                    elif resource.get("DoesNotHavePermission"):
                        managed_policy = mock_policy_no_permission
                    else:
                        return True
                # uncomment next line to optimize for production use
                # managed_policy = resource_lookup(managed_policy_id)
            except BadLookup:
                return True
            except NoCredentialsError:
                return True

            policy_text = managed_policy.get("PolicyDocument")
            if not check_policy(policy_text):
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_role_external_permission.py
PolicyID: "AWS.IAM.Role.ExternalPermission"
DisplayName: "AWS IAM Role Grants (permission) to Non-organizational Account"
Enabled: false
ResourceTypes:
  - AWS.IAM.Role
Tags:
  - AWS
  - Identity & Access Management
  - Configuration Required
Severity: Critical
Description: >
  This policy validates that IAM roles that grant the (specified) permission do not allow accounts outside the organization to assume them.

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

AWS IAM Role Restricts Usage

#
Severity
medium
Compliance
PCI 2.2.4
Resource types
AWS.IAM.Role
Tags
AWS, Security, Identity & Compliance, PCI, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that IAM roles in the account are restrictive in what entities may assume them. This can help prevent malicious actors from assuming roles they should not be assuming.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

import json

from policyuniverse.policy import Policy

BAD_PRINCIPALS = {
    "*",
}


def policy(resource):
    if resource["AssumeRolePolicyDocument"] is None:
        return True

    iam_policy = Policy(json.loads(resource["AssumeRolePolicyDocument"]))

    for statement in iam_policy.statements:
        # Only apply to allow effects
        if statement.effect != "Allow":
            continue

        # Don't apply where there are strong conditions
        if statement.condition_entries:
            continue

        if BAD_PRINCIPALS.intersection(statement.principals):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_role_restricts_usage.py
PolicyID: "AWS.IAM.Role.RestrictsUsage"
DisplayName: "AWS IAM Role Restricts Usage"
Enabled: true
ResourceTypes:
  - AWS.IAM.Role
Tags:
  - AWS
  - Security, Identity & Compliance
  - PCI
  - Privilege Escalation:Valid Accounts
Reports:
  PCI:
    - 2.2.4
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Medium
Description: >
  This policy validates that IAM roles in the account are restrictive in what entities may assume them. This can help prevent malicious actors from assuming roles they should not be assuming.
Runbook: Assign an appropriate assume role policy to the IAM role.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_permissions-to-switch.html

Stages and Predicates

Flags AWS.IAM.Role resources when the condition below holds.

Condition

  • AssumeRolePolicyDocument is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AssumeRolePolicyDocumentis_null(no value, null check)excludes:AssumeRolePolicyDocument

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AssumeRolePolicyDocumentis_not_null
  • (no value, null check)
field:"AssumeRolePolicyDocument" kind:is_not_null

Response runbook

Assign an appropriate assume role policy to the IAM role.

AWS IAM Role Trust Relationship for GitHub Actions

#
Severity
high
Resource types
AWS.IAM.Role
Tags
AWS, GitHub Actions, Identity & Access Management, Configuration Required
Reference
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html - https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers - https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
Source
github.com/panther-labs/panther-analysis

This policy ensures that IAM roles used with GitHub Actions are securely configured to prevent unauthorized access to AWS resources. It validates trust relationships by checking for proper audience (aud) restrictions, ensuring it is set to sts.amazonaws.com, and subject (sub) conditions, confirming they are scoped to specific repositories or environments. Misconfigurations, such as overly permissive wildcards or missing conditions, can allow unauthorized repositories to assume roles, leading to potential data breaches or compliance violations. By enforcing these checks, the policy mitigates risks of exploitation, enhances security posture, and protects critical AWS resources from external threats.

Detection logic

import json

from panther_base_helpers import deep_get

# Add/remove pairs as desired
ALLOWED_ORG_REPO_PAIRS = ["org/repo", "allowed-org-example/allowed-repo-example"]


def policy(resource):
    # check if resource.AssumRolePolicyDocument is a string, and if so convert to json
    if isinstance(resource.get("AssumeRolePolicyDocument"), str):
        policy_document = json.loads(resource.get("AssumeRolePolicyDocument", {}))
    else:
        policy_document = resource.get("AssumeRolePolicyDocument", {})
    assume_role_policy = policy_document.get("Statement", [])

    for statement in assume_role_policy:
        # only check for Allow sts:AssumeRoleWithWebIdentity
        if (
            statement.get("Effect") != "Allow"
            or statement.get("Action") != "sts:AssumeRoleWithWebIdentity"
        ):
            continue

        principal = deep_get(statement, "Principal", "Federated")
        audience = deep_get(
            statement, "Condition", "StringEquals", "token.actions.githubusercontent.com:aud"
        )
        subject = deep_get(
            statement,
            "Condition",
            "StringLike",
            "token.actions.githubusercontent.com:sub",
            default="",
        ) or deep_get(
            statement,
            "Condition",
            "StringEquals",
            "token.actions.githubusercontent.com:sub",
            default="",
        )

        if subject.startswith("repo:"):
            # repo subjects must have github as the principal and sts.amazonaws.com as the audience
            if any(
                [
                    "oidc-provider/token.actions.githubusercontent.com" not in principal,
                    audience != "sts.amazonaws.com",
                    (
                        "*" in subject
                        and not any(
                            subject.startswith(f"repo:{org_repo}:*")
                            for org_repo in ALLOWED_ORG_REPO_PAIRS
                        )
                    ),
                ]
            ):
                return False
        else:
            # non-repo subjects must not have github as the principal
            if "oidc-provider/token.actions.githubusercontent.com" in principal:
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_role_github_actions_trust.py
PolicyID: "AWS.IAM.Role.GitHubActionsTrust"
DisplayName: "AWS IAM Role Trust Relationship for GitHub Actions"
Enabled: false
ResourceTypes:
  - AWS.IAM.Role
Tags:
  - AWS
  - GitHub Actions
  - Identity & Access Management
  - Configuration Required
Severity: High
Description: >
  This policy ensures that IAM roles used with GitHub Actions are securely configured to prevent unauthorized access to AWS resources. 
  It validates trust relationships by checking for proper audience (aud) restrictions, ensuring it is set to sts.amazonaws.com, and subject (sub) conditions, 
  confirming they are scoped to specific repositories or environments. Misconfigurations, such as overly permissive wildcards or missing conditions, 
  can allow unauthorized repositories to assume roles, leading to potential data breaches or compliance violations. 
  By enforcing these checks, the policy mitigates risks of exploitation, enhances security posture, and protects critical AWS resources from external threats.
Runbook: >
  To fix roles flagged by this policy:
  1. Update the trust relationship of the flagged IAM role in the AWS Management Console or CLI.
  2. Add a Condition block with 'StringLike' or 'StringEquals' for 'token.actions.githubusercontent.com:sub'.
  3. Ensure the audience is set to 'sts.amazonaws.com'.
  4. Avoid overly permissive wildcards in the sub condition.
Reference: >
  - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html
  - https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers
  - https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

To fix roles flagged by this policy: 1. Update the trust relationship of the flagged IAM role in the AWS Management Console or CLI. 2. Add a Condition block with 'StringLike' or 'StringEquals' for 'token.actions.githubusercontent.com:sub'. 3. Ensure the audience is set to 'sts.amazonaws.com'. 4. Avoid overly permissive wildcards in the sub condition.

AWS IAM User MFA

#
Severity
high
Compliance
CIS 1.2; PCI 8.3.1, 8.3.2
Resource types
AWS.IAM.User
Tags
AWS, Identity & Access Management, Credential Access:Brute Force
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all AWS IAM users with access to the AWS Console have Multi-Factor Authentication (MFA) enabled.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    # If password logins are disabled, we don't need to worry about MFA
    if not deep_get(resource, "CredentialReport", "PasswordEnabled"):
        return True

    # Explicit True check to avoid returning NoneType
    return deep_get(resource, "CredentialReport", "MfaActive") is True

Rule specification

AnalysisType: policy
Filename: aws_iam_user_mfa.py
PolicyID: "AWS.IAM.User.MFA"
DisplayName: "AWS IAM User MFA "
Enabled: true
ResourceTypes:
  - AWS.IAM.User
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Brute Force
Reports:
  CIS:
    - 1.2
  PCI:
    - 8.3.1
    - 8.3.2
  MITRE ATT&CK:
    - TA0006:T1110
Severity: High
Description: >
  This policy validates that all AWS IAM users with access to the AWS Console have Multi-Factor Authentication (MFA) enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-user-has-mfa-enabled
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable.html

Stages and Predicates

Flags AWS.IAM.User resources when all of the conditions below hold.

Condition

  • CredentialReport.PasswordEnabled is present
  • CredentialReport.MfaActive is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReport.MfaActiveeqtrueexcludes:CredentialReport.MfaActive field:"CredentialReport.MfaActive" value:"true"
CredentialReport.PasswordEnabledis_null(no value, null check)excludes:CredentialReport.PasswordEnabled

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-iam-user-has-mfa-enabled

AWS IAM User Not In Conflicting Groups

#
Severity
medium
Compliance
PCI 7.2.2
Resource types
AWS.IAM.User
Tags
AWS, Configuration Required, Security, Identity & Compliance, PCI, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that IAM users are not in IAM groups that are considered mutually exclusive. For example, in some workflows developers are responsible for dev environments and sysadmins are responsible for prod environments. In this situation no (or very few) users should be in both sysadmin and developer groups. This is in following with the principle of least privilege.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

# This policy ensures that users do not belong to groups that should be exclusive.
# A common example would be the Developer group and the Production admin group, as in tightly
# controlled environments developers should not be able to deploy to production directly and
# sysadmins should not have access to developmental source code.
#
# GROUP_CONFLICTS is formatted as a list of sets. Each inner set contains mutually exclusive groups.
GROUP_CONFLICTS = [
    {"PROD_ADMIN", "DEV"},
]


def policy(resource):
    group_names = {group["GroupName"] for group in resource["Groups"] or []}

    # If the user is in more than one group in a mutually exclusive set, return False
    for conflict_set in GROUP_CONFLICTS:
        if len(group_names.intersection(conflict_set)) > 1:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_user_not_in_conflicting_groups.py
PolicyID: "AWS.IAM.User.NotInConflictingGroups"
DisplayName: "AWS IAM User Not In Conflicting Groups"
Enabled: false
ResourceTypes:
  - AWS.IAM.User
Tags:
  - AWS
  - Configuration Required
  - Security, Identity & Compliance
  - PCI
  - Privilege Escalation:Valid Accounts
Reports:
  PCI:
    - 7.2.2
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Medium
Description: >
  This policy validates that IAM users are not in IAM groups that are considered mutually exclusive. For example, in some workflows developers are responsible for dev environments and sysadmins are responsible for prod environments. In this situation no (or very few) users should be in both sysadmin and developer groups. This is in following with the principle of least privilege.
Runbook: Remove the IAM user from one of the conflicting groups.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_groups.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Remove the IAM user from one of the conflicting groups.

AWS IMDS Credential Usage Outside Expected Services

#
Status
Experimental
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, AWS STS, Privilege Escalation:Valid Accounts, Defense Evasion:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when an EC2 instance identity (credentials obtained via IMDS) is used to make API calls outside of expected internal AWS services like SSM. This indicates that IMDS credentials may have been exfiltrated from a compromised instance and are being used externally for lateral movement or privilege escalation.

MITRE ATT&CK coverage

Detection logic

import ipaddress
import re

from panther_aws_helpers import aws_rule_context
from panther_ipinfo_helpers import get_ipinfo_asn

# IMDS credentials appear as assumed-role sessions where the session name
# is an EC2 instance ID (i-xxxxxxxxxxxxxxxxx)
INSTANCE_SESSION_PATTERN = re.compile(r":assumed-role/.+/i-[0-9a-f]+$")

# Legitimate internal services and actions that use instance identity
INTERNAL_SOURCES = {
    "ssm.amazonaws.com",
    "ec2messages.amazonaws.com",  # SSM agent heartbeat/polling channel
    "ssmmessages.amazonaws.com",  # SSM Session Manager channel
}
INTERNAL_EVENTS = {"RegisterManagedInstance"}
INTERNAL_IPS = {"AWS Internal"}


def _is_valid_ip(ip_str):
    try:
        ipaddress.ip_address(ip_str)
        return True
    except ValueError:
        return False


def _is_private_ip(ip_str):
    try:
        return ipaddress.ip_address(ip_str).is_private
    except ValueError:
        return False


def _is_amazon_domain(event):
    """Check if the source IP's ipinfo ASN enrichment resolves to an amazon.com domain."""
    ipinfo_asn = get_ipinfo_asn(event)
    if not ipinfo_asn:
        return False
    return ipinfo_asn.domain("sourceIPAddress") == "amazon.com"


def rule(event):
    arn = event.deep_get("userIdentity", "arn", default="")
    if not INSTANCE_SESSION_PATTERN.search(arn):
        return False
    # Exclude calls made by an AWS service on behalf of the instance (e.g. EKS, ECS, CodeDeploy)
    if event.deep_get("userIdentity", "invokedBy", default="").endswith(".amazonaws.com"):
        return False
    # Exclude legitimate internal services
    if event.get("eventSource") in INTERNAL_SOURCES:
        return False
    if event.get("eventName") in INTERNAL_EVENTS:
        return False
    # Only alert when credentials are used from a public IP.
    # Filters out "AWS Internal", service hostnames (e.g. "eks.amazonaws.com"), and private VPC IPs.
    source_ip = event.get("sourceIPAddress", "")
    if source_ip in INTERNAL_IPS or source_ip.endswith(".amazonaws.com"):
        return False
    return _is_valid_ip(source_ip) and not _is_private_ip(source_ip)


def severity(event):
    # Source IPs that resolve to an amazon.com ASN domain are lower risk
    # (e.g. AWS-managed infrastructure making calls on the instance's behalf)
    if _is_amazon_domain(event):
        return "INFO"
    return "DEFAULT"


def title(event):
    arn = event.deep_get("userIdentity", "arn", default="<unknown>")
    ip_addr = event.get("sourceIPAddress", "<unknown>")
    action = event.get("eventName", "<unknown>")
    return (
        f"IMDS instance credential [{arn}] used from [{ip_addr}] "
        f"to call [{event.get('eventSource', '')}:{action}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_imds_credential_exfiltration.py
RuleID: "AWS.CloudTrail.IMDSCredentialExfiltration"
DisplayName: "AWS IMDS Credential Usage Outside Expected Services"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - AWS STS
  - Privilege Escalation:Valid Accounts
  - Defense Evasion:Valid Accounts
Reports:
  MITRE ATT&CK:
    - TA0004:T1078.004
    - TA0005:T1078.004
Status: Experimental
Severity: High
Description: >
  Detects when an EC2 instance identity (credentials obtained via IMDS) is used to
  make API calls outside of expected internal AWS services like SSM. This indicates
  that IMDS credentials may have been exfiltrated from a compromised instance and
  are being used externally for lateral movement or privilege escalation.
Runbook: |
  1. Query CloudTrail for all API calls by userIdentity:arn in the 24 hours before and after this alert to identify the full scope of actions taken with the exfiltrated instance credentials
  2. Check if sourceIPAddress is external to AWS or outside the expected VPC CIDR ranges for the EC2 instance associated with this role
  3. Find all other alerts associated with this userIdentity:arn or sourceIPAddress in the past 7 days to assess whether this is part of a broader compromise campaign
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • userIdentity.invokedBy does not end with .amazonaws.com
  • eventSource is not one of ssm.amazonaws.com, ec2messages.amazonaws.com, ssmmessages.amazonaws.com
  • eventName is not one of RegisterManagedInstance
  • sourceIPAddress is not one of AWS Internal
  • sourceIPAddress does not end with .amazonaws.com

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

1. Query CloudTrail for all API calls by userIdentity:arn in the 24 hours before and after this alert to identify the full scope of actions taken with the exfiltrated instance credentials

2. Check if sourceIPAddress is external to AWS or outside the expected VPC CIDR ranges for the EC2 instance associated with this role

3. Find all other alerts associated with this userIdentity:arn or sourceIPAddress in the past 7 days to assess whether this is part of a broader compromise campaign

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "DescribeInstances",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-01-15T10:30:00Z",
  "eventType": "AwsApiCall",
  "recipientAccountId": "123456789012",
  "sourceIPAddress": "45.33.32.156",
  "userAgent": "aws-cli/2.15.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
    "principalId": "AROAEXAMPLE:i-0abcdef1234567890",
    "sessionContext": {
      "sessionIssuer": {
        "arn": "arn:aws:iam::123456789012:role/MyAppRole",
        "type": "Role"
      }
    },
    "type": "AssumedRole"
  }
}

AWS KMS CMK Key Rotation

#
Severity
low
Group by
AccountId
Compliance
CIS 2.8; PCI 3.5.2
Resource types
AWS.KMS.Key
Tags
AWS, Identity & Access Management, Credential Access:Unsecured Credentials
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that customer master keys (CMKs) have automatic key rotation enabled.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def policy(resource):
    # per AWS Docs automatic rotation is only supported on managed symmetric keys
    # These are of Origin AWS_KMS
    if resource.get("Origin") != "AWS_KMS":
        return True
    return (  # Ignore AWS managed keys
        resource.get("KeyManager") != "CUSTOMER"  # Check that the KeyRotation exists
        # Explicit True check to avoid returning NoneType
        or (resource.get("KeyRotationEnabled") is True and resource.get("KeyState") == "Enabled")
    )


def dedup(resource):
    return f"AWS KMS CMK Key Rotation - Account {resource.get('AccountId')}"

Rule specification

AnalysisType: policy
Filename: aws_cmk_key_rotation.py
PolicyID: "AWS.CMK.KeyRotation"
DisplayName: "AWS KMS CMK Key Rotation"
Enabled: true
ResourceTypes:
  - AWS.KMS.Key
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Unsecured Credentials
Reports:
  CIS:
    - 2.8
  PCI:
    - 3.5.2
  MITRE ATT&CK:
    - TA0006:T1552
Severity: Low
Description: >
  This policy validates that customer master keys (CMKs) have automatic key rotation enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-customer-created-cmk-has-key-rotation-enabled
Reference: >
  https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html

Stages and Predicates

Flags AWS.KMS.Key resources when all of the conditions below hold.

Condition

  • Origin is AWS_KMS
  • KeyManager is CUSTOMER
  • any of:
    • KeyRotationEnabled is not true
    • KeyState is not Enabled

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-customer-created-cmk-has-key-rotation-enabled

AWS KMS Key Restricts Usage

#
Severity
high
Compliance
PCI 3.5.2
Resource types
AWS.KMS.Key
Tags
AWS, PCI, Credential Access:Unsecured Credentials
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that KMS Keys restrict what entities can use them and how. This is to ensure that encryption keys are limited in who can use them in order to prevent unapproved decryption.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

import json

from policyuniverse.policy import Policy

BAD_PRINCIPALS = {
    "*",
}

BAD_ACTIONS = {
    "*",
    "kms:*",
}


def policy(resource):
    if resource["Policy"] is None:
        return True

    iam_policy = Policy(json.loads(resource["Policy"]))

    for statement in iam_policy.statements:
        # Only apply to allow effects
        if statement.effect != "Allow":
            continue

        # Don't apply where there are strong conditions
        if statement.condition_entries:
            continue

        if BAD_PRINCIPALS.intersection(statement.principals) and BAD_ACTIONS.intersection(
            statement.actions
        ):
            return False
        if statement.uses_not_principal():
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_kms_key_policy_restricts_usage.py
PolicyID: "AWS.KMS.RestrictsUsage"
DisplayName: "AWS KMS Key Restricts Usage"
Enabled: true
ResourceTypes:
  - AWS.KMS.Key
Tags:
  - AWS
  - PCI
  - Credential Access:Unsecured Credentials
Reports:
  PCI:
    - 3.5.2
  MITRE ATT&CK:
    - TA0006:T1552
Severity: High
Description: >
  This policy validates that KMS Keys restrict what entities can use them and how. This is to ensure that encryption keys are limited in who can use them in order to prevent unapproved decryption.
Runbook: Add appropriate limitations to the KMS Key's policy.
Reference: https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html

Stages and Predicates

Flags AWS.KMS.Key resources when the condition below holds.

Condition

  • Policy is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_not_null
  • (no value, null check)
field:"Policy" kind:is_not_null

Response runbook

Add appropriate limitations to the KMS Key's policy.

AWS Lambda Public Access

#
Severity
high
Resource types
AWS.Lambda.Function
Tags
AWS, Data Protection
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that the function policy attached to the Lambda resource prohibits public access

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

import json

from panther_base_helpers import deep_get


def policy(resource):
    json_policy = json.loads(deep_get(resource, "Policy", "Policy", default="{}"))
    if any(
        (statement.get("Principal") == "*" or deep_get(statement, "Principal", "AWS") == "*")
        and statement.get("Effect") == "Allow"
        and (
            statement.get("Condition", {}) == {}
            or deep_get(statement, "Condition", "StringEquals", "lambda:FunctionUrlAuthType")
            == "NONE"
        )
        for statement in json_policy.get("Statement", [])
    ):
        return False
    return True


def severity(resource):
    json_policy = json.loads(deep_get(resource, "Policy", "Policy", default="{}"))
    if not any(
        deep_get(statement, "Condition", "StringEquals", "lambda:FunctionUrlAuthType") == "NONE"
        for statement in json_policy.get("Statement", [])
    ):
        return "LOW"
    return "DEFAULT"

Rule specification

AnalysisType: policy
Filename: aws_lambda_public_access.py
PolicyID: "AWS.Lambda.PublicAccess"
DisplayName: "AWS Lambda Public Access"
Enabled: true
ResourceTypes:
  - AWS.Lambda.Function
Tags:
  - AWS
  - Data Protection
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
Severity: High
Description: >
  This policy ensures that the function policy attached to the Lambda resource prohibits public access
Reference: https://docs.aws.amazon.com/config/latest/developerguide/lambda-function-public-access-prohibited.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

AWS Macie Disabled/Updated

#
Severity
medium
Entities
aws_account_ids, aws_arns, ip_addresses, trace_ids
Log types
AWS.CloudTrail
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Amazon Macie is a data security and data privacy service to discover and protect sensitive data. Security teams use Macie to detect open S3 Buckets that could have potentially sensitive data in it along with policy violations, such as missing Encryption. If an attacker disables Macie, it could potentially hide data exfiltration.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Detection logic

from panther_base_helpers import pattern_match

MACIE_EVENTS = {
    "ArchiveFindings",
    "CreateFindingsFilter",
    "DeleteMember",
    "DisassociateFromMasterAccount",
    "DisassociateMember",
    "DisableMacie",
    "DisableOrganizationAdminAccount",
    "UpdateFindingsFilter",
    "UpdateMacieSession",
    "UpdateMemberSession",
    "UpdateClassificationJob",
}


def rule(event):
    return event.get("eventName") in MACIE_EVENTS and pattern_match(
        event.get("eventSource"), "macie*.amazonaws.com"
    )


def title(event):
    account = event.get("recipientAccountId")
    user_arn = event.deep_get("userIdentity", "arn")
    return f"AWS Macie in AWS Account [{account}] Disabled/Updated by [{user_arn}]"

Rule specification

AnalysisType: rule
Filename: aws_macie_evasion.py
RuleID: "AWS.Macie.Evasion"
DisplayName: "AWS Macie Disabled/Updated"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - "TA0005:T1562" # Tactic ID:Technique ID (https://attack.mitre.org/tactics/enterprise/)
Severity: Medium
Description: >
  Amazon Macie is a data security and data privacy service to discover and protect sensitive data.
  Security teams use Macie to detect open S3 Buckets that could have potentially sensitive data in it along with
  policy violations, such as missing Encryption. If an attacker disables Macie, it could potentially hide data exfiltration.
Reference: https://aws.amazon.com/macie/
Runbook: |
  Analyze the events to ensure it's not normal maintenance.
  If it's abnormal, run the Indicator Search on the UserIdentity:Arn for the past hour and analyze other services accessed/changed.
DedupPeriodMinutes: 60
Threshold: 5
SummaryAttributes:
  - awsRegion
  - eventName
  - p_any_aws_arns
  - p_any_ip_addresses
  - userIdentity:type
  - userIdentity:arn

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is one of ArchiveFindings, CreateFindingsFilter, DeleteMember, DisassociateFromMasterAccount, DisassociateMember (+6 more values, see Indicators below)
  • eventSource matches the pattern macie*.amazonaws.com
Alert cadence
alerts after 5 matches within 1h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • ArchiveFindings
  • CreateFindingsFilter
  • DeleteMember
  • DisableMacie
  • DisableOrganizationAdminAccount
  • DisassociateFromMasterAccount
  • DisassociateMember
  • UpdateClassificationJob
  • UpdateFindingsFilter
  • UpdateMacieSession
  • UpdateMemberSession
field:"aws::eventName" kind:in
eventSourcewildcard
  • macie*.amazonaws.com
field:"aws::eventSource" kind:wildcard value:"macie*.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
recipientAccountId
arnuserIdentity.arn

Response runbook

Analyze the events to ensure it's not normal maintenance.

If it's abnormal, run the Indicator Search on the UserIdentity:Arn for the past hour and analyze other services accessed/changed.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-2",
  "eventCategory": "Management",
  "eventID": "63033dfd-08c9-42f3-80ae-dca45e86ae84",
  "eventName": "UpdateMacieSession",
  "eventSource": "macie2.amazonaws.com",
  "eventTime": "2022-09-27 19:59:08",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123456789012"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123456789012:role/Admin",
    "arn:aws:sts::123456789012:assumed-role/Admin/Jack"
  ],
  "p_any_ip_addresses": [
    "46.91.25.204"
  ],
  "p_any_trace_ids": [
    "ASIASWJRT64Z42HFV6QX"
  ],
  "p_event_time": "2022-09-27 19:59:08",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-09-27 20:02:43.816",
  "p_row_id": "665d45a409cad7d68ff7bbd4138123",
  "p_source_id": "b00eb354-da7a-49dd-9cc6-32535e32096a",
  "p_source_label": "CloudTrail Test",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1b9981dc-21d2-4f77-92b0-69e23c8a40de",
  "requestParameters": {
    "findingPublishingFrequency": "SIX_HOURS"
  },
  "sourceIPAddress": "46.91.25.204",
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
  "userIdentity": {
    "accessKeyId": "ASIASWJRT64Z42HFV6QX",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/Admin/Jack",
    "principalId": "AAAAA44444LE6DYFKKKKK:Jack",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-27T17:56:01Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/Admin",
        "principalId": "AAAAA44444LE6DYFKKKKK",
        "type": "Role",
        "userName": "Admin"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Modify Cloud Compute Infrastructure

#
Severity
medium
Group by
requestParameters.instancesSet.items
Entities
aws_account_ids, aws_arns, aws_instance_ids, aws_tags, domain_names, ip_addresses, trace_ids, usernames
Log types
AWS.CloudTrail
Tags
Configuration Required
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

Detection when EC2 compute infrastructure is modified outside of expected automation methods.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

PlatformRecord / event type
AWSCloudTrail event AssociateIamInstanceProfile: Associates an IAM instance profile with a running or stopped EC2 instance.
AWSCloudTrail event CreateInstanceExportTask: Exports a running or stopped EC2 instance to an Amazon S3 bucket in OVA, VHD, or VMDK format.
AWSCloudTrail event ModifyInstanceAttribute: Modifies the specified attribute of the specified EC2 instance, such as instance type or user data.
AWSCloudTrail event StartInstances: Starts one or more stopped EC2 instances, transitioning them to the running state.
AWSCloudTrail event StopInstances: Stops one or more running EC2 instances, transitioning them to the stopped state.
AWSCloudTrail event AssociateInstanceEventWindow: Associates one or more targets with an event window.
AWSCloudTrail event BundleInstance: Bundles an Amazon instance store-backed Windows instance.
AWSCloudTrail event CancelSpotInstanceRequests: Cancels one or more Spot Instance requests.
AWSCloudTrail event ConfirmProductInstance: Determines whether a product code is associated with an instance.
AWSCloudTrail event CreateInstanceEventWindow: Creates an event window in which scheduled events for the associated Amazon EC2 instances can run.
AWSCloudTrail event DeleteInstanceEventWindow: Deletes the specified event window.
AWSCloudTrail event DeregisterInstanceEventNotificationAttributes: Deregisters tag keys to prevent tags that have the specified tag keys from being included in scheduled event notifications for resources in the Region.
AWSCloudTrail event DisassociateIamInstanceProfile: Disassociates an IAM instance profile from a running or stopped instance.
AWSCloudTrail event DisassociateInstanceEventWindow: Disassociates one or more targets from an event window.
AWSCloudTrail event ImportInstance: Creates an import instance task using metadata from the specified disk image.
AWSCloudTrail event ModifyInstanceCapacityReservationAttributes: Modifies the Capacity Reservation settings for a stopped instance.
AWSCloudTrail event ModifyInstanceCreditSpecification: Modifies the credit option for CPU usage on a running or stopped burstable performance instance.
AWSCloudTrail event ModifyInstanceEventStartTime: Modifies the start time for a scheduled Amazon EC2 instance event.
AWSCloudTrail event ModifyInstanceEventWindow: Modifies the specified event window.
AWSCloudTrail event ModifyInstanceMaintenanceOptions: Modifies the recovery behavior of your instance to disable simplified automatic recovery or set the recovery behavior to default.
AWSCloudTrail event ModifyInstanceMetadataOptions: Modify the instance metadata parameters on a running or stopped instance.
AWSCloudTrail event ModifyInstancePlacement: Set the instance affinity value for a specific stopped instance and modify the instance tenancy setting.
AWSCloudTrail event MonitorInstances: Enables monitoring for a running instance.
AWSCloudTrail event RegisterInstanceEventNotificationAttributes: Registers a set of tag keys to include in scheduled event notifications for your resources.
AWSCloudTrail event ReportInstanceStatus: Submits feedback about the status of an instance.
AWSCloudTrail event RequestSpotInstances: Creates a Spot Instance request.
AWSCloudTrail event ResetInstanceAttribute: Resets an attribute of an instance to its default value.
AWSCloudTrail event RunInstances: Launches the specified number of instances using an AMI for which you have permissions.
AWSCloudTrail event RunScheduledInstances: Launches the specified Scheduled Instances.
AWSCloudTrail event TerminateInstances: Shuts down one or more instances.
AWSCloudTrail event UnmonitorInstances: Disables monitoring for a running instance.

Rules detecting the same action

These rules filter on the same operation.

Detection logic

EC2_CRUD_ACTIONS = {
    "AssociateIamInstanceProfile",
    "AssociateInstanceEventWindow",
    "BundleInstance",
    "CancelSpotInstanceRequests",
    "ConfirmProductInstance",
    "CreateInstanceEventWindow",
    "CreateInstanceExportTask",
    "DeleteInstanceEventWindow",
    "DeregisterInstanceEventNotificationAttributes",
    "DisassociateIamInstanceProfile",
    "DisassociateInstanceEventWindow",
    "ImportInstance",
    "ModifyInstanceAttribute",
    "ModifyInstanceCapacityReservationAttributes",
    "ModifyInstanceCreditSpecification",
    "ModifyInstanceEventStartTime",
    "ModifyInstanceEventWindow",
    "ModifyInstanceMaintenanceOptions",
    "ModifyInstanceMetadataOptions",
    "ModifyInstancePlacement",
    "MonitorInstances",
    "RegisterInstanceEventNotificationAttributes",
    "ReportInstanceStatus",
    "RequestSpotInstances",
    "ResetInstanceAttribute",
    "RunInstances",
    "RunScheduledInstances",
    "StartInstances",
    "StopInstances",
    "TerminateInstances",
    "UnmonitorInstances",
}


def rule(event):
    # Disqualify any eventSource that is not ec2
    if event.get("eventSource", "") != "ec2.amazonaws.com":
        return False
    if event.get("readOnly"):
        return False
    # Disqualify AWS Service-Service operations, which can appear in a variety of forms
    if (
        # FYI there is a weird quirk in the sourceIPAddress field of CloudTrail
        #  events with ec2.amazonaws.com as the source name where users of the
        #  web-console will have their sourceIPAddress recorded as "AWS Internal"
        #  though their userIdentity will be more normal.
        #  Example cloudtrail event in the "Terminate instance From WebUI with assumedRole" test
        event.get("sourceIPAddress", "").endswith(".amazonaws.com")
        or event.deep_get("userIdentity", "type", default="") == "AWSService"
        or event.deep_get("userIdentity", "invokedBy", default="") == "AWS Internal"
        or event.deep_get("userIdentity", "invokedBy", default="").endswith(".amazonaws.com")
    ):
        return False
    # Dry run operations get logged as SES Internal in the sourceIPAddress
    #  but not in the invokedBy field
    if event.get("errorCode", "") == "Client.DryRunOperation":
        return False
    # Disqualify any eventNames that do not Include instance
    # and events that have readOnly set to false
    if event.get("eventName", "") in EC2_CRUD_ACTIONS:
        return True
    return False


def title(event):
    items = event.deep_get(
        "requestParameters", "instancesSet", "items", default=[{"instanceId": "none"}]
    )
    return (
        f"AWS Event [{event.get('eventName')}] Instance ID "
        f"[{items[0].get('instanceId')}] AWS Account ID [{event.get('recipientAccountId')}]"
    )


def dedup(event):
    items = event.deep_get(
        "requestParameters",
        "instancesSet",
        "items",
        default=[{"instanceId": "INSTANCE_ID_NOT_FOUND"}],
    )
    return items[0].get("instanceId", "INSTANCE_ID_NOT_FOUND")


def alert_context(event):
    items = event.deep_get(
        "requestParameters", "instancesSet", "items", default=[{"instanceId": "none"}]
    )
    return {
        "awsRegion": event.get("awsRegion"),
        "eventName": event.get("eventName"),
        "recipientAccountId": event.get("recipientAccountId"),
        "instanceId": items[0].get("instanceId"),
    }

Rule specification

AnalysisType: rule
Description: Detection when EC2 compute infrastructure is modified outside of expected automation methods.
DisplayName: "AWS Modify Cloud Compute Infrastructure"
Enabled: false
Filename: aws_modify_cloud_compute_infrastructure.py
Reference: https://attack.mitre.org/techniques/T1578/
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0005:T1578
Tags:
  # Note: This detection doesn't require configuration. It carries the Configuration Required
  # tag due to checks for Enabled:false and Pack content
  - Configuration Required
Runbook: |
  This detection reports on eventSource ec2 Change events. This detection excludes Cross-Service
  change events.  As such, this detection will perform well in environments where changes are
  expected to originate only from AWS service entities.

  This detection will emit alerts frequently in environments where users are
  making ec2 related changes.
DedupPeriodMinutes: 120
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.Modify.Cloud.Compute.Infrastructure"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is ec2.amazonaws.com
  • readOnly is empty
  • sourceIPAddress does not end with .amazonaws.com
  • userIdentity.type is not AWSService
  • userIdentity.invokedBy is not AWS Internal
  • userIdentity.invokedBy does not end with .amazonaws.com
  • errorCode is not Client.DryRunOperation
  • eventName is one of AssociateIamInstanceProfile, AssociateInstanceEventWindow, BundleInstance, CancelSpotInstanceRequests, ConfirmProductInstance (+26 more values, see Indicators below)
Alert deduplication
repeat matches within 2h group into one alert

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodene
  • Client.DryRunOperation
field:"aws::errorCode" kind:ne value:"Client.DryRunOperation"
eventNamein
  • AssociateIamInstanceProfile
  • AssociateInstanceEventWindow
  • BundleInstance
  • CancelSpotInstanceRequests
  • ConfirmProductInstance
  • CreateInstanceEventWindow
  • CreateInstanceExportTask
  • DeleteInstanceEventWindow
  • DeregisterInstanceEventNotificationAttributes
  • DisassociateIamInstanceProfile
  • DisassociateInstanceEventWindow
  • ImportInstance
  • ModifyInstanceAttribute
  • ModifyInstanceCapacityReservationAttributes
  • ModifyInstanceCreditSpecification
  • ModifyInstanceEventStartTime
  • ModifyInstanceEventWindow
  • ModifyInstanceMaintenanceOptions
  • ModifyInstanceMetadataOptions
  • ModifyInstancePlacement
  • MonitorInstances
  • RegisterInstanceEventNotificationAttributes
  • ReportInstanceStatus
  • RequestSpotInstances
  • ResetInstanceAttribute
  • RunInstances
  • RunScheduledInstances
  • StartInstances
  • StopInstances
  • TerminateInstances
  • UnmonitorInstances
field:"aws::eventName" kind:in
eventSourceeq
  • ec2.amazonaws.com
field:"aws::eventSource" kind:eq value:"ec2.amazonaws.com"
readOnlyis_null
  • (no value, null check)
field:"readOnly" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
awsRegion
eventName
recipientAccountId
itemsrequestParameters.instancesSet.items

Response runbook

This detection reports on eventSource ec2 Change events. This detection excludes Cross-Service

change events. As such, this detection will perform well in environments where changes are

expected to originate only from AWS service entities.

This detection will emit alerts frequently in environments where users are

making ec2 related changes.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "59e8d6b8-de7b-43ca-961f-0c6f4531fcf0",
  "eventName": "TerminateInstances",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2021-10-29 23:50:09",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "111222333444"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::111222333444:role/FakeRole"
  ],
  "p_any_aws_instance_ids": [
    "i-0d9853f67e40ab80b"
  ],
  "p_any_domain_names": [
    "ec2.amazonaws.com"
  ],
  "p_event_time": "2021-10-29 23:50:09",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2021-10-29 23:54:06.45",
  "p_row_id": "e6f7bd65083bfeb7feced38f0da18a01",
  "p_source_id": "5f9f0f60-9c56-4027-b93a-8bab3019f0f1",
  "p_source_label": "SomeCloudTrail",
  "readOnly": false,
  "recipientAccountId": "111222333444",
  "requestID": "a520eeaf-c258-4260-954e-b4a976e6c72b",
  "requestParameters": {
    "instancesSet": {
      "items": [
        {
          "instanceId": "i-0d9853f67e40ab80b"
        }
      ]
    }
  },
  "responseElements": {
    "instancesSet": {
      "items": [
        {
          "currentState": {
            "code": 32,
            "name": "shutting-down"
          },
          "instanceId": "i-0d9853f67e40ab80b",
          "previousState": {
            "code": 16,
            "name": "running"
          }
        }
      ],
      "requestId": "a520eeaf-c258-4260-954e-b4a976e6c72b"
    }
  },
  "userIdentity": {
    "accountId": "111222333444",
    "arn": "arn:aws:sts::111222333444:assumed-role/SomeRole/AThing",
    "sessionContext": {
      "attributes": {
        "creationDate": "2021-10-29T23:50:08Z",
        "mfaAuthenticated": "false"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Network ACL Overly Permissive Entry Created

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Persistence:Account Manipulation
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A Network ACL entry that allows access from anywhere was added.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    # Only check successful actions creating a new Network ACL entry
    if not aws_cloudtrail_success(event) or event.get("eventName") != "CreateNetworkAclEntry":
        return False

    # Check if this new NACL entry is allowing traffic from anywhere
    return (
        event.deep_get("requestParameters", "cidrBlock") == "0.0.0.0/0"
        and event.deep_get("requestParameters", "ruleAction") == "allow"
        and event.deep_get("requestParameters", "egress") is False
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_network_acl_permissive_entry.py
RuleID: "AWS.CloudTrail.NetworkACLPermissiveEntry"
DisplayName: "AWS Network ACL Overly Permissive Entry Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Persistence:Account Manipulation
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
Description: >
  A Network ACL entry that allows access from anywhere was added.
Runbook: >
  Remove the overly permissive Network ACL entry and add a new entry with more restrictive permissions.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html#nacl-rules
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is CreateNetworkAclEntry
  • requestParameters.cidrBlock is 0.0.0.0/0
  • requestParameters.ruleAction is allow
  • requestParameters.egress is false

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneCreateNetworkAclEntryexcludes:eventName field:"eventName" value:"CreateNetworkAclEntry"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Remove the overly permissive Network ACL entry and add a new entry with more restrictive permissions.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1111",
  "eventName": "CreateNetworkAclEntry",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "aclProtocol": "6",
    "cidrBlock": "0.0.0.0/0",
    "egress": false,
    "icmpTypeCode": {},
    "networkAclId": "acl-1111",
    "portRange": {
      "from": 700,
      "to": 702
    },
    "ruleAction": "allow",
    "ruleNumber": 12
  },
  "responseElements": {
    "_return": true,
    "requestId": "1111"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla/2.0 (compatible; NEWT ActiveX; Win32)",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Network ACL Restricts Inbound Traffic

#
Severity
medium
Compliance
PCI 1.3.5, 1.2.1, 1.1.4
Resource types
AWS.EC2.NetworkACL
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Network ACLs restrict inbound traffic in some way.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

# This is a generic policy for checking inbound rules on a Network ACL.
# It is recommended to add additional logic here based on your own use cases.


def policy(resource):

    for entry in resource["Entries"]:
        if entry["RuleAction"] == "allow" and not entry["Egress"]:
            # Check if entry is set to "All Ports"
            if entry["PortRange"] is None:
                return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_network_acl_restricts_inbound_traffic.py
PolicyID: "AWS.NetworkACL.RestrictsInboundTraffic"
DisplayName: "AWS Network ACL Restricts Inbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.NetworkACL
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 1.3.5
    - 1.2.1
    - 1.1.4
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  This policy validates that Network ACLs restrict inbound traffic in some way.
Runbook: >
  Add appropriate entries to the Network ACLs' IP permissions list.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-recommended-nacl-rules.html

Stages and Predicates

Flags AWS.EC2.NetworkACL resources when the condition below holds.

Condition

  • any element of Entries matches all of:
    • Entries.RuleAction is allow
    • Entries.Egress is empty
    • Entries.PortRange is empty

Response runbook

Add appropriate entries to the Network ACLs' IP permissions list.

AWS Network ACL Restricts Insecure Protocols

#
Severity
low
Compliance
PCI 8.2.1
Resource types
AWS.EC2.NetworkACL
Tags
AWS, PCI, Command and Control:Non-Application Layer Protocol
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Network ACLs block the usage of ports typically associated with insecure or unencrypted protocols.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Detection logic

# This is a list of default ports for insecure protocols. As AWS Network ACLs and Security Groups
# are not application layer aware, this is the closest approximation that can be made to blocking
# insecure protocols. Application layer firewalls can provide stronger protections.
INSECURE_PORTS = {
    21,  # FTP command channel
    25,  # Unencrypted pop3 outgoing
    23,  # Telnet
    80,  # HTTP
    110,  # Unencrypted pop3 incoming
    587,  # Unencrypted pop3 outgoing
}


def policy(resource):

    for entry in resource["Entries"]:
        # Look for ingress rules from any IP.
        # This could be modified in the future to inspect the size
        # of the source network with the ipaddress.ip_network.num_addresses call.
        if entry["Egress"]:
            continue

        # This indicates that all protocols are allowed, and the port range is ignored
        if entry["Protocol"] == "-1" or not entry["PortRange"]:
            return False

        if any(
            entry["PortRange"]["From"] <= port <= entry["PortRange"]["To"]
            for port in INSECURE_PORTS
        ):
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_network_acl_restricts_insecure_protocols.py
PolicyID: "AWS.NetworkACL.RestrictsInsecureProtocols"
DisplayName: "AWS Network ACL Restricts Insecure Protocols"
Enabled: false
ResourceTypes:
  - AWS.EC2.NetworkACL
Tags:
  - AWS
  - PCI
  - Command and Control:Non-Application Layer Protocol
Reports:
  PCI:
    - 8.2.1
  MITRE ATT&CK:
    - TA0011:T1095
Severity: Low
Description: >
  This policy validates that Network ACLs block the usage of ports typically associated with insecure or unencrypted protocols.
Runbook: Add Network ACL entries to block ports typically associated with insecure protocols.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-recommended-nacl-rules.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Add Network ACL entries to block ports typically associated with insecure protocols.

AWS Network ACL Restricts Outbound Traffic

#
Severity
low
Compliance
PCI 1.1.4, 1.3.2
Resource types
AWS.EC2.NetworkACL
Tags
AWS, PCI, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Network ACLs have some restrictions on outbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

# This is generic policy that checks outbound traffic rules on a Network ACL.
# It is recommended you add additional logic for your own use cases.


def policy(resource):

    for entry in resource["Entries"]:
        if entry["RuleAction"] == "allow" and entry["Egress"]:
            # Check if entry is set to "All Ports"
            if entry["PortRange"] is None:
                return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_network_acl_restricts_outbound_traffic.py
PolicyID: "AWS.NetworkACL.RestrictsOutboundTraffic"
DisplayName: "AWS Network ACL Restricts Outbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.NetworkACL
Tags:
  - AWS
  - PCI
  - Exfiltration:Exfiltration Over Web Service
Reports:
  PCI:
    - 1.1.4
    - 1.3.2
  MITRE ATT&CK:
    - TA0010:T1567
Severity: Low
Description: >
  This policy validates that Network ACLs have some restrictions on outbound traffic.
Runbook: >
  Add appropriate restrictions on outbound traffic.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-recommended-nacl-rules.html

Stages and Predicates

Flags AWS.EC2.NetworkACL resources when the condition below holds.

Condition

  • any element of Entries matches all of:
    • Entries.RuleAction is allow
    • Entries.Egress is present
    • Entries.PortRange is empty

Response runbook

Add appropriate restrictions on outbound traffic.

AWS Network ACL Restricts SSH

#
Severity
high
Resource types
AWS.EC2.NetworkACL
Tags
AWS, Panther, Lateral Movement:Remote Services
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

SSH access should only be granted from protected network CIDR ranges.

MITRE ATT&CK coverage

TacticTechniques
Lateral Movement

Detection logic

import ipaddress

GLOBAL_IPV6 = ipaddress.IPv6Network("::/0")
# Choose an arbitrary sentinel value that isn't equivalent to the GLOBAL_IPV6 value.
IPV6_SENTINEL = ipaddress.IPv6Network("::1/128")


def policy(resource):
    # Enumerate the entries in the network ACL, in evaluation order.
    ingress_entries = sorted(
        (entry for entry in resource["Entries"] if not entry["Egress"]),
        key=lambda x: x["RuleNumber"],
    )
    for entry in ingress_entries:
        # Look for SSH ingress rules from wildcard IPs.
        if (
            entry.get("CidrBlock") == "0.0.0.0/0"
            # Handle non-standard representations like `"0::/0"`.
            or ipaddress.IPv6Network(entry.get("Ipv6CidrBlock") or IPV6_SENTINEL) == GLOBAL_IPV6
        ) and (
            not entry.get("PortRange")
            or entry["PortRange"]["From"] <= 22 <= entry["PortRange"]["To"]
        ):
            # If this is a deny rule, then the ACL has an explicit deny rule with a lower (more
            # important) precedence than any rule that would allow SSH from arbitrary IPs. If it's
            # an allow rule, then the opposite is true. Either way, this rule determines the
            # entire outcome of the policy evaluation.
            #
            # Another way to read this: pass the policy check if the SSH rule here is a deny.
            return entry["RuleAction"] == "deny"

    # Found no SSH ingress rules from wildcard IPs.
    return True

Rule specification

AnalysisType: policy
Filename: aws_network_acl_restricted_ssh.py
PolicyID: "AWS.NetworkACL.RestrictedSSH"
DisplayName: "AWS Network ACL Restricts SSH"
Enabled: true
ResourceTypes:
  - AWS.EC2.NetworkACL
Tags:
  - AWS
  - Panther
  - Lateral Movement:Remote Services
Reports:
  MITRE ATT&CK:
    - TA0008:T1021
Severity: High
Description: >
  SSH access should only be granted from protected network CIDR ranges.
Runbook: >
  Remove the NACL rule granting unprotected SSH access.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-recommended-nacl-rules.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Remove the NACL rule granting unprotected SSH access.

AWS Password Policy Complexity Guidelines

#
Severity
high
Compliance
CIS 1.5, 1.6, 1.7, 1.8, 1.9; PCI 8.2.3
Resource types
AWS.PasswordPolicy
Tags
AWS, Identity & Access Management, Credential Access:Brute Force, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the account password policy enforces the recommended password complexity requirements.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

# if you want to enforce any of the password policies below, leave them commented.
IGNORED = {
    # "RequireUppercaseCharacters",
    # "RequireLowercaseCharacters",
    # "RequireSymbols",
    # "RequireNumbers",
}


def policy(resource):
    if (
        not resource.get("RequireUppercaseCharacters")
        and "RequireUppercaseCharacters" not in IGNORED
    ):
        return False

    if (
        not resource.get("RequireLowercaseCharacters")
        and "RequireLowercaseCharacters" not in IGNORED
    ):
        return False

    if not resource.get("RequireSymbols") and "RequireSymbols" not in IGNORED:
        return False

    if not resource.get("RequireNumbers") and "RequireNumbers" not in IGNORED:
        return False

    if (
        not (
            resource.get("MinimumPasswordLength") and resource.get("MinimumPasswordLength", 0) >= 14
        )
        and "MinimumPasswordLength" not in IGNORED
    ):
        return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_password_policy_complexity_guidelines.py
PolicyID: "AWS.PasswordPolicy.ComplexityGuidelines"
DisplayName: "AWS Password Policy Complexity Guidelines"
Enabled: true
ResourceTypes:
  - AWS.PasswordPolicy
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Brute Force
  - Configuration Required
Reports:
  CIS:
    - 1.5
    - 1.6
    - 1.7
    - 1.8
    - 1.9
  PCI:
    - 8.2.3
  MITRE ATT&CK:
    - TA0006:T1110
Severity: High
Description: >
  This policy validates that the account password policy enforces the recommended password complexity requirements.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-enforces-complexity-guidelines
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.PasswordPolicy resources when any of the conditions below holds.

Condition

  • any of:
    • RequireUppercaseCharacters is empty
    • RequireLowercaseCharacters is empty
    • RequireSymbols is empty
    • RequireNumbers is empty
    • MinimumPasswordLength is empty
    • MinimumPasswordLength is less than 14

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
MinimumPasswordLengthge14excludes:MinimumPasswordLength field:"MinimumPasswordLength" value:"14"
MinimumPasswordLengthis_not_null(no value, null check)excludes:MinimumPasswordLength
RequireLowercaseCharactersis_null(no value, null check)excludes:RequireLowercaseCharacters
RequireNumbersis_null(no value, null check)excludes:RequireNumbers
RequireSymbolsis_null(no value, null check)excludes:RequireSymbols
RequireUppercaseCharactersis_null(no value, null check)excludes:RequireUppercaseCharacters

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-enforces-complexity-guidelines

AWS Password Policy Password Age Limit

#
Severity
medium
Compliance
CIS 1.11; PCI 8.2.4
Resource types
AWS.PasswordPolicy
Tags
AWS, Identity & Access Management, Credential Access:Brute Force
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the account password policy enforces a maximum password age of 90 days or less.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def policy(resource):
    if resource["MaxPasswordAge"] is None:
        return False
    return resource["MaxPasswordAge"] <= 90

Rule specification

AnalysisType: policy
Filename: aws_password_policy_password_age_limit.py
PolicyID: "AWS.PasswordPolicy.PasswordAgeLimit"
DisplayName: "AWS Password Policy Password Age Limit"
Enabled: true
ResourceTypes:
  - AWS.PasswordPolicy
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Brute Force
Reports:
  CIS:
    - 1.11
  PCI:
    - 8.2.4
  MITRE ATT&CK:
    - TA0006:T1110
Severity: Medium
Description: >
  This policy validates that the account password policy enforces a maximum password age of 90
  days or less.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-enforces-password-age-limit-of-90-days-or-less
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.PasswordPolicy resources when any of the conditions below holds.

Condition

  • any of:
    • MaxPasswordAge is empty
    • MaxPasswordAge is greater than 90

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
MaxPasswordAgeis_not_null(no value, null check)excludes:MaxPasswordAge
MaxPasswordAgele90excludes:MaxPasswordAge field:"MaxPasswordAge" value:"90"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MaxPasswordAgegt
  • 90 transforms: number
field:"MaxPasswordAge" kind:gt value:"90"
MaxPasswordAgeis_null
  • (no value, null check)
field:"MaxPasswordAge" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-enforces-password-age-limit-of-90-days-or-less

AWS Password Policy Password Reuse

#
Severity
medium
Compliance
CIS 1.1; PCI 8.2.5
Resource types
AWS.PasswordPolicy
Tags
AWS, Identity & Access Management, Credential Access:Brute Force
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the account password policy prevents users from re-using previous passwords, and prevents password reuse for 24 or more prior passwords.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Detection logic

def policy(resource):
    if resource["PasswordReusePrevention"] is None:
        return False
    return resource["PasswordReusePrevention"] >= 24

Rule specification

AnalysisType: policy
Filename: aws_password_policy_password_reuse.py
PolicyID: "AWS.PasswordPolicy.PasswordReuse"
DisplayName: "AWS Password Policy Password Reuse"
Enabled: true
ResourceTypes:
  - AWS.PasswordPolicy
Tags:
  - AWS
  - Identity & Access Management
  - Credential Access:Brute Force
Reports:
  CIS:
    - 1.10
  PCI:
    - 8.2.5
  MITRE ATT&CK:
    - TA0006:T1110
Severity: Medium
Description: >
  This policy validates that the account password policy prevents users from re-using previous
  passwords, and prevents password reuse for 24 or more prior passwords.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-prevents-password-reuse
Reference: >
  https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.PasswordPolicy resources when any of the conditions below holds.

Condition

  • any of:
    • PasswordReusePrevention is empty
    • PasswordReusePrevention is less than 24

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PasswordReusePreventionge24excludes:PasswordReusePrevention field:"PasswordReusePrevention" value:"24"
PasswordReusePreventionis_not_null(no value, null check)excludes:PasswordReusePrevention

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-account-password-policy-prevents-password-reuse

AWS Potential Backdoor Lambda Function Through Resource-Based Policy

#
Status
Experimental
Severity
informational
Compliance
Stratus Red Team aws.persistence.lambda-backdoor-function
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Identifies when a permission is added to a Lambda function, which could indicate a potential security risk.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

ADD_PERMISSION_EVENTS = {
    "AddPermission20150331",
    "AddPermission20150331v2",
}


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "lambda.amazonaws.com"
        and event.get("eventName") in ADD_PERMISSION_EVENTS
    )


def title(event):
    lambda_name = event.deep_get(
        "requestParameters", "functionName", default="LAMBDA_NAME_NOT_FOUND"
    )
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"added permission to Lambda function [{lambda_name}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_backdoor_lambda_function.py
RuleID: "AWS.Potential.Backdoor.Lambda"
DisplayName: "AWS Potential Backdoor Lambda Function Through Resource-Based Policy"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0007:T1078

  Stratus Red Team:
    - aws.persistence.lambda-backdoor-function
Severity: Info
Status: Experimental
Description: >
  Identifies when a permission is added to a Lambda function, which could indicate a potential security risk.
Runbook: Make sure that the permission is legitimate and necessary. If not, remove the permission
Reference: https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is lambda.amazonaws.com
  • eventName is one of AddPermission20150331, AddPermission20150331v2

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AddPermission20150331
  • AddPermission20150331v2
field:"aws::eventName" kind:in
eventSourceeq
  • lambda.amazonaws.com
field:"aws::eventSource" kind:eq value:"lambda.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
functionNamerequestParameters.functionName

Response runbook

Make sure that the permission is legitimate and necessary. If not, remove the permission

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "AddPermission20150331",
  "eventSource": "lambda.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "p_log_type": "AWS.CloudTrail",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "functionName": "my-lambda-function"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Potentially Stolen Service Role

#
Severity
high
Tags
AWS
Source
github.com/panther-labs/panther-analysis

A role was assumed by an AWS service, followed by a user within 24 hours. This could indicate a stolen or compromised AWS service role.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(_):
    return True

Rule specification

AnalysisType: scheduled_rule
RuleID: "AWS.Potentially.Stolen.Service.Role.Scheduled"
DisplayName: "AWS Potentially Stolen Service Role"
Enabled: true
Tags:
    - AWS
Severity: High
Reports:
  MITRE ATT&CK:
    - TA0006:T1528 # Steal Application Access Token
Description: A role was assumed by an AWS service, followed by a user within 24 hours.  This could indicate a stolen or compromised AWS service role.
Filename: aws_potentially_compromised_service_role.py
ScheduledQueries:
  - "AWS Potentially Stolen Service Role"

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query AWS Potentially Stolen Service Role; its Python module (Detection logic above) shapes the alert rather than filtering.

AWS Potentially Stolen Service Role

#
Source
github.com/panther-labs/panther-analysis

A role was assumed by an AWS service, followed by a user within 24 hours. This could indicate a stolen or compromised AWS service role.

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
Description: A role was assumed by an AWS service, followed by a user within 24 hours.  This could indicate a stolen or compromised AWS service role.
Enabled: false
SnowflakeQuery: |
  SELECT
    requestParameters:roleArn AS role,
    ARRAY_AGG(distinct userIdentity:principalId) AS users,
    ARRAY_AGG(distinct userIdentity:type) AS types
  FROM
    panther_logs.public.aws_cloudtrail
  WHERE
    P_OCCURS_SINCE('1 day')
    AND eventName = 'AssumeRole'
    AND errorCode IS NULL
  GROUP BY role
  HAVING
    ARRAY_SIZE(types) > 1
    AND ARRAY_CONTAINS('AWSService'::VARIANT, types)
  LIMIT 100

DatabricksQuery: |
  SELECT
    requestParameters:roleArn AS role,
    COLLECT_SET(userIdentity:principalId) AS users,
    COLLECT_SET(userIdentity:type) AS types
  FROM
    panther_logs.aws_cloudtrail
  WHERE
    P_OCCURS_SINCE('1 day')
    AND eventName = 'AssumeRole'
    AND errorCode IS NULL
  GROUP BY role
  HAVING
    SIZE(types) > 1
    AND ARRAY_CONTAINS(types, 'AWSService')
  LIMIT 100
QueryName: "AWS Potentially Stolen Service Role"
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 5

Stages and Predicates

Stage 1: source

Table
panther_logs.public.aws_cloudtrail

Stage 2: filter

  • eventName is AssumeRole
  • errorCode is empty
Grouped by
role
Window
1d

Stage 3: having

aggregate comparison (>); threshold not fully parsed

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
rolerequestParameters:roleArn
usersARRAY_AGG ( DISTINCT userIdentity:principalId )
typesARRAY_AGG ( DISTINCT userIdentity:type )

AWS Privilege Escalation Via User Compromise

#
Severity
medium
Time window
30h
Match by
p_alert_context.ip_accessKeyId
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.Privilege.Escalation.Via.User.Compromise.Group"
DisplayName: "AWS Privilege Escalation Via User Compromise"
Enabled: false
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0004:T1098.001 # Additional Cloud Credentials
Detection:
    - Group:
        - ID: User Backdoored
          RuleID: AWS.IAM.Backdoor.User.Keys
        - ID: User Accessed
          RuleID: AWS.CloudTrail.UserAccessKeyAuth
      MatchCriteria:
        field_name:
          - GroupID: User Backdoored
            Match: p_alert_context.ip_accessKeyId
          - GroupID: User Accessed
            Match: p_alert_context.ip_accessKeyId
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 10
      LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.ip_accessKeyId. Each step needs one match unless a higher minimum is shown.

Stage 1: step User Backdoored

References detection AWS User API Key Created.

Stage 2: step User Accessed

References detection AWS.CloudTrail.UserAccessKeyAuth.

AWS Public RDS Restore

#
Severity
high
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects the recovery of a new public database instance from a snapshot. It may be part of data exfiltration.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if (
        event.get("eventSource", "") == "rds.amazonaws.com"
        and event.get("eventName", "") == "RestoreDBInstanceFromDBSnapshot"
    ):
        if event.deep_get("responseElements", "publiclyAccessible"):
            return True
    return False


def title(event):
    return f"Publicly Accessible RDS restore created in [{event.get('recipientAccountId','')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: Detects the recovery of a new public database instance from a snapshot. It may be part of data exfiltration.
DisplayName: "AWS Public RDS Restore"
Enabled: true
Filename: aws_rds_publicrestore.py
Reports:
  MITRE ATT&CK:
    - TA0010:T1020
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_RestoreFromSnapshot.html
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.RDS.PublicRestore"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is RestoreDBInstanceFromDBSnapshot
  • responseElements.publiclyAccessible is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "797163d3-5726-441d-80a7-6eeb7464acd4",
  "eventName": "RestoreDBInstanceFromDBSnapshot",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2018-07-30T22:14:06Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.04",
  "recipientAccountId": "123456789012",
  "requestID": "daf2e3f5-96a3-4df7-a026-863f96db793e",
  "requestParameters": {
    "allocatedStorage": 20,
    "dBInstanceClass": "db.m1.small",
    "dBInstanceIdentifier": "test-instance",
    "enableCloudwatchLogsExports": [
      "audit",
      "error",
      "general",
      "slowquery"
    ],
    "engine": "mysql",
    "masterUserPassword": "****",
    "masterUsername": "myawsuser"
  },
  "responseElements": {
    "allocatedStorage": 20,
    "autoMinorVersionUpgrade": true,
    "backupRetentionPeriod": 1,
    "cACertificateIdentifier": "rds-ca-2015",
    "copyTagsToSnapshot": false,
    "dBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:test-instance",
    "dBInstanceClass": "db.m1.small",
    "dBInstanceIdentifier": "test-instance",
    "dBInstanceStatus": "creating",
    "dBParameterGroups": [
      {
        "dBParameterGroupName": "default.mysql8.0",
        "parameterApplyStatus": "in-sync"
      }
    ],
    "dBSecurityGroups": [],
    "dBSubnetGroup": {
      "dBSubnetGroupDescription": "default",
      "dBSubnetGroupName": "default",
      "subnetGroupStatus": "Complete",
      "subnets": [
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1b"
          },
          "subnetIdentifier": "subnet-cbfff283",
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1e"
          },
          "subnetIdentifier": "subnet-d7c825e8",
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1f"
          },
          "subnetIdentifier": "subnet-6746046b",
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1c"
          },
          "subnetIdentifier": "subnet-bac383e0",
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1d"
          },
          "subnetIdentifier": "subnet-42599426",
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-east-1a"
          },
          "subnetIdentifier": "subnet-da327bf6",
          "subnetStatus": "Active"
        }
      ],
      "vpcId": "vpc-136a4c6a"
    },
    "dbInstancePort": 0,
    "dbiResourceId": "db-ETDZIIXHEWY5N7GXVC4SH7H5IA",
    "domainMemberships": [],
    "engine": "mysql",
    "engineVersion": "8.0.28",
    "iAMDatabaseAuthenticationEnabled": false,
    "licenseModel": "general-public-license",
    "masterUsername": "myawsuser",
    "monitoringInterval": 0,
    "multiAZ": false,
    "optionGroupMemberships": [
      {
        "optionGroupName": "default:mysql-8-0",
        "status": "in-sync"
      }
    ],
    "pendingModifiedValues": {
      "masterUserPassword": "****",
      "pendingCloudwatchLogsExports": {
        "logTypesToEnable": [
          "audit",
          "error",
          "general",
          "slowquery"
        ]
      }
    },
    "performanceInsightsEnabled": false,
    "preferredBackupWindow": "10:27-10:57",
    "preferredMaintenanceWindow": "sat:05:47-sat:06:17",
    "publiclyAccessible": true,
    "readReplicaDBInstanceIdentifiers": [],
    "storageEncrypted": false,
    "storageType": "standard",
    "vpcSecurityGroups": [
      {
        "status": "active",
        "vpcSecurityGroupId": "sg-f839b688"
      }
    ]
  },
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "aws-cli/1.15.42 Python/3.6.1 Darwin/17.7.0 botocore/1.10.42",
  "userIdentity": {
    "accessKeyId": "AKIAI44QH8DHBEXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/johndoe",
    "principalId": "AKIAIOSFODNN7EXAMPLE",
    "type": "IAMUser",
    "userName": "johndoe"
  }
}

AWS RDS Activity Stream Stopped

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impair Defenses, Disable Cloud Logs, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS Database Activity Streams are stopped. Activity Streams provide real-time monitoring of database activity. Disabling them is a clear evasion technique used by attackers to avoid detection before performing malicious operations.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    if event.get("eventName") != "StopActivityStream":
        return False
    return event.deep_get("errorCode") is None


def title(event):
    db_identifier = event.deep_get("requestParameters", "resourceArn", default="<UNKNOWN>").split(
        ":"
    )[-1]
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    return f"RDS Activity Stream Stopped: [{db_identifier}] by [{user}]"


def alert_context(event):
    context = aws_rule_context(event)
    context["resource_arn"] = event.deep_get("requestParameters", "resourceArn", default="N/A")
    context["apply_immediately"] = event.deep_get(
        "requestParameters", "applyImmediately", default="N/A"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_activity_stream_stopped.py
RuleID: "AWS.RDS.ActivityStreamStopped"
DisplayName: "AWS RDS Activity Stream Stopped"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impair Defenses
  - Disable Cloud Logs
  - RDS
Severity: High
Description: >
  Detects when RDS Database Activity Streams are stopped. Activity Streams provide real-time
  monitoring of database activity. Disabling them is a clear evasion technique used by
  attackers to avoid detection before performing malicious operations.
Runbook: |
  1. Find all RDS API calls by the user ARN in the 24 hours before the alert
  2. Check if activity streams have been stopped by this user in the past 90 days to determine if this is routine maintenance
  3. Look for sensitive database operations from this user in the 2 hours after the stream was stopped
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/DBActivityStreams.html
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.008  # Disable or Modify Cloud Logs
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:resourceArn
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is StopActivityStream
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
userNameuserIdentity.userName

Response runbook

1. Find all RDS API calls by the user ARN in the 24 hours before the alert

2. Check if activity streams have been stopped by this user in the past 90 days to determine if this is routine maintenance

3. Look for sensitive database operations from this user in the 2 hours after the stream was stopped

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "StopActivityStream",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-15T20:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "applyImmediately": true,
    "resourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:production-aurora"
  },
  "responseElements": {
    "kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
    "status": "stopping"
  },
  "sourceIPAddress": "203.0.113.45",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DeveloperRole/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "sessionContext": {
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/DeveloperRole",
        "principalId": "AIDAI23HXS3EXAMPLE",
        "type": "Role",
        "userName": "DeveloperRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS RDS Automated Backup Deleted

#
Severity
critical
Group by
awsRegion, recipientAccountId, requestParameters.dbiResourceId
Log types
AWS.CloudTrail
Tags
AWS, Impact, Defense Evasion, Data Destruction, Inhibit System Recovery, Ransomware, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects deletion of RDS automated backups. This is a classic ransomware tactic where attackers delete automated backups before encrypting or destroying databases to prevent recovery. Any automated backup deletion should be investigated immediately.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    deletion_events = ["DeleteDBInstanceAutomatedBackup", "DeleteDBClusterAutomatedBackup"]
    if event.get("eventName") not in deletion_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    backup_arn = event.deep_get("requestParameters", "dbiResourceId") or event.deep_get(
        "requestParameters", "dbClusterResourceId", default="<UNKNOWN>"
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = (
        "Automated Backup" if event_name == "DeleteDBInstanceAutomatedBackup" else "Cluster Backup"
    )
    return f"RDS {resource_type} Deleted: [{backup_arn}] by [{user}]"


def dedup(event):
    backup_id = event.deep_get("requestParameters", "dbiResourceId") or event.deep_get(
        "requestParameters", "dbClusterResourceId", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{backup_id}"


def alert_context(event):
    context = aws_rule_context(event)
    context["dbi_resource_id"] = event.deep_get("requestParameters", "dbiResourceId", default="N/A")
    context["db_cluster_resource_id"] = event.deep_get(
        "requestParameters", "dbClusterResourceId", default="N/A"
    )
    context["backup_arn"] = event.deep_get(
        "responseElements", "dBInstanceAutomatedBackupArn"
    ) or event.deep_get("responseElements", "dBClusterAutomatedBackupArn", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_automated_backup_deleted.py
RuleID: "AWS.RDS.AutomatedBackupDeleted"
DisplayName: "AWS RDS Automated Backup Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Impact
  - Defense Evasion
  - Data Destruction
  - Inhibit System Recovery
  - Ransomware
  - RDS
Severity: Critical
Description: >
  Detects deletion of RDS automated backups. This is a classic ransomware tactic where
  attackers delete automated backups before encrypting or destroying databases to prevent
  recovery. Any automated backup deletion should be investigated immediately.
Runbook: |
  1. Find all automated backup deletion events by the user ARN in the past 24 hours to identify bulk deletion patterns
  2. Check if this user has deleted automated backups in the past 90 days to determine if this is normal behavior
  3. Look for database deletion or modification events from this user in the 6 hours after this backup deletion
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html
Reports:
  MITRE ATT&CK:
    - TA0040:T1485  # Data Destruction
    - TA0040:T1490  # Inhibit System Recovery
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dbiResourceId
  - requestParameters:dbClusterResourceId
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of DeleteDBInstanceAutomatedBackup, DeleteDBClusterAutomatedBackup
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • DeleteDBClusterAutomatedBackup
  • DeleteDBInstanceAutomatedBackup
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dbiResourceIdrequestParameters.dbiResourceId
userNameuserIdentity.userName

Response runbook

1. Find all automated backup deletion events by the user ARN in the past 24 hours to identify bulk deletion patterns

2. Check if this user has deleted automated backups in the past 90 days to determine if this is normal behavior

3. Look for database deletion or modification events from this user in the 6 hours after this backup deletion

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "DeleteDBInstanceAutomatedBackup",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-16T10:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dbiResourceId": "db-ABCDEF1234567890"
  },
  "responseElements": {
    "allocatedStorage": 100,
    "dBInstanceAutomatedBackupArn": "arn:aws:rds:us-east-1:123456789012:auto-backup:ab-1234567890abcdef",
    "dbiResourceId": "db-ABCDEF1234567890",
    "engine": "mysql",
    "engineVersion": "8.0.35",
    "status": "deleting"
  },
  "sourceIPAddress": "185.220.101.50",
  "userAgent": "python-requests/2.28.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/CompromisedRole/attacker",
    "principalId": "AIDAI23HXS3EXAMPLE:attacker",
    "type": "AssumedRole"
  }
}

AWS RDS Cluster Failover Initiated

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Impact, Availability, Endpoint Denial of Service, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS cluster or global cluster failovers are manually initiated. Forced failovers cause brief service interruptions and may indicate disaster recovery testing, operational troubleshooting, or disruption attempts.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    failover_events = ["FailoverDBCluster", "FailoverGlobalCluster"]
    if event.get("eventName") not in failover_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    cluster_id = event.deep_get("requestParameters", "dBClusterIdentifier") or event.deep_get(
        "requestParameters", "globalClusterIdentifier", default="<UNKNOWN>"
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    cluster_type = "Cluster" if event_name == "FailoverDBCluster" else "Global Cluster"
    return f"RDS {cluster_type} Failover Initiated: [{cluster_id}] by [{user}]"


def alert_context(event):
    context = aws_rule_context(event)
    context["cluster_identifier"] = event.deep_get(
        "requestParameters", "dBClusterIdentifier"
    ) or event.deep_get("requestParameters", "globalClusterIdentifier", default="N/A")
    context["target_identifier"] = event.deep_get(
        "requestParameters", "targetDBInstanceIdentifier"
    ) or event.deep_get("requestParameters", "targetDbClusterIdentifier", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_cluster_failover.py
RuleID: "AWS.RDS.ClusterFailover"
DisplayName: "AWS RDS Cluster Failover Initiated"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Impact
  - Availability
  - Endpoint Denial of Service
  - RDS
Severity: Medium
Description: >
  Detects when RDS cluster or global cluster failovers are manually initiated. Forced
  failovers cause brief service interruptions and may indicate disaster recovery testing,
  operational troubleshooting, or disruption attempts.
Runbook: |
  1. Find all cluster failover events by the user ARN in the past 24 hours
  2. Check if this user has performed failovers in the past 90 days to determine if this is normal behavior
  3. Look for database health or modification events in the 30 minutes before the failover
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database-disaster-recovery.html
Reports:
  MITRE ATT&CK:
    - TA0040:T1499  # Endpoint Denial of Service
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBClusterIdentifier
  - requestParameters:globalClusterIdentifier
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of FailoverDBCluster, FailoverGlobalCluster
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • FailoverDBCluster
  • FailoverGlobalCluster
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBClusterIdentifierrequestParameters.dBClusterIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all cluster failover events by the user ARN in the past 24 hours

2. Check if this user has performed failovers in the past 90 days to determine if this is normal behavior

3. Look for database health or modification events in the 30 minutes before the failover

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "FailoverDBCluster",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-18T03:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBClusterIdentifier": "aurora-cluster-prod",
    "targetDBInstanceIdentifier": "aurora-instance-2"
  },
  "responseElements": {
    "dBClusterIdentifier": "aurora-cluster-prod",
    "status": "failing-over"
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DBARole/admin",
    "principalId": "AIDAI23HXS3EXAMPLE:admin",
    "type": "AssumedRole"
  }
}

AWS RDS Deletion Protection Disabled

#
Severity
high
Group by
awsRegion, recipientAccountId, requestParameters.dBInstanceIdentifier
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact, Impair Defenses, Inhibit System Recovery, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when deletion protection is disabled on an RDS instance or cluster. This is often a precursor to database deletion and may indicate ransomware or data destruction attacks where attackers first disable protections before deleting resources.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rds_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    modify_events = ["ModifyDBInstance", "ModifyDBCluster"]
    if event.get("eventName") not in modify_events:
        return False
    if event.deep_get("errorCode") is not None:
        return False
    deletion_protection = event.deep_get("requestParameters", "deletionProtection", default=None)
    if deletion_protection is False:
        return True
    return False


def title(event):
    event_name = event.get("eventName", "Unknown")
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="<UNKNOWN>"
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = "Instance" if event_name == "ModifyDBInstance" else "Cluster"
    return f"RDS {resource_type} Deletion Protection Disabled: [{db_identifier}] by [{user}]"


def dedup(event):
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{db_identifier}"


def alert_context(event):
    context = aws_rds_context(event)
    context["deletion_protection"] = event.deep_get(
        "requestParameters", "deletionProtection", default="N/A"
    )
    context["apply_immediately"] = event.deep_get(
        "requestParameters", "applyImmediately", default="N/A"
    )
    backup_retention = event.deep_get("requestParameters", "backupRetentionPeriod", default=None)
    if backup_retention is not None:
        context["backup_retention_period"] = backup_retention
    publicly_accessible = event.deep_get("requestParameters", "publiclyAccessible", default=None)
    if publicly_accessible is not None:
        context["publicly_accessible_changed"] = publicly_accessible
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_deletion_protection_disabled.py
RuleID: "AWS.RDS.DeletionProtectionDisabled"
DisplayName: "AWS RDS Deletion Protection Disabled"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact
  - Impair Defenses
  - Inhibit System Recovery
  - RDS
Severity: High
Description: >
  Detects when deletion protection is disabled on an RDS instance or cluster. This is often
  a precursor to database deletion and may indicate ransomware or data destruction attacks
  where attackers first disable protections before deleting resources.
Runbook: |
  1. Find all RDS modification events by the user ARN in the 24 hours before the alert
  2. Check if this user has disabled deletion protection in the past 90 days to determine if this is normal behavior
  3. Look for database deletion attempts from this user in the 2 hours after this modification
  5. Immediately re-enable deletion protection if unauthorized using ModifyDBInstance or ModifyDBCluster with deletionProtection:true
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html
Reports:
  MITRE ATT&CK:
    - TA0005:T1562  # Impair Defenses
    - TA0040:T1490  # Inhibit System Recovery
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:dBClusterIdentifier
  - requestParameters:deletionProtection
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of ModifyDBInstance, ModifyDBCluster
  • errorCode is empty
  • requestParameters.deletionProtection is false

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all RDS modification events by the user ARN in the 24 hours before the alert

2. Check if this user has disabled deletion protection in the past 90 days to determine if this is normal behavior

3. Look for database deletion attempts from this user in the 2 hours after this modification

5. Immediately re-enable deletion protection if unauthorized using ModifyDBInstance or ModifyDBCluster with deletionProtection:true

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "ModifyDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-15T18:20:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "applyImmediately": true,
    "dBInstanceIdentifier": "production-mysql",
    "deletionProtection": false
  },
  "responseElements": {
    "dBInstanceIdentifier": "production-mysql",
    "dBInstanceStatus": "modifying",
    "deletionProtection": false,
    "pendingModifiedValues": {
      "deletionProtection": false
    }
  },
  "sourceIPAddress": "203.0.113.45",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/PowerUserRole/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "sessionContext": {
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/PowerUserRole",
        "principalId": "AIDAI23HXS3EXAMPLE",
        "type": "Role",
        "userName": "PowerUserRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS RDS Instance Backup

#
Severity
medium
Resource types
AWS.RDS.Instance
Tags
AWS, Availability
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This Policy ensures that RDS Instances have Backups enabled. Backups are an important aspect of disaster recovery that can protect sensitive data from destruction.

Detection logic

def policy(event):
    return event.get("BackupRetentionPeriod") != 0

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_backup.py
PolicyID: "AWS.RDS.InstanceBackup"
DisplayName: "AWS RDS Instance Backup"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Availability
Severity: Medium
Description: >
  This Policy ensures that RDS Instances have Backups enabled. Backups are an important aspect
  of disaster recovery that can protect sensitive data from destruction.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-backups-enabled
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html

Stages and Predicates

Flags AWS.RDS.Instance resources when the condition below holds.

Condition

  • BackupRetentionPeriod is 0

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
BackupRetentionPeriodne0excludes:BackupRetentionPeriod field:"BackupRetentionPeriod" value:"0"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-backups-enabled

AWS RDS Instance Encryption

#
Severity
high
Compliance
PCI 3.4
Resource types
AWS.RDS.Instance
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that RDS instances have encryption enabled.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):
    return resource["KmsKeyId"] is not None

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_encryption.py
PolicyID: "AWS.RDS.Instance.Encryption"
DisplayName: "AWS RDS Instance Encryption"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 3.4
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  This policy validates that RDS instances have encryption enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-storage-encrypted
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html

Stages and Predicates

Flags AWS.RDS.Instance resources when the condition below holds.

Condition

  • KmsKeyId is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
KmsKeyIdis_not_null(no value, null check)excludes:KmsKeyId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
KmsKeyIdis_null
  • (no value, null check)
field:"KmsKeyId" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-storage-encrypted

AWS RDS Instance Has Acceptable Backup Retention Period

#
Severity
low
Compliance
PCI 3.1
Resource types
AWS.RDS.Instance
Tags
AWS, Database, PCI
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that RDS instances are configured with a backup retention period that is acceptable to company policy. This ensures for both compliance and security reasons that records are kept for a minimum period of time, and for compliance and performance reasons that records are not kept indefinitely.

Detection logic

MAX_RETENTION_DAYS = 180
MIN_RETENTION_DAYS = 7


def policy(resource):

    return MIN_RETENTION_DAYS <= resource["BackupRetentionPeriod"] <= MAX_RETENTION_DAYS

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_backup_retention_acceptable.py
PolicyID: "AWS.RDS.InstanceBackupRetentionAcceptable"
DisplayName: "AWS RDS Instance Has Acceptable Backup Retention Period"
Enabled: false
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Database
  - PCI
Reports:
  PCI:
    - 3.1
Severity: Low
Description: >
  This policy validates that RDS instances are configured with a backup retention period that is acceptable to company policy. This ensures for both compliance and security reasons that records are kept for a minimum period of time, and for compliance and performance reasons that records are not kept indefinitely.
Runbook: >
  Adjust the backup retention period to a timeframe within company policy.
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html

Stages and Predicates

Flags AWS.RDS.Instance resources when any of the conditions below holds.

Condition

  • any of:
    • BackupRetentionPeriod is less than 7
    • BackupRetentionPeriod is greater than 180

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Response runbook

Adjust the backup retention period to a timeframe within company policy.

AWS RDS Instance High Availability

#
Severity
low
Resource types
AWS.RDS.Instance
Tags
AWS, Availability
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This Policy ensures that RDS Instances have are running in High Availability mode to provide redundancy in the event of an operational failure. For Aurora, storage is replicated across all the Availability Zones and doesn't require this setting.

Detection logic

def policy(resource):
    if resource["MultiAZ"] is False and resource["StorageType"] == "aurora":
        return True

    # Explicit check for True to avoid returning NoneType
    return resource["MultiAZ"] is True

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_high_availability.py
PolicyID: "AWS.RDS.InstanceHighAvailability"
DisplayName: "AWS RDS Instance High Availability"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Availability
Severity: Low
Description: >
  This Policy ensures that RDS Instances have are running in High Availability mode to provide
  redundancy in the event of an operational failure. For Aurora, storage is replicated across all the Availability Zones and doesn't require this setting.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-high-availability-configured
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html

Stages and Predicates

Flags AWS.RDS.Instance resources when all of the conditions below hold.

Condition

  • any of:
    • MultiAZ is not false
    • StorageType is not aurora
  • MultiAZ is not true

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MultiAZne
  • false transforms: boolean
  • true transforms: boolean
field:"MultiAZ" kind:ne
StorageTypene
  • aurora
field:"StorageType" kind:ne value:"aurora"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-has-high-availability-configured

AWS RDS Instance Minor Version Upgrades

#
Severity
low
Compliance
PCI 6.2
Resource types
AWS.RDS.Instance
Tags
AWS, Panther, PCI
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

If you want Amazon RDS to upgrade the DB engine version of a database automatically, you can enable auto minor version upgrades for the database.

Detection logic

def policy(resource):
    return resource["AutoMinorVersionUpgrade"]

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_auto_minor_version_upgrade_enabled.py
PolicyID: "AWS.RDS.Instance.AutoMinorVersionUpgradeEnabled"
DisplayName: "AWS RDS Instance Minor Version Upgrades"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Panther
  - PCI
Reports:
  PCI:
    - 6.2
Severity: Low
Description: >
  If you want Amazon RDS to upgrade the DB engine version of a database automatically,
  you can enable auto minor version upgrades for the database.
Runbook: |
  For major version upgrades, you must manually modify the DB engine version through the
  AWS Management Console, AWS CLI, or RDS API. For minor version upgrades,
  you can manually modify the engine version, or you can choose to enable auto minor version upgrades.
Reference: https://amzn.to/2L8POnD

Stages and Predicates

Flags AWS.RDS.Instance resources when the condition below holds.

Condition

  • AutoMinorVersionUpgrade is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AutoMinorVersionUpgradeis_not_null(no value, null check)excludes:AutoMinorVersionUpgrade

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AutoMinorVersionUpgradeis_null
  • (no value, null check)
field:"AutoMinorVersionUpgrade" kind:is_null

Response runbook

For major version upgrades, you must manually modify the DB engine version through the

AWS Management Console, AWS CLI, or RDS API. For minor version upgrades,

you can manually modify the engine version, or you can choose to enable auto minor version upgrades.

AWS RDS Instance Modified to be Publicly Accessible

#
Severity
critical
Log types
AWS.CloudTrail
Tags
AWS, Persistence, Defense Evasion, Initial Access, Account Manipulation, Disable or Modify Cloud Firewall, External Remote Services, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when an RDS instance or cluster is modified to become publicly accessible. This exposes the database to the internet and is used by attackers for persistence or data exfiltration. This detects the modification event in real-time, unlike static policy checks.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rds_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    modify_events = ["ModifyDBInstance", "ModifyDBCluster"]
    if event.get("eventName") not in modify_events:
        return False
    if event.deep_get("errorCode") is not None:
        return False
    publicly_accessible = event.deep_get("requestParameters", "publiclyAccessible", default=None)
    if publicly_accessible is True:
        return True
    return False


def title(event):
    event_name = event.get("eventName", "Unknown")
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="<UNKNOWN>"
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = "Instance" if event_name == "ModifyDBInstance" else "Cluster"
    return f"RDS {resource_type} Made Public: [{db_identifier}] by [{user}]"


def alert_context(event):
    context = aws_rds_context(event)
    context["publicly_accessible"] = event.deep_get(
        "requestParameters", "publiclyAccessible", default="N/A"
    )
    context["apply_immediately"] = event.deep_get(
        "requestParameters", "applyImmediately", default="N/A"
    )
    vpc_security_groups = event.deep_get("requestParameters", "vPCSecurityGroupIds", default=None)
    if vpc_security_groups:
        context["vpc_security_groups_modified"] = vpc_security_groups
    subnet_group = event.deep_get("requestParameters", "dBSubnetGroupName")
    if subnet_group:
        context["subnet_group_modified"] = subnet_group
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_instance_made_public.py
RuleID: "AWS.RDS.InstanceMadePublic"
DisplayName: "AWS RDS Instance Modified to be Publicly Accessible"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Persistence
  - Defense Evasion
  - Initial Access
  - Account Manipulation
  - Disable or Modify Cloud Firewall
  - External Remote Services
  - RDS
Severity: Critical
Description: >
  Detects when an RDS instance or cluster is modified to become publicly accessible. This exposes
  the database to the internet and is used by attackers for persistence or data exfiltration.
  This detects the modification event in real-time, unlike static policy checks.
Runbook: |
  1. Find all RDS modification events by the user ARN in the 24 hours before the alert
  2. Check if this database has been made public before by searching for ModifyDBInstance events with publiclyAccessible in the past 90 days
  3. Look for database connection attempts from external IPs in the 6 hours after this modification
  3. Find all API calls from the sourceIPAddress in the 6 hours before and after the alert to identify other suspicious modifications
  4. Immediately revert publiclyAccessible to false if unauthorized, then review VPC security groups for overly permissive rules (0.0.0.0/0)
  5. Check RDS database logs for any access attempts from external IPs in the time window after this modification
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html
Reports:
  MITRE ATT&CK:
    - TA0003:T1098  # Account Manipulation
    - TA0005:T1562.007  # Disable or Modify Cloud Firewall
    - TA0001:T1133  # External Remote Services
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:dBClusterIdentifier
  - requestParameters:publiclyAccessible
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of ModifyDBInstance, ModifyDBCluster
  • errorCode is empty
  • requestParameters.publiclyAccessible is true

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all RDS modification events by the user ARN in the 24 hours before the alert

2. Check if this database has been made public before by searching for ModifyDBInstance events with publiclyAccessible in the past 90 days

3. Look for database connection attempts from external IPs in the 6 hours after this modification

3. Find all API calls from the sourceIPAddress in the 6 hours before and after the alert to identify other suspicious modifications

4. Immediately revert publiclyAccessible to false if unauthorized, then review VPC security groups for overly permissive rules (0.0.0.0/0)

5. Check RDS database logs for any access attempts from external IPs in the time window after this modification

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "ModifyDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-15T16:45:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "applyImmediately": true,
    "dBInstanceIdentifier": "production-db",
    "publiclyAccessible": true
  },
  "responseElements": {
    "dBInstanceIdentifier": "production-db",
    "dBInstanceStatus": "modifying",
    "pendingModifiedValues": {
      "publiclyAccessible": true
    },
    "publiclyAccessible": true
  },
  "sourceIPAddress": "203.0.113.45",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DeveloperRole/developer",
    "principalId": "AIDAI23HXS3EXAMPLE:developer",
    "sessionContext": {
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/DeveloperRole",
        "principalId": "AIDAI23HXS3EXAMPLE",
        "type": "Role",
        "userName": "DeveloperRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS RDS Instance or Cluster Deleted

#
Status
Experimental
Severity
high
Group by
awsRegion, recipientAccountId, requestParameters.dBInstanceIdentifier
Log types
AWS.CloudTrail
Tags
AWS, Impact, Data Destruction, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects RDS database instance or cluster deletion. Deletions that skip final snapshots result in permanent data loss and may indicate ransomware, insider threats, or compromised credentials being used to destroy data.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rds_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    deletion_events = ["DeleteDBInstance", "DeleteDBCluster"]
    if event.get("eventName") not in deletion_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="<UNKNOWN>"
    )
    skip_snapshot = event.deep_get("requestParameters", "skipFinalSnapshot", default=False)
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    snapshot_warning = " [NO FINAL SNAPSHOT]" if skip_snapshot else ""
    if event_name == "DeleteDBInstance":
        return f"RDS Instance Deleted: [{db_identifier}] by [{user}]{snapshot_warning}"
    return f"RDS Cluster Deleted: [{db_identifier}] by [{user}]{snapshot_warning}"


def dedup(event):
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{db_identifier}"


def alert_context(event):
    context = aws_rds_context(event)
    context["skip_final_snapshot"] = event.deep_get(
        "requestParameters", "skipFinalSnapshot", default=False
    )
    context["final_snapshot_identifier"] = event.deep_get(
        "requestParameters", "finalDBSnapshotIdentifier"
    ) or event.deep_get("requestParameters", "finalDBClusterSnapshotIdentifier", default="N/A")
    context["delete_automated_backups"] = event.deep_get(
        "requestParameters", "deleteAutomatedBackups", default="N/A"
    )
    return context


def severity(event):
    skip_snapshot = event.deep_get("requestParameters", "skipFinalSnapshot", default=False)
    delete_backups = event.deep_get("requestParameters", "deleteAutomatedBackups", default=False)
    if skip_snapshot or delete_backups:
        return "CRITICAL"
    return "HIGH"

Rule specification

AnalysisType: rule
Filename: aws_rds_instance_deletion.py
RuleID: "AWS.RDS.InstanceDeletion"
DisplayName: "AWS RDS Instance or Cluster Deleted"
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Impact
  - Data Destruction
  - RDS
Severity: High
Description: >
  Detects RDS database instance or cluster deletion. Deletions that skip final snapshots
  result in permanent data loss and may indicate ransomware, insider threats, or
  compromised credentials being used to destroy data.
Runbook: |
  1. Find all RDS API calls by the user ARN in the 48 hours before the alert to identify precursor activities
  2. Check if this user has deleted databases in the past 90 days to determine if this is unusual behavior
  3. Look for deletion protection changes from this user in the 24 hours before the deletion
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html
Reports:
  MITRE ATT&CK:
    - TA0040:T1485  # Data Destruction
    - TA0040:T1531  # Account Access Removal
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:dBClusterIdentifier
  - requestParameters:skipFinalSnapshot
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of DeleteDBInstance, DeleteDBCluster
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • DeleteDBCluster
  • DeleteDBInstance
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all RDS API calls by the user ARN in the 48 hours before the alert to identify precursor activities

2. Check if this user has deleted databases in the past 90 days to determine if this is unusual behavior

3. Look for deletion protection changes from this user in the 24 hours before the deletion

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "DeleteDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-15T14:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBInstanceIdentifier": "production-mysql-db",
    "deleteAutomatedBackups": false,
    "finalDBSnapshotIdentifier": "production-mysql-db-final-snapshot-2024-01-15",
    "skipFinalSnapshot": false
  },
  "responseElements": {
    "dBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:production-mysql-db",
    "dBInstanceIdentifier": "production-mysql-db",
    "dBInstanceStatus": "deleting"
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DBARole/admin",
    "principalId": "AIDAI23HXS3EXAMPLE:admin",
    "sessionContext": {
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/DBARole",
        "principalId": "AIDAI23HXS3EXAMPLE",
        "type": "Role",
        "userName": "DBARole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS RDS Instance or Cluster Rebooted

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Impact, Availability, Endpoint Denial of Service, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS instances, clusters, or shard groups are rebooted. Unexpected reboots cause service disruption and may indicate DoS attempts, unauthorized testing, or operational issues requiring investigation.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    reboot_events = ["RebootDBInstance", "RebootDBCluster", "RebootDBShardGroup"]
    if event.get("eventName") not in reboot_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    db_identifier = (
        event.deep_get("requestParameters", "dBInstanceIdentifier")
        or event.deep_get("requestParameters", "dBClusterIdentifier")
        or event.deep_get("requestParameters", "dBShardGroupIdentifier", default="<UNKNOWN>")
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = (
        "Instance"
        if event_name == "RebootDBInstance"
        else "Cluster" if event_name == "RebootDBCluster" else "Shard Group"
    )
    return f"RDS {resource_type} Rebooted: [{db_identifier}] by [{user}]"


def alert_context(event):
    context = aws_rule_context(event)
    context["resource_identifier"] = (
        event.deep_get("requestParameters", "dBInstanceIdentifier")
        or event.deep_get("requestParameters", "dBClusterIdentifier")
        or event.deep_get("requestParameters", "dBShardGroupIdentifier", default="N/A")
    )
    context["force_failover"] = event.deep_get("requestParameters", "forceFailover", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_instance_rebooted.py
RuleID: "AWS.RDS.InstanceRebooted"
DisplayName: "AWS RDS Instance or Cluster Rebooted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Impact
  - Availability
  - Endpoint Denial of Service
  - RDS
Severity: Medium
Description: >
  Detects when RDS instances, clusters, or shard groups are rebooted. Unexpected reboots
  cause service disruption and may indicate DoS attempts, unauthorized testing, or
  operational issues requiring investigation.
Runbook: |
  1. Find all database reboot events by the user ARN in the past 24 hours to identify bulk reboot patterns
  2. Check if this user has rebooted databases in the past 90 days to determine if this is normal behavior
  3. Look for database modification events from this user in the 30 minutes before the reboot
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_RebootInstance.html
Reports:
  MITRE ATT&CK:
    - TA0040:T1499  # Endpoint Denial of Service
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:dBClusterIdentifier
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of RebootDBInstance, RebootDBCluster, RebootDBShardGroup
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • RebootDBCluster
  • RebootDBInstance
  • RebootDBShardGroup
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all database reboot events by the user ARN in the past 24 hours to identify bulk reboot patterns

2. Check if this user has rebooted databases in the past 90 days to determine if this is normal behavior

3. Look for database modification events from this user in the 30 minutes before the reboot

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "RebootDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-18T02:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBInstanceIdentifier": "production-mysql",
    "forceFailover": false
  },
  "responseElements": {
    "dBInstanceIdentifier": "production-mysql",
    "dBInstanceStatus": "rebooting"
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DBARole/admin",
    "principalId": "AIDAI23HXS3EXAMPLE:admin",
    "type": "AssumedRole"
  }
}

AWS RDS Instance Public Access

#
Severity
high
Resource types
AWS.RDS.Instance
Tags
AWS, Data Protection, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This Policy checks that an RDS Instance is not accessible from the public internet.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

def policy(resource):
    return not resource["PubliclyAccessible"]

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_public_access.py
PolicyID: "AWS.RDS.Instance.PublicAccess"
DisplayName: "AWS RDS Instance Public Access"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Data Protection
  - Exfiltration:Exfiltration Over Web Service
Reports:
  MITRE ATT&CK:
    - TA0010:T1567
Severity: High
Description: >
  This Policy checks that an RDS Instance is not accessible from the public internet.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-is-not-publicly-accessible
Reference: https://docs.aws.amazon.com/en_pv/AmazonRDS/latest/UserGuide/USER_VPC.Scenarios.html

Stages and Predicates

Flags AWS.RDS.Instance resources when the condition below holds.

Condition

  • PubliclyAccessible is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PubliclyAccessibleis_null(no value, null check)excludes:PubliclyAccessible

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
PubliclyAccessibleis_not_null
  • (no value, null check)
field:"PubliclyAccessible" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-is-not-publicly-accessible

AWS RDS Instance Snapshot Public Access

#
Severity
critical
Resource types
AWS.RDS.Instance
Tags
AWS, Data Protection, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that RDS Instance snapshots are not publicly restorable. This would allow anyone to restore an old version of your database and have full access to its contents.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

from panther_base_helpers import listify


def policy(resource):
    # Check if this instance has snapshots
    if resource["SnapshotAttributes"] is None:
        return True

    # Check that no snapshots are able to be restored by all (i.e. are public)
    for snapshot_attrs in resource["SnapshotAttributes"]:
        for snapshot_attr in snapshot_attrs["DBSnapshotAttributes"]:
            if (
                snapshot_attr["AttributeName"] == "restore"
                and snapshot_attr["AttributeValues"] is not None
                and "all" in listify(snapshot_attr["AttributeValues"])
            ):
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_rds_instance_snapshot_public_access.py
PolicyID: "AWS.RDS.Instance.SnapshotPublicAccess"
DisplayName: "AWS RDS Instance Snapshot Public Access"
Enabled: true
ResourceTypes:
  - AWS.RDS.Instance
Tags:
  - AWS
  - Data Protection
  - Exfiltration:Exfiltration Over Web Service
Reports:
  MITRE ATT&CK:
    - TA0010:T1567
Severity: Critical
Description: >
  This policy validates that RDS Instance snapshots are not publicly restorable. This would allow anyone to restore an old version of your database and have full access to its contents.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-snapshots-are-not-publicly-accessible
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html

Stages and Predicates

Flags AWS.RDS.Instance resources when the condition below holds.

Condition

  • SnapshotAttributes is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SnapshotAttributesis_null(no value, null check)excludes:SnapshotAttributes

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SnapshotAttributesis_not_null
  • (no value, null check)
field:"SnapshotAttributes" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-rds-instance-snapshots-are-not-publicly-accessible

AWS RDS Log File Downloaded

#
Status
Experimental
Severity
low
Group by
awsRegion, recipientAccountId, requestParameters.dBInstanceIdentifier, requestParameters.logFileName
Log types
AWS.CloudTrail
Tags
AWS, Credential Access, Discovery, Credentials in Files, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS database log files are downloaded. Log files may contain credentials, sensitive queries, or application secrets. Bulk downloads from unusual locations may indicate credential harvesting or data reconnaissance.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    if event.get("eventName") != "DownloadDBLogFilePortion":
        return False
    return event.deep_get("errorCode") is None


def title(event):
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier", default="<UNKNOWN>")
    log_file = event.deep_get("requestParameters", "logFileName", default="<UNKNOWN_FILE>")
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    return f"RDS Log Downloaded: [{log_file}] from [{db_identifier}] by [{user}]"


def dedup(event):
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier", default="unknown")
    log_file = event.deep_get("requestParameters", "logFileName", default="unknown")
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{db_identifier}:{log_file}"


def alert_context(event):
    context = aws_rule_context(event)
    context["db_instance_identifier"] = event.deep_get(
        "requestParameters", "dBInstanceIdentifier", default="N/A"
    )
    context["log_file_name"] = event.deep_get("requestParameters", "logFileName", default="N/A")
    context["marker"] = event.deep_get("requestParameters", "marker", default="N/A")
    context["number_of_lines"] = event.deep_get("requestParameters", "numberOfLines", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_log_file_downloaded.py
RuleID: "AWS.RDS.LogFileDownloaded"
DisplayName: "AWS RDS Log File Downloaded"
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Credential Access
  - Discovery
  - Credentials in Files
  - RDS
Severity: Low
Description: >
  Detects when RDS database log files are downloaded. Log files may contain credentials,
  sensitive queries, or application secrets. Bulk downloads from unusual locations may
  indicate credential harvesting or data reconnaissance.
Runbook: |
  1. Find all log file download events by the user ARN in the past 6 hours to identify bulk download patterns
  2. Check if the source IP address matches the user's normal access patterns from the past 30 days
  3. Look for database access or modification events from this user in the 48 hours before the log download
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_LogAccess.html
Reports:
  MITRE ATT&CK:
    - TA0006:T1552.001  # Credentials in Files
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:logFileName
  - sourceIPAddress
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is DownloadDBLogFilePortion
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
logFileNamerequestParameters.logFileName
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all log file download events by the user ARN in the past 6 hours to identify bulk download patterns

2. Check if the source IP address matches the user's normal access patterns from the past 30 days

3. Look for database access or modification events from this user in the 48 hours before the log download

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "DownloadDBLogFilePortion",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-18T06:15:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBInstanceIdentifier": "production-mysql",
    "logFileName": "error/mysql-error.log",
    "marker": "0",
    "numberOfLines": 1000
  },
  "responseElements": {
    "additionalDataPending": true,
    "logFileData": "[REDACTED]",
    "marker": "1000"
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DBARole/dba",
    "principalId": "AIDAI23HXS3EXAMPLE:dba",
    "type": "AssumedRole"
  }
}

AWS RDS Manual/Public Snapshot Created

#
Severity
low
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration, Transfer Data to Cloud Account
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A manual snapshot of an RDS database was created. An attacker may use this to exfiltrate the DB contents to another account; use this as a correlation rule.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return all(
        [
            event.get("eventSource", "") == "rds.amazonaws.com",
            event.get("eventName", "") == "CreateDBSnapshot",
            event.deep_get("responseElements", "snapshotType") in {"manual", "public"},
        ]
    )


def title(event):
    account_id = event.get("recipientAccountId", "")
    rds_instance_id = event.deep_get("responseElements", "dBInstanceIdentifier")
    return f"Manual RDS Snapshot Created in [{account_id}] for RDS instance [{rds_instance_id}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_rds_manual_snapshot_created.py
RuleID: "AWS.RDS.ManualSnapshotCreated"
DisplayName: "AWS RDS Manual/Public Snapshot Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration
  - Transfer Data to Cloud Account
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Severity: Low
Description: >
  A manual snapshot of an RDS database was created.
  An attacker may use this to exfiltrate the DB contents to another account; use this as a correlation rule.
Runbook: >
  Ensure the snapshot was shared with an allowed AWS account. If not, delete the snapshot and quarantine the compromised IAM user.
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CreateSnapshot.html
SummaryAttributes:
  - eventSource
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is CreateDBSnapshot
  • responseElements.snapshotType is one of manual, public

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierresponseElements.dBInstanceIdentifier

Response runbook

Ensure the snapshot was shared with an allowed AWS account. If not, delete the snapshot and quarantine the compromised IAM user.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "c665b42c-89b4-4072-ad71-0f9c8d50f649",
  "eventName": "CreateDBSnapshot",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2023-12-08T14:55:19Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "e5fd8d41-db7c-45df-a21a-f9cff8c19755",
  "requestParameters": {
    "dBInstanceIdentifier": "terraform-20231208145149286600000001",
    "dBSnapshotIdentifier": "exfiltration"
  },
  "responseElements": {
    "allocatedStorage": 10,
    "availabilityZone": "us-west-2b",
    "dBInstanceIdentifier": "terraform-20231208145149286600000001",
    "dBSnapshotArn": "arn:aws:rds:us-west-2:123456789012:snapshot:exfiltration",
    "dBSnapshotIdentifier": "exfiltration",
    "dbiResourceId": "db-TYZSSMTWIABIR6QKKFGI55XKJQ",
    "dedicatedLogVolume": false,
    "encrypted": false,
    "engine": "mysql",
    "engineVersion": "8.0.33",
    "iAMDatabaseAuthenticationEnabled": false,
    "instanceCreateTime": "Dec 8, 2023 2:55:17 PM",
    "licenseModel": "general-public-license",
    "masterUsername": "admin",
    "optionGroupName": "default:mysql-8-0",
    "percentProgress": 0,
    "port": 3306,
    "processorFeatures": [],
    "snapshotTarget": "region",
    "snapshotType": "manual",
    "status": "creating",
    "storageThroughput": 0,
    "storageType": "gp2",
    "vpcId": "vpc-0c9c141888d129377"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "rds.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "APN/1.0 HashiCorp/1.0 Terraform/1.1.2 (+https://www.terraform.io) terraform-provider-aws/3.76.1 (+https://registry.terraform.io/providers/hashicorp/aws) aws-sdk-go/1.44.157 (go1.19.3; darwin; arm64) 68319f60-9dec-43b2-9702-de3a08c9d8a3 HashiCorp-terraform-exec/0.17.3",
  "userIdentity": {
    "accessKeyId": "ASIAFFA5AFEC02FFCD8ED",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/ARole/fake.user",
    "principalId": "AROA2DFDF0C1FDFCAD2B2:fake.user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2023-12-08T13:53:48Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/us-west-2/ARole",
        "principalId": "AROA2DFDF0C1FDFCAD2B2",
        "type": "Role",
        "userName": "ARole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS RDS Master Password Updated

#
Severity
low
Entities
aws_account_ids, aws_arns, domain_names, trace_ids
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A sensitive database operation that should be performed carefully or rarely

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    return (
        event.get("eventName") == "ModifyDBInstance"
        and event.get("eventSource") == "rds.amazonaws.com"
        and bool(event.deep_get("responseElements", "pendingModifiedValues", "masterUserPassword"))
    )


def title(event):
    return f"RDS Master Password Updated on [{event.deep_get('responseElements', 'dBInstanceArn')}]"

Rule specification

AnalysisType: rule
Description: A sensitive database operation that should be performed carefully or rarely
DisplayName: "AWS RDS Master Password Updated"
Enabled: true
Filename: aws_rds_master_pass_updated.py
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html
Severity: Low
DedupPeriodMinutes: 60
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
SummaryAttributes:
  - awsRegion
  - userIdentity:arn
  - responseElements:dBInstanceIdentifier
  - p_any_aws_arns
  - p_any_aws_account_ids
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.RDS.MasterPasswordUpdated"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ModifyDBInstance
  • eventSource is rds.amazonaws.com
  • responseElements.pendingModifiedValues.masterUserPassword is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
dBInstanceArnresponseElements.dBInstanceArn

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-1",
  "eventCategory": "Management",
  "eventID": "09191e37-4632-4722-82bf-50288436cf47",
  "eventName": "ModifyDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2022-09-24 00:28:15",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123456789012"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123456789012:role/Admin",
    "arn:aws:kms:us-west-1:123456789012:key/e2c16323-1c31-45fb-adda-07e5c9f78997",
    "arn:aws:rds:us-west-1:123456789012:db:my-database",
    "arn:aws:sts::123456789012:assumed-role/Admin/Jack"
  ],
  "p_any_domain_names": [
    "AWS Internal"
  ],
  "p_any_trace_ids": [
    "ASIASWJRT64ZWCAMGCWI"
  ],
  "p_event_time": "2022-09-24 00:28:15",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-09-24 00:32:43.679",
  "p_row_id": "eea2890fafffb7b88fae80cb138a08",
  "p_source_id": "b00eb354-da7a-49dd-9cc6-32535e32096a",
  "p_source_label": "CloudTrail",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "93fba047-0282-4c53-b3a6-1c3bb684f563",
  "requestParameters": {
    "allowMajorVersionUpgrade": false,
    "applyImmediately": true,
    "dBInstanceIdentifier": "my-database",
    "masterUserPassword": "****",
    "maxAllocatedStorage": 1000
  },
  "responseElements": {
    "allocatedStorage": 20,
    "associatedRoles": [],
    "autoMinorVersionUpgrade": true,
    "availabilityZone": "us-west-1b",
    "backupRetentionPeriod": 0,
    "backupTarget": "region",
    "cACertificateIdentifier": "rds-ca-2019",
    "copyTagsToSnapshot": true,
    "customerOwnedIpEnabled": false,
    "dBInstanceArn": "arn:aws:rds:us-west-1:123456789012:db:my-database",
    "dBInstanceClass": "db.t3.micro",
    "dBInstanceIdentifier": "my-database",
    "dBInstanceStatus": "available",
    "dBName": "test",
    "dBParameterGroups": [
      {
        "dBParameterGroupName": "default.mysql8.0",
        "parameterApplyStatus": "in-sync"
      }
    ],
    "dBSecurityGroups": [],
    "dBSubnetGroup": {
      "dBSubnetGroupDescription": "Created from the RDS Management Console",
      "dBSubnetGroupName": "default-vpc-f9999999",
      "subnetGroupStatus": "Complete",
      "subnets": [
        {
          "subnetAvailabilityZone": {
            "name": "us-west-1c"
          },
          "subnetIdentifier": "subnet-8cb458ea",
          "subnetOutpost": {},
          "subnetStatus": "Active"
        },
        {
          "subnetAvailabilityZone": {
            "name": "us-west-1b"
          },
          "subnetIdentifier": "subnet-8382bbd8",
          "subnetOutpost": {},
          "subnetStatus": "Active"
        }
      ],
      "vpcId": "vpc-f9999999"
    },
    "dbInstancePort": 0,
    "dbiResourceId": "db-FEVDSUCWJ43PXONVT6ZU2TK4WY",
    "deletionProtection": false,
    "domainMemberships": [],
    "enabledCloudwatchLogsExports": [
      "audit",
      "error",
      "general"
    ],
    "endpoint": {
      "address": "my-database.cbsugyyyyyyy.us-west-1.rds.amazonaws.com",
      "hostedZoneId": "Z10WI91S59XXQN",
      "port": 3306
    },
    "engine": "mysql",
    "engineVersion": "8.0.28",
    "httpEndpointEnabled": false,
    "iAMDatabaseAuthenticationEnabled": false,
    "instanceCreateTime": "Sep 23, 2022 11:25:46 PM",
    "kmsKeyId": "arn:aws:kms:us-west-1:123456789012:key/e2c16323-1c31-45fb-adda-07e5c9f78997",
    "licenseModel": "general-public-license",
    "masterUsername": "admin",
    "maxAllocatedStorage": 1000,
    "monitoringInterval": 0,
    "multiAZ": false,
    "networkType": "IPV4",
    "optionGroupMemberships": [
      {
        "optionGroupName": "default:mysql-8-0",
        "status": "in-sync"
      }
    ],
    "pendingModifiedValues": {
      "masterUserPassword": "****"
    },
    "performanceInsightsEnabled": false,
    "preferredBackupWindow": "11:52-12:22",
    "preferredMaintenanceWindow": "tue:13:03-tue:13:33",
    "publiclyAccessible": true,
    "readReplicaDBInstanceIdentifiers": [],
    "storageEncrypted": true,
    "storageThroughput": 0,
    "storageType": "gp2",
    "tagList": [],
    "vpcSecurityGroups": [
      {
        "status": "active",
        "vpcSecurityGroupId": "sg-d963a5a4"
      }
    ]
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ASIASWJRT64ZWCAMGCWI",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/Admin/Jack",
    "principalId": "AROAJ4ULUNLE6DYF4PCOK:jack",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-23T23:17:13Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/Admin",
        "principalId": "AROAJ4ULUNLE6DYF4PCOK",
        "type": "Role",
        "userName": "Admin"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS RDS Security Group Ingress Authorized

#
Severity
medium
Group by
awsRegion, recipientAccountId, requestParameters.dBSecurityGroupName
Log types
AWS.CloudTrail
Tags
AWS, Persistence, Defense Evasion, Account Manipulation, Disable or Modify Cloud Firewall, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when ingress rules are added to RDS security groups. Overly permissive rules, especially 0.0.0.0/0, expose databases to the internet and may indicate attackers opening network access for persistence or data exfiltration.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    if event.get("eventName") != "AuthorizeDBSecurityGroupIngress":
        return False
    return event.deep_get("errorCode") is None


def title(event):
    security_group = event.deep_get("requestParameters", "dBSecurityGroupName", default="<UNKNOWN>")
    cidr_ip = event.deep_get("requestParameters", "cIDRIP", default="")
    ec2_group = event.deep_get("requestParameters", "eC2SecurityGroupName", default="")
    source = cidr_ip if cidr_ip else ec2_group if ec2_group else "<UNKNOWN_SOURCE>"
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    return f"RDS Security Group Ingress Authorized: [{security_group}] from [{source}] by [{user}]"


def dedup(event):
    security_group = event.deep_get("requestParameters", "dBSecurityGroupName", default="unknown")
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{security_group}"


def alert_context(event):
    context = aws_rule_context(event)
    context["security_group_name"] = event.deep_get(
        "requestParameters", "dBSecurityGroupName", default="N/A"
    )
    context["cidr_ip"] = event.deep_get("requestParameters", "cIDRIP", default="N/A")
    context["ec2_security_group_name"] = event.deep_get(
        "requestParameters", "eC2SecurityGroupName", default="N/A"
    )
    context["ec2_security_group_id"] = event.deep_get(
        "requestParameters", "eC2SecurityGroupId", default="N/A"
    )
    context["ec2_security_group_owner_id"] = event.deep_get(
        "requestParameters", "eC2SecurityGroupOwnerId", default="N/A"
    )
    return context


def severity(event):
    cidr_ip = event.deep_get("requestParameters", "cIDRIP", default="")
    if cidr_ip == "0.0.0.0/0":
        return "CRITICAL"
    return "MEDIUM"

Rule specification

AnalysisType: rule
Filename: aws_rds_security_group_ingress_authorized.py
RuleID: "AWS.RDS.SecurityGroupIngressAuthorized"
DisplayName: "AWS RDS Security Group Ingress Authorized"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Persistence
  - Defense Evasion
  - Account Manipulation
  - Disable or Modify Cloud Firewall
  - RDS
Severity: Medium
Description: >
  Detects when ingress rules are added to RDS security groups. Overly permissive rules,
  especially 0.0.0.0/0, expose databases to the internet and may indicate attackers
  opening network access for persistence or data exfiltration.
Runbook: |
  1. Find all security group ingress authorizations by the user ARN in the past 24 hours
  2. Check if the CIDR range in requestParameters:cIDRIP is overly permissive or previously used by this user
  3. Look for database connection attempts from external IPs in the 6 hours after this rule was added
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithSecurityGroups.html
Reports:
  MITRE ATT&CK:
    - TA0003:T1098  # Account Manipulation
    - TA0005:T1562.007  # Disable or Modify Cloud Firewall
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBSecurityGroupName
  - requestParameters:cIDRIP
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is AuthorizeDBSecurityGroupIngress
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBSecurityGroupNamerequestParameters.dBSecurityGroupName
userNameuserIdentity.userName

Response runbook

1. Find all security group ingress authorizations by the user ARN in the past 24 hours

2. Check if the CIDR range in requestParameters:cIDRIP is overly permissive or previously used by this user

3. Look for database connection attempts from external IPs in the 6 hours after this rule was added

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "AuthorizeDBSecurityGroupIngress",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-17T09:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "cIDRIP": "203.0.113.0/24",
    "dBSecurityGroupName": "production-db-sg"
  },
  "responseElements": {
    "dBSecurityGroupName": "production-db-sg",
    "iPRanges": [
      {
        "cIDRIP": "203.0.113.0/24",
        "status": "authorizing"
      }
    ]
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/NetworkAdmin/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "type": "AssumedRole"
  }
}

AWS RDS Snapshot Copied Cross-Region

#
Severity
medium
Group by
awsRegion, recipientAccountId, requestParameters.targetDBSnapshotIdentifier
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration, Transfer Data to Cloud Account, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS snapshots are copied to different AWS regions. While legitimate for disaster recovery, cross-region snapshot copies can be used for data exfiltration or to prepare snapshots for sharing with external accounts.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    copy_events = ["CopyDBSnapshot", "CopyDBClusterSnapshot"]
    if event.get("eventName") not in copy_events:
        return False
    if event.deep_get("errorCode") is not None:
        return False
    source_region = event.deep_get("requestParameters", "sourceRegion", default="")
    target_region = event.get("awsRegion", "")
    if source_region and target_region and source_region != target_region:
        return True
    return False


def title(event):
    event_name = event.get("eventName", "Unknown")
    source_id = event.deep_get("requestParameters", "sourceDBSnapshotIdentifier") or event.deep_get(
        "requestParameters", "sourceDBClusterSnapshotIdentifier", default="<UNKNOWN>"
    )
    source_region = event.deep_get("requestParameters", "sourceRegion", default="<UNKNOWN>")
    target_region = event.get("awsRegion", "<UNKNOWN>")
    resource_type = "Snapshot" if event_name == "CopyDBSnapshot" else "Cluster Snapshot"
    return (
        f"RDS {resource_type} Copied Cross-Region: [{source_id}] "
        f"from [{source_region}] to [{target_region}]"
    )


def dedup(event):
    target_id = event.deep_get("requestParameters", "targetDBSnapshotIdentifier") or event.deep_get(
        "requestParameters", "targetDBClusterSnapshotIdentifier", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{target_id}"


def alert_context(event):
    context = aws_rule_context(event)
    context["source_snapshot_identifier"] = event.deep_get(
        "requestParameters", "sourceDBSnapshotIdentifier"
    ) or event.deep_get("requestParameters", "sourceDBClusterSnapshotIdentifier", default="N/A")
    context["target_snapshot_identifier"] = event.deep_get(
        "requestParameters", "targetDBSnapshotIdentifier"
    ) or event.deep_get("requestParameters", "targetDBClusterSnapshotIdentifier", default="N/A")
    context["source_region"] = event.deep_get("requestParameters", "sourceRegion", default="N/A")
    context["kms_key_id"] = event.deep_get("requestParameters", "kmsKeyId", default="N/A")
    context["copy_tags"] = event.deep_get("requestParameters", "copyTags", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_snapshot_copied_cross_region.py
RuleID: "AWS.RDS.SnapshotCopiedCrossRegion"
DisplayName: "AWS RDS Snapshot Copied Cross-Region"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration
  - Transfer Data to Cloud Account
  - RDS
Severity: Medium
Description: >
  Detects when RDS snapshots are copied to different AWS regions. While legitimate for
  disaster recovery, cross-region snapshot copies can be used for data exfiltration or
  to prepare snapshots for sharing with external accounts.
Runbook: |
  1. Find all snapshot copy operations by the user ARN in the past 24 hours to identify bulk copying patterns
  2. Check if cross-region snapshot copies are normal for this user by searching the past 90 days
  3. Look for snapshot sharing events in the target region in the 48 hours after this copy
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CopySnapshot.html
Reports:
  MITRE ATT&CK:
    - TA0010:T1537  # Transfer Data to Cloud Account
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:sourceRegion
  - awsRegion
  - requestParameters:sourceDBSnapshotIdentifier
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of CopyDBSnapshot, CopyDBClusterSnapshot
  • errorCode is empty
  • requestParameters.sourceRegion is present
  • awsRegion is present
  • requestParameters.sourceRegion differs from field awsRegion

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
sourceDBSnapshotIdentifierrequestParameters.sourceDBSnapshotIdentifier
sourceRegionrequestParameters.sourceRegion

Response runbook

1. Find all snapshot copy operations by the user ARN in the past 24 hours to identify bulk copying patterns

2. Check if cross-region snapshot copies are normal for this user by searching the past 90 days

3. Look for snapshot sharing events in the target region in the 48 hours after this copy

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "CopyDBSnapshot",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-17T11:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "copyTags": true,
    "kmsKeyId": "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
    "sourceDBSnapshotIdentifier": "arn:aws:rds:us-east-1:123456789012:snapshot:prod-backup-2024",
    "sourceRegion": "us-east-1",
    "targetDBSnapshotIdentifier": "prod-backup-2024-copy"
  },
  "responseElements": {
    "dBSnapshotArn": "arn:aws:rds:us-west-2:123456789012:snapshot:prod-backup-2024-copy",
    "dBSnapshotIdentifier": "prod-backup-2024-copy",
    "sourceDBSnapshotIdentifier": "arn:aws:rds:us-east-1:123456789012:snapshot:prod-backup-2024",
    "sourceRegion": "us-east-1",
    "status": "copying"
  },
  "sourceIPAddress": "203.0.113.45",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DeveloperRole/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "type": "AssumedRole"
  }
}

AWS RDS Snapshot Deleted

#
Status
Experimental
Severity
high
Group by
awsRegion, recipientAccountId, requestParameters.dBSnapshotIdentifier
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact, Data Destruction, Indicator Removal, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects deletion of RDS snapshots. Attackers delete backups to prevent recovery or hide evidence of data exfiltration. Multiple snapshot deletions may indicate ransomware preparing to encrypt databases without recovery options.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    deletion_events = ["DeleteDBSnapshot", "DeleteDBClusterSnapshot"]
    if event.get("eventName") not in deletion_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    snapshot_id = event.deep_get("requestParameters", "dBSnapshotIdentifier") or event.deep_get(
        "requestParameters", "dBClusterSnapshotIdentifier", default="<UNKNOWN>"
    )
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = "Snapshot" if event_name == "DeleteDBSnapshot" else "Cluster Snapshot"
    return f"RDS {resource_type} Deleted: [{snapshot_id}] by [{user}]"


def dedup(event):
    snapshot_id = event.deep_get("requestParameters", "dBSnapshotIdentifier") or event.deep_get(
        "requestParameters", "dBClusterSnapshotIdentifier", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{snapshot_id}"


def alert_context(event):
    context = aws_rule_context(event)
    context["snapshot_identifier"] = event.deep_get(
        "requestParameters", "dBSnapshotIdentifier"
    ) or event.deep_get("requestParameters", "dBClusterSnapshotIdentifier", default="N/A")
    context["snapshot_arn"] = event.deep_get("responseElements", "dBSnapshotArn") or event.deep_get(
        "responseElements", "dBClusterSnapshotArn", default="N/A"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_snapshot_deleted.py
RuleID: "AWS.RDS.SnapshotDeleted"
DisplayName: "AWS RDS Snapshot Deleted"
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact
  - Data Destruction
  - Indicator Removal
  - RDS
Severity: High
Description: >
  Detects deletion of RDS snapshots. Attackers delete backups to prevent recovery or hide
  evidence of data exfiltration. Multiple snapshot deletions may indicate ransomware preparing
  to encrypt databases without recovery options.
Runbook: |
  1. Find all snapshot deletion events by the user ARN in the past 24 hours to identify bulk deletion patterns
  2. Check if the deleted snapshot was shared with external accounts in the 7 days before deletion
  3. Look for database deletion or modification events from this user in the 2 hours after this snapshot deletion
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html
Reports:
  MITRE ATT&CK:
    - TA0040:T1485  # Data Destruction
    - TA0005:T1070  # Indicator Removal
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBSnapshotIdentifier
  - requestParameters:dBClusterSnapshotIdentifier
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of DeleteDBSnapshot, DeleteDBClusterSnapshot
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • DeleteDBClusterSnapshot
  • DeleteDBSnapshot
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBSnapshotIdentifierrequestParameters.dBSnapshotIdentifier
userNameuserIdentity.userName

Response runbook

1. Find all snapshot deletion events by the user ARN in the past 24 hours to identify bulk deletion patterns

2. Check if the deleted snapshot was shared with external accounts in the 7 days before deletion

3. Look for database deletion or modification events from this user in the 2 hours after this snapshot deletion

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "DeleteDBSnapshot",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-16T08:15:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBSnapshotIdentifier": "backup-2024-01-15"
  },
  "responseElements": {
    "dBSnapshotArn": "arn:aws:rds:us-west-2:123456789012:snapshot:backup-2024-01-15",
    "dBSnapshotIdentifier": "backup-2024-01-15",
    "snapshotType": "manual",
    "status": "deleted"
  },
  "sourceIPAddress": "198.51.100.25",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/PowerUserRole/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "type": "AssumedRole"
  }
}

AWS RDS Snapshot Enumeration with Public or Shared Flag

#
Status
Experimental
Severity
low
Group by
awsRegion, recipientAccountId, userIdentity.arn
Log types
AWS.CloudTrail
Tags
AWS, Discovery, Cloud Infrastructure Discovery, Reconnaissance, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when RDS snapshots are queried with includePublic or includeShared flags. This indicates reconnaissance for publicly accessible or shared snapshots that may contain sensitive data and be exploitable by attackers.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    describe_events = ["DescribeDBSnapshots", "DescribeDBClusterSnapshots"]
    if event.get("eventName") not in describe_events:
        return False
    if event.deep_get("errorCode") is not None:
        return False
    include_public = event.deep_get("requestParameters", "includePublic", default=False)
    include_shared = event.deep_get("requestParameters", "includeShared", default=False)
    if include_public or include_shared:
        return True
    return False


def title(event):
    event_name = event.get("eventName", "Unknown")
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    include_public = event.deep_get("requestParameters", "includePublic", default=False)
    include_shared = event.deep_get("requestParameters", "includeShared", default=False)
    if include_public and include_shared:
        scope = "Public and Shared"
    elif include_public:
        scope = "Public"
    elif include_shared:
        scope = "Shared"
    else:
        scope = "Unknown"
    resource_type = "Snapshots" if event_name == "DescribeDBSnapshots" else "Cluster Snapshots"
    return f"RDS {resource_type} Enumeration: {scope} snapshots queried by [{user}]"


def dedup(event):
    user_arn = event.deep_get("userIdentity", "arn", default="unknown")
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{user_arn}"


def alert_context(event):
    context = aws_rule_context(event)
    context["include_public"] = event.deep_get("requestParameters", "includePublic", default="N/A")
    context["include_shared"] = event.deep_get("requestParameters", "includeShared", default="N/A")
    context["max_records"] = event.deep_get("requestParameters", "maxRecords", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_snapshot_enumeration.py
RuleID: "AWS.RDS.SnapshotEnumeration"
DisplayName: "AWS RDS Snapshot Enumeration with Public or Shared Flag"
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Discovery
  - Cloud Infrastructure Discovery
  - Reconnaissance
  - RDS
Severity: Low
Description: >
  Detects when RDS snapshots are queried with includePublic or includeShared flags.
  This indicates reconnaissance for publicly accessible or shared snapshots that may
  contain sensitive data and be exploitable by attackers.
Runbook: |
  1. Find all snapshot enumeration events by the user ARN in the past 6 hours to identify systematic patterns
  2. Check if the source IP address is associated with known VPN or proxy services
  3. Look for snapshot copy or restore operations from this user in the 24 hours after the enumeration
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ListSnapshots.html
Reports:
  MITRE ATT&CK:
    - TA0007:T1580  # Cloud Infrastructure Discovery
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:includePublic
  - requestParameters:includeShared
  - sourceIPAddress
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of DescribeDBSnapshots, DescribeDBClusterSnapshots
  • errorCode is empty
  • any of:
    • requestParameters.includePublic is present
    • requestParameters.includeShared is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
userNameuserIdentity.userName

Response runbook

1. Find all snapshot enumeration events by the user ARN in the past 6 hours to identify systematic patterns

2. Check if the source IP address is associated with known VPN or proxy services

3. Look for snapshot copy or restore operations from this user in the 24 hours after the enumeration

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "DescribeDBSnapshots",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-18T05:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "includePublic": true,
    "maxRecords": 100
  },
  "responseElements": null,
  "sourceIPAddress": "185.220.101.75",
  "userAgent": "Boto3/1.26.0 Python/3.9.0",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/researcher",
    "principalId": "AIDAI23HXS3EXAMPLE",
    "type": "IAMUser",
    "userName": "researcher"
  }
}

AWS RDS Snapshot Exported to S3

#
Status
Experimental
Severity
high
Group by
awsRegion, recipientAccountId
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration, Transfer Data to Cloud Account, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when an RDS snapshot is exported to S3 using StartExportTask. Attackers use this to exfiltrate database contents by exporting snapshots to buckets they control. While snapshot exports are legitimate for analytics, they provide complete database access.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return all(
        [
            event.get("eventSource") == "rds.amazonaws.com",
            event.get("eventName") == "StartExportTask",
            event.deep_get("errorCode") is None,
        ]
    )


def title(event):
    db_identifier = (
        event.deep_get("requestParameters", "sourceArn", default="<UNKNOWN_SOURCE>")
        .split(":")[-1]
        .split("/")[-1]
    )
    s3_bucket = event.deep_get("requestParameters", "s3BucketName", default="<UNKNOWN_BUCKET>")
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    return f"RDS Snapshot Export: [{db_identifier}] exported to S3 bucket [{s3_bucket}] by [{user}]"


def dedup(event):
    db_identifier = (
        event.deep_get("requestParameters", "sourceArn", default="<UNKNOWN_SOURCE>")
        .split(":")[-1]
        .split("/")[-1]
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{db_identifier}"


def alert_context(event):
    context = aws_rule_context(event)
    context["export_task_id"] = event.deep_get(
        "responseElements", "exportTaskIdentifier", default="N/A"
    )
    context["source_arn"] = event.deep_get("requestParameters", "sourceArn", default="N/A")
    context["s3_bucket"] = event.deep_get("requestParameters", "s3BucketName", default="N/A")
    context["s3_prefix"] = event.deep_get("requestParameters", "s3Prefix", default="N/A")
    context["kms_key_id"] = event.deep_get("requestParameters", "kmsKeyId", default="N/A")
    context["iam_role_arn"] = event.deep_get("requestParameters", "iamRoleArn", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_snapshot_export.py
RuleID: "AWS.RDS.SnapshotExport"
DisplayName: "AWS RDS Snapshot Exported to S3"
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration
  - Transfer Data to Cloud Account
  - RDS
Severity: High
Description: >
  Detects when an RDS snapshot is exported to S3 using StartExportTask. Attackers use this
  to exfiltrate database contents by exporting snapshots to buckets they control. While
  snapshot exports are legitimate for analytics, they provide complete database access.
Runbook: |
  1. Find all API calls by the user ARN in the 24 hours before the alert to establish normal behavior
  2. Check if the S3 bucket in requestParameters:s3BucketName has been accessed by this user in the past 90 days
  3. Look for other StartExportTask events or snapshot operations from this user in the past 7 days
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ExportSnapshot.html
Reports:
  MITRE ATT&CK:
    - TA0010:T1537  # Exfiltration: Transfer Data to Cloud Account
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:s3BucketName
  - requestParameters:sourceArn
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is StartExportTask
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
s3BucketNamerequestParameters.s3BucketName
userNameuserIdentity.userName

Response runbook

1. Find all API calls by the user ARN in the 24 hours before the alert to establish normal behavior

2. Check if the S3 bucket in requestParameters:s3BucketName has been accessed by this user in the past 90 days

3. Look for other StartExportTask events or snapshot operations from this user in the past 7 days

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "StartExportTask",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-15T10:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "exportOnly": [
      "schema1.table1",
      "schema1.table2"
    ],
    "iamRoleArn": "arn:aws:iam::123456789012:role/RDSExportRole",
    "kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
    "s3BucketName": "my-export-bucket",
    "s3Prefix": "exports/2024-01-15/",
    "sourceArn": "arn:aws:rds:us-east-1:123456789012:snapshot:my-db-snapshot"
  },
  "responseElements": {
    "exportTaskIdentifier": "export-snapshot-2024-01-15-103000",
    "iamRoleArn": "arn:aws:iam::123456789012:role/RDSExportRole",
    "kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
    "percentProgress": 0,
    "s3Bucket": "my-export-bucket",
    "s3Prefix": "exports/2024-01-15/",
    "snapshotTime": "2024-01-14T00:00:00Z",
    "sourceArn": "arn:aws:rds:us-east-1:123456789012:snapshot:my-db-snapshot",
    "status": "STARTING",
    "taskStartTime": "2024-01-15T10:30:00Z",
    "totalExtractedDataInGB": 0
  },
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/Admin/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "type": "AssumedRole"
  }
}

AWS RDS Snapshot Shared

#
Severity
high
Compliance
Stratus Red Team aws.exfiltration.rds-share-snapshot
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration, Transfer Data to Cloud Account
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An RDS snapshot was shared with another account. This could be an indicator of exfiltration.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if all(
        [
            event.get("eventSource", "") == "rds.amazonaws.com",
            event.get("eventName", "") == "ModifyDBSnapshotAttribute"
            or event.get("eventName", "") == "ModifyDBClusterSnapshotAttribute",
            event.deep_get("requestParameters", "attributeName") == "restore",
        ]
    ):
        current_account_id = event.deep_get("userIdentity", "accountId", default="")
        shared_account_ids = event.deep_get("requestParameters", "valuesToAdd", default=[])
        if shared_account_ids:
            return any(
                account_id for account_id in shared_account_ids if account_id != current_account_id
            )
        return False
    return False


def title(event):
    account_id = event.get("recipientAccountId", "<ACCOUNT_ID_NOT_FOUND>")
    rds_instance_id = event.deep_get(
        "responseElements", "dBInstanceIdentifier", default="<DB_INSTANCE_ID_NOT_FOUND>"
    )
    return f"RDS Snapshot Shared in [{account_id}] for RDS instance [{rds_instance_id}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_rds_snapshot_shared.py
RuleID: "AWS.RDS.SnapshotShared"
DisplayName: "AWS RDS Snapshot Shared"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration
  - Transfer Data to Cloud Account
Severity: High
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
  Stratus Red Team:
    - aws.exfiltration.rds-share-snapshot
Description: >
  An RDS snapshot was shared with another account. This could be an indicator of exfiltration.
Runbook: >
  Ensure that the snapshot was shared intentionally and with an approved account. If not, remove the snapshot and quarantine the compromised IAM user.
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ShareSnapshot.html
SummaryAttributes:
  - eventSource
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • any of:
    • eventName is ModifyDBSnapshotAttribute
    • eventName is ModifyDBClusterSnapshotAttribute
  • requestParameters.attributeName is restore
  • requestParameters.valuesToAdd is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierresponseElements.dBInstanceIdentifier

Response runbook

Ensure that the snapshot was shared intentionally and with an approved account. If not, remove the snapshot and quarantine the compromised IAM user.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "86581591-0f39-4eae-9a8d-b2224a3c91fa",
  "eventName": "ModifyDBSnapshotAttribute",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2023-12-12T20:12:22Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "b7f91314-eb8b-4be5-995d-6b97d70dfb3b",
  "requestParameters": {
    "attributeName": "restore",
    "dBSnapshotIdentifier": "exfiltration",
    "valuesToAdd": [
      "193672423079"
    ]
  },
  "responseElements": {
    "dBSnapshotAttributes": [
      {
        "attributeName": "restore",
        "attributeValues": [
          "193672423079"
        ]
      }
    ],
    "dBSnapshotIdentifier": "exfiltration"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "rds.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "68319f60-9dec-43b2-9702-de3a08c9d8a3",
  "userIdentity": {
    "accessKeyId": "ASIAFFA5AFEC02FFCD8ED",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_DevAdmin_635426549a280cc6/fake.user",
    "principalId": "AROA2DFDF0C1FDFCAD2B2:fake.user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2023-12-12T19:43:57Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin_635426549a280cc6",
        "principalId": "AROA2DFDF0C1FDFCAD2B2",
        "type": "Role",
        "userName": "AWSReservedSSO_DevAdmin_635426549a280cc6"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Redshift Cluster Encryption

#
Severity
high
Compliance
PCI 3.4
Resource types
AWS.Redshift.Cluster
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Clusters have encryption enabled.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):
    return bool(resource["Encrypted"])

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_encryption.py
PolicyID: "AWS.Redshift.Cluster.Encryption"
DisplayName: "AWS Redshift Cluster Encryption"
Enabled: true
ResourceTypes:
  - AWS.Redshift.Cluster
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 3.4
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  This policy validates that Redshift Clusters have encryption enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-encryption-enabled
Reference: https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-db-encryption.html

Stages and Predicates

Flags AWS.Redshift.Cluster resources when the condition below holds.

Condition

  • Encrypted is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Encryptedis_not_null(no value, null check)excludes:Encrypted

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Encryptedis_null
  • (no value, null check)
field:"Encrypted" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-encryption-enabled

AWS Redshift Cluster Has Acceptable Snapshot Retention Period

#
Severity
low
Compliance
PCI 3.1
Resource types
AWS.Redshift.Cluster
Tags
AWS, PCI, Database
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Cluster snapshot retention periods are set to an appropriate time. This ensures that records are kept long enough for compliance and security reasons, but no too long for compliance and performance reasons.

Detection logic

# Retention period in days
MIN_RETENTION_DAYS = 3
MAX_RETENTION_DAYS = 90


def policy(resource):

    return MIN_RETENTION_DAYS <= resource["AutomatedSnapshotRetentionPeriod"] <= MAX_RETENTION_DAYS

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_snapshot_retention_acceptable.py
PolicyID: "AWS.Redshift.Cluster.SnapshotRetentionAcceptable"
DisplayName: "AWS Redshift Cluster Has Acceptable Snapshot Retention Period"
Enabled: false
ResourceTypes:
  - AWS.Redshift.Cluster
Tags:
  - AWS
  - PCI
  - Database
Reports:
  PCI:
    - 3.1
Severity: Low
Description: >
  This policy validates that Redshift Cluster snapshot retention periods are set to an appropriate time. This ensures that records are kept long enough for compliance and security reasons, but no too long for compliance and performance reasons.
Runbook: >
  Adjust the Cluster snapshot retention period to be an appropriate length.
Reference: https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-snapshots.html

Stages and Predicates

Flags AWS.Redshift.Cluster resources when any of the conditions below holds.

Condition

  • any of:
    • AutomatedSnapshotRetentionPeriod is less than 3
    • AutomatedSnapshotRetentionPeriod is greater than 90

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Response runbook

Adjust the Cluster snapshot retention period to be an appropriate length.

AWS Redshift Cluster Logging

#
Severity
medium
Resource types
AWS.Redshift.Cluster
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Cluster have logging enabled. This includes audit logs.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    return deep_get(resource, "LoggingStatus", "LoggingEnabled", default=False)

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_logging.py
PolicyID: "AWS.Redshift.Cluster.Logging"
DisplayName: "AWS Redshift Cluster Logging"
Enabled: true
ResourceTypes:
  - AWS.Redshift.Cluster
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Severity: Medium
Description: >
  This policy validates that Redshift Cluster have logging enabled. This includes audit logs.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-logging-enabled
Reference: https://aws.amazon.com/premiumsupport/knowledge-center/logs-redshift-database-cluster/

Stages and Predicates

Flags AWS.Redshift.Cluster resources when the condition below holds.

Condition

  • LoggingStatus.LoggingEnabled is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LoggingStatus.LoggingEnabledis_not_null(no value, null check)excludes:LoggingStatus.LoggingEnabled

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-logging-enabled

AWS Redshift Cluster Maintenance Window

#
Severity
informational
Resource types
AWS.Redshift.Cluster
Tags
AWS, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Clusters have the correct preferred maintenance window configured.

Detection logic

MAINTENANCE_WINDOW = "sat:10:30-sat:11:00"


def policy(resource):
    return resource["PreferredMaintenanceWindow"] == MAINTENANCE_WINDOW

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_maintenance_window.py
PolicyID: "AWS.Redshift.Cluster.MaintenanceWindow"
DisplayName: "AWS Redshift Cluster Maintenance Window"
Enabled: false
ResourceTypes:
  - AWS.Redshift.Cluster
Tags:
  - AWS
  - Configuration Required
Severity: Info
Description: >
  This policy validates that Redshift Clusters have the correct preferred maintenance window configured.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-correct-preferred-maintenance-window
Reference: https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#rs-maintenance-windows

Stages and Predicates

Flags AWS.Redshift.Cluster resources when the condition below holds.

Condition

  • PreferredMaintenanceWindow is not sat:10:30-sat:11:00

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PreferredMaintenanceWindoweqsat:10:30-sat:11:00excludes:PreferredMaintenanceWindow field:"PreferredMaintenanceWindow" value:"sat:10:30-sat:11:00"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-correct-preferred-maintenance-window

AWS Redshift Cluster Snapshot Retention

#
Severity
medium
Resource types
AWS.Redshift.Cluster
Tags
AWS, Availability
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Clusters have sufficient snapshot retention periods, so that snapshots are not lost before they are needed.

Detection logic

# Retention period in days
RETENTION_PERIOD = 3


def policy(resource):
    return resource["AutomatedSnapshotRetentionPeriod"] >= RETENTION_PERIOD

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_snapshot_retention.py
PolicyID: "AWS.Redshift.Cluster.SnapshotRetention"
DisplayName: "AWS Redshift Cluster Snapshot Retention"
Enabled: true
ResourceTypes:
  - AWS.Redshift.Cluster
Tags:
  - AWS
  - Availability
Severity: Medium
Description: >
  This policy validates that Redshift Clusters have sufficient snapshot retention periods, so that snapshots are not lost before they are needed.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-sufficient-snapshot-retention-period
Reference: https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-snapshots.html

Stages and Predicates

Flags AWS.Redshift.Cluster resources when the condition below holds.

Condition

  • AutomatedSnapshotRetentionPeriod is less than 3

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AutomatedSnapshotRetentionPeriodge3excludes:AutomatedSnapshotRetentionPeriod field:"AutomatedSnapshotRetentionPeriod" value:"3"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-has-sufficient-snapshot-retention-period

AWS Redshift Cluster Version Upgrade

#
Severity
low
Compliance
PCI 6.2
Resource types
AWS.Redshift.Cluster
Tags
AWS, Security Control
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Redshift Clusters automatically perform upgrades during scheduled maintenance windows.

Detection logic

def policy(resource):
    # Explicit True check to avoid returning NoneType
    return resource["AllowVersionUpgrade"] is True

Rule specification

AnalysisType: policy
Filename: aws_redshift_cluster_version_upgrade.py
PolicyID: "AWS.Redshift.Cluster.VersionUpgrade"
DisplayName: "AWS Redshift Cluster Version Upgrade"
Enabled: true
ResourceTypes:
  - AWS.Redshift.Cluster
Tags:
  - AWS
  - Security Control
Reports:
  PCI:
    - 6.2
Severity: Low
Description: >
  This policy validates that Redshift Clusters automatically perform upgrades during scheduled maintenance windows.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-allows-version-upgrades
Reference: https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html

Stages and Predicates

Flags AWS.Redshift.Cluster resources when the condition below holds.

Condition

  • AllowVersionUpgrade is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AllowVersionUpgradeeqtrueexcludes:AllowVersionUpgrade field:"AllowVersionUpgrade" value:"true"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AllowVersionUpgradene
  • true transforms: boolean
field:"AllowVersionUpgrade" kind:ne value:"true"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-redshift-cluster-allows-version-upgrades

AWS Resource Made Public

#
Severity
medium
Entities
aws_account_ids, aws_arns
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration:Transfer Data to Cloud Account
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Some AWS resource was made publicly accessible over the internet. Checks ECR, Elasticsearch, KMS, S3, S3 Glacier, SNS, SQS, and Secrets Manager.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Detection logic

import json

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_base_helpers import deep_get
from policyuniverse.policy import Policy


def _has_organization_condition(statement):
    """
    Check if a policy statement has organization ID conditions that restrict access.

    Args:
        statement: A policyuniverse Statement object

    Returns:
        bool: True if organization conditions are found, False otherwise
    """
    # Check both the policyuniverse category and specific AWS condition keys
    for condition in statement.condition_entries:
        # Check policyuniverse category
        if condition.category == "organization":
            return True

        # Also check for specific AWS organization condition keys
        # These include aws:PrincipalOrgID, aws:SourceOrgID, aws:PrincipalOrgPaths, etc.
        condition_key = getattr(condition, "key", "").lower()
        if "orgid" in condition_key or "orgpath" in condition_key:
            return True

    # Alternative: Check raw conditions in the statement if condition_entries doesn't work
    if hasattr(statement, "statement"):
        raw_conditions = statement.statement.get("Condition", {})
        for conditions in raw_conditions.values():
            for key in conditions.keys():
                # Check for organization-related condition keys
                if any(
                    org_key in key.lower()
                    for org_key in ["principalorgid", "sourceorgid", "principalorgpaths"]
                ):
                    return True

    return False


# Check if a policy (string or JSON) allows resource accessibility via the Internet
def policy_is_internet_accessible(policy):
    """
    Check if a policy (string or JSON) allows resource accessibility via the Internet.

    Args:
        policy: A policy object that can be either a string or a JSON object

    Returns:
        bool: True if the policy allows internet access, False otherwise
    """
    # Handle empty policies (None, empty strings, empty dicts, etc.)
    if not policy:
        return False

    # Handle string policies by converting to JSON
    if isinstance(policy, str):
        try:
            policy = json.loads(policy)
        except json.JSONDecodeError:
            return False

    # Check if the policy has a wildcard principal but also has organization ID restrictions
    # which should not be considered internet accessible
    policy_obj = Policy(policy)

    # If policyuniverse thinks it's not internet accessible, trust that
    if not policy_obj.is_internet_accessible():
        return False

    # For policies with multiple statements, we need to check each statement individually
    # If ANY statement is truly internet accessible, the policy is internet accessible
    for statement in policy_obj.statements:
        if statement.effect != "Allow" or "*" not in statement.principals:
            continue

        # If this statement has a wildcard principal but no organization ID restrictions,
        # it's truly internet accessible
        if not _has_organization_condition(statement):
            return True

    return False


def rule(event):
    if not aws_cloudtrail_success(event):
        return False

    parameters = event.get("requestParameters", {})
    # Ignore events that are missing request params
    if not parameters:
        return False

    event_name = event.get("eventName", "")

    # Special case for SNS topic attributes that need additional attribute name check
    if event_name == "SetTopicAttributes" and parameters.get("attributeName", "") == "Policy":
        policy_value = parameters.get("attributeValue", {})
        return policy_is_internet_accessible(policy_value)

    # Map of event names to policy locations in parameters
    policy_location_map = {
        # S3
        "PutBucketPolicy": lambda p: p.get("bucketPolicy", {}),
        # ECR
        "SetRepositoryPolicy": lambda p: p.get("policyText", {}),
        # Elasticsearch
        "CreateElasticsearchDomain": lambda p: p.get("accessPolicies", {}),
        "UpdateElasticsearchDomainConfig": lambda p: p.get("accessPolicies", {}),
        # KMS
        "CreateKey": lambda p: p.get("policy", {}),
        "PutKeyPolicy": lambda p: p.get("policy", {}),
        # S3 Glacier
        "SetVaultAccessPolicy": lambda p: deep_get(p, "policy", "policy", default={}),
        # SNS & SQS
        "SetQueueAttributes": lambda p: deep_get(p, "attributes", "Policy", default={}),
        "CreateTopic": lambda p: deep_get(p, "attributes", "Policy", default={}),
        # SecretsManager
        "PutResourcePolicy": lambda p: p.get("resourcePolicy", {}),
    }

    # Get the policy extraction function for this event name
    policy_extractor = policy_location_map.get(event_name)
    if not policy_extractor:
        return False

    # Extract the policy using the appropriate function
    policy = policy_extractor(parameters)
    return policy_is_internet_accessible(policy)


def title(event):
    # TODO(): Update this rule to use data models
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity",
        "sessionContext",
        "sessionIssuer",
        "userName",
        default="<MISSING_USER>",
    )

    if event.get("Resources"):
        return f"Resource {event.get('Resources')[0].get('arn', 'MISSING')} made public by {user}"

    return f"{event.get('eventSource', 'MISSING SOURCE')} resource made public by {user}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_resource_made_public.py
RuleID: "AWS.CloudTrail.ResourceMadePublic"
DisplayName: "AWS Resource Made Public"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration:Transfer Data to Cloud Account
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Description: >
  Some AWS resource was made publicly accessible over the internet.
  Checks ECR, Elasticsearch, KMS, S3, S3 Glacier, SNS, SQS, and Secrets Manager.
Runbook: Adjust the policy so that the resource is no longer publicly accessible
Reference: https://aws.amazon.com/blogs/security/identifying-publicly-accessible-resources-with-amazon-vpc-network-access-analyzer/
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - vpcEndpointId
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • requestParameters is present
  • any of:
    • all of:
      • eventName is SetTopicAttributes
      • requestParameters.attributeName is Policy
      • requestParameters.attributeValue is present
    • eventName is not SetTopicAttributes
    • requestParameters.attributeName is not Policy

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
Resources
userNameuserIdentity.userName

Response runbook

Adjust the policy so that the resource is no longer publicly accessible

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "eu-west-1",
  "eventID": "685e066d-a3aa-4323-a6a1-2f187a2fc986",
  "eventName": "SetRepositoryPolicy",
  "eventSource": "ecr.amazonaws.com",
  "eventTime": "2020-11-20 06:19:05.000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "p_any_aws_account_ids": [
    "112233445566"
  ],
  "p_any_aws_arns": [
    "arn:aws:ecr:eu-west-1:112233445566:repository/community",
    "arn:aws:iam::112233445566:role/ServiceRole",
    "arn:aws:sts::112233445566:assumed-role/ServiceRole/AWSCloudFormation"
  ],
  "p_event_time": "2020-11-20 06:19:05.000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2020-11-20 06:31:53.258",
  "p_row_id": "ea68a92f0295a6bed49fa8af068faa05",
  "recipientAccountId": "112233445566",
  "requestID": "95fd6392-627c-467b-b940-895183d3298d",
  "requestParameters": {
    "force": false,
    "policyText": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"ecr:BatchCheckLayerAvailability\",\"ecr:BatchGetImage\",\"ecr:GetAuthorizationToken\",\"ecr:GetDownloadUrlForLayer\"],\"Effect\":\"Allow\",\"Principal\":\"*\",\"Sid\":\"PublicRead\"}]}",
    "repositoryName": "community"
  },
  "resources": [
    {
      "accountId": "112233445566",
      "arn": "arn:aws:ecr:eu-west-1:112233445566:repository/community"
    }
  ],
  "responseElements": {
    "policyText": "{\n  \"Version\" : \"2012-10-17\",\n  \"Statement\" : [ {\n    \"Sid\" : \"PublicRead\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : \"*\",\n    \"Action\" : [ \"ecr:BatchCheckLayerAvailability\", \"ecr:BatchGetImage\", \"ecr:GetAuthorizationToken\", \"ecr:GetDownloadUrlForLayer\" ]\n  } ]\n}",
    "registryId": "112233445566",
    "repositoryName": "community"
  },
  "sourceIPAddress": "cloudformation.amazonaws.com",
  "userAgent": "cloudformation.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "ASIAIJJG73VC6IW5OFVQ",
    "accountId": "112233445566",
    "arn": "arn:aws:sts::112233445566:assumed-role/ServiceRole/AWSCloudFormation",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "AROAJJJJTTTT44445IJJJ:AWSCloudFormation",
    "sessionContext": {
      "attributes": {
        "creationDate": "2020-11-20T06:19:04Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "112233445566",
        "arn": "arn:aws:iam::112233445566:role/ServiceRole",
        "principalId": "AROAJJJJTTTT44445IJJJ",
        "type": "Role",
        "userName": "ServiceRole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Resource Minimum Tags

#
Severity
low
Resource types
AWS.EC2.Instance, AWS.EC2.VPC, AWS.EC2.SecurityGroup, AWS.IAM.User
Tags
AWS, Configuration Required, Security Control
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that applicable resources have a minimum number of tags set.

Detection logic

MINIMUM_TAGS = 1


def policy(resource):
    if resource["Tags"] is None:
        return False

    return len(resource["Tags"]) >= MINIMUM_TAGS

Rule specification

AnalysisType: policy
Filename: aws_resource_minimum_tags.py
PolicyID: "AWS.Resource.MinimumTags"
DisplayName: "AWS Resource Minimum Tags "
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
  - AWS.EC2.VPC
  - AWS.EC2.SecurityGroup
  - AWS.IAM.User
Tags:
  - AWS
  - Configuration Required
  - Security Control
Severity: Low
Description: >
  This policy ensures that applicable resources have a minimum number of tags set.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-resource-has-minimum-number-of-tags
Reference: https://aws.amazon.com/answers/account-management/aws-tagging-strategies/

Stages and Predicates

Flags AWS.EC2.Instance, AWS.EC2.VPC, AWS.EC2.SecurityGroup (and 1 more) resources when any of the conditions below holds.

Condition

  • any of:
    • Tags is empty
    • Tags has length less than 1

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Tagsis_not_null(no value, null check)excludes:Tags
Tagslength_compare1excludes:Tags field:"Tags" value:"1"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Tagsis_null
  • (no value, null check)
field:"Tags" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-resource-has-minimum-number-of-tags

AWS Resource Required Tags

#
Severity
low
Resource types
AWS.EC2.Instance, AWS.EC2.SecurityGroup, AWS.EC2.VPC, AWS.IAM.User
Tags
AWS, Configuration Required, Security Control
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that AWS resources have specific tags, dependent on their resource type.

Detection logic

import json
from unittest.mock import MagicMock

# REQUIRED_TAGS_MAPPINGS maps resource types to a set of tag keys required for that resource.
# Example: REQUIRED_TAGS_MAPPINGS = {'AWS.EC2.Instance.Snapshot': ['Owner', 'CreatedBy']}
# The above example would check all EC2 instances for the presence of tags keyed Owner and CreatedBy

REQUIRED_TAGS_MAPPINGS = {}


def policy(resource):
    # pylint: disable=not-callable
    # pylint: disable=global-statement
    global REQUIRED_TAGS_MAPPINGS
    if isinstance(REQUIRED_TAGS_MAPPINGS, MagicMock):
        REQUIRED_TAGS_MAPPINGS = json.loads(REQUIRED_TAGS_MAPPINGS())
    if resource["ResourceType"] in REQUIRED_TAGS_MAPPINGS:
        required_tags = set(REQUIRED_TAGS_MAPPINGS[resource.get("ResourceType")])
        tags = resource.get("Tags")
        if not tags:
            tags = {}
        actual_tags = tags.keys()
        return required_tags.issubset(actual_tags)
    return True

Rule specification

AnalysisType: policy
Filename: aws_resource_required_tags.py
PolicyID: "AWS.Resource.RequiredTags"
DisplayName: "AWS Resource Required Tags"
Enabled: false
ResourceTypes:
  - AWS.EC2.Instance
  - AWS.EC2.SecurityGroup
  - AWS.EC2.VPC
  - AWS.IAM.User
Tags:
  - AWS
  - Configuration Required
  - Security Control
Severity: Low
Description: >
  This policy ensures that AWS resources have specific tags, dependent on their resource type.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-resource-has-required-tags
Reference: https://aws.amazon.com/answers/account-management/aws-tagging-strategies/

Stages and Predicates

Flags AWS.EC2.Instance, AWS.EC2.SecurityGroup, AWS.EC2.VPC (and 1 more) resources when the condition below holds.

Condition

  • ResourceType is not one of

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ResourceTypein(no value, null check)excludes:ResourceType

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-resource-has-required-tags

AWS Root Account Access Keys

#
Severity
critical
Compliance
CIS 1.12; PCI 2.2.2, 7.1, 8.2
Resource types
AWS.IAM.RootUser
Tags
AWS, Identity & Access Management, Persistence:Account Manipulation
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Validates that no programmatic access keys exist for the AWS root account. Root access keys provide unrestricted access to all AWS resources and cannot have permissions limited. If compromised, these keys grant attackers complete account control including resource modification, data access, and billing changes.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    return not deep_get(resource, "CredentialReport", "AccessKey1Active") and not deep_get(
        resource, "CredentialReport", "AccessKey2Active"
    )

Rule specification

AnalysisType: policy
Filename: aws_root_account_access_keys.py
PolicyID: "AWS.RootAccount.AccessKeys"
DisplayName: "AWS Root Account Access Keys"
Enabled: true
ResourceTypes:
  - AWS.IAM.RootUser
Tags:
  - AWS
  - Identity & Access Management
  - Persistence:Account Manipulation
Reports:
  CIS:
    - 1.12
  PCI:
    - 2.2.2
    - 7.1
    - 8.2
  MITRE ATT&CK:
    - TA0003:T1098
Severity: Critical
Description: >
  Validates that no programmatic access keys exist for the AWS root account. Root access keys provide unrestricted access to all AWS resources and cannot have permissions limited. If compromised, these keys grant attackers complete account control including resource modification, data access, and billing changes.
Runbook: |
  1. Query CloudTrail for all API calls where userIdentity.type equals "Root" in the past 90 days to identify if the root access keys are actively being used and document which applications or automation depend on them
  2. Create IAM users or roles with minimum required permissions, update all applications to use the new IAM credentials, then delete the root access keys via AWS Console Security Credentials page
  3. Enable CloudWatch Events rules to alert on future root account activity and consider implementing AWS Organizations SCPs to prevent root access key creation
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.IAM.RootUser resources when any of the conditions below holds.

Condition

  • any of:
    • CredentialReport.AccessKey1Active is present
    • CredentialReport.AccessKey2Active is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReport.AccessKey1Activeis_null(no value, null check)excludes:CredentialReport.AccessKey1Active
CredentialReport.AccessKey2Activeis_null(no value, null check)excludes:CredentialReport.AccessKey2Active

Indicators

These rows show field, operator, and value matches.

Response runbook

1. Query CloudTrail for all API calls where userIdentity.type equals "Root" in the past 90 days to identify if the root access keys are actively being used and document which applications or automation depend on them

2. Create IAM users or roles with minimum required permissions, update all applications to use the new IAM credentials, then delete the root access keys via AWS Console Security Credentials page

3. Enable CloudWatch Events rules to alert on future root account activity and consider implementing AWS Organizations SCPs to prevent root access key creation

AWS Root Account Hardware MFA

#
Severity
high
Compliance
CIS 1.13, 1.14; PCI 7.1.2
Resource types
AWS.IAM.RootUser
Tags
AWS, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that a hardware MFA device is in use for access to the root account.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    if not deep_get(resource, "CredentialReport", "MfaActive"):
        # MFA is not enabled, this is reported by a different rule
        return True
    return resource["VirtualMFA"] is None

Rule specification

AnalysisType: policy
Filename: aws_root_account_hardware_mfa.py
PolicyID: "AWS.RootAccount.HardwareMFA"
DisplayName: "AWS Root Account Hardware MFA"
Enabled: true
ResourceTypes:
  - AWS.IAM.RootUser
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 1.13
    - 1.14
  PCI:
    - 7.1.2
  MITRE ATT&CK:
    - TA0004:T1078
Severity: High
Description: >
  This policy validates that a hardware MFA device is in use for access to the root account.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-root-account-has-mfa-enabled
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.IAM.RootUser resources when all of the conditions below hold.

Condition

  • CredentialReport.MfaActive is present
  • VirtualMFA is present

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReport.MfaActiveis_null(no value, null check)excludes:CredentialReport.MfaActive
VirtualMFAis_null(no value, null check)excludes:VirtualMFA

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CredentialReport.MfaActiveis_not_null
  • (no value, null check)
field:"CredentialReport.MfaActive" kind:is_not_null
VirtualMFAis_not_null
  • (no value, null check)
field:"VirtualMFA" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-root-account-has-mfa-enabled

AWS Root Account MFA

#
Severity
critical
Compliance
CIS 1.13; PCI 8.3, 8.4
Resource types
AWS.IAM.RootUser
Tags
AWS, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Validates that Multi-Factor Authentication (MFA) is enabled for the AWS root account. The root account has complete unrestricted access to all AWS resources and is the highest-value target for attackers. Without MFA, accounts are vulnerable to phishing, credential stuffing, and password compromise attacks.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    # Explicit check for True as the value may be None, and we want to return a bool not a NoneType
    return deep_get(resource, "CredentialReport", "MfaActive") is True

Rule specification

AnalysisType: policy
Filename: aws_root_account_mfa.py
PolicyID: "AWS.RootAccount.MFA"
DisplayName: "AWS Root Account MFA"
Enabled: true
ResourceTypes:
  - AWS.IAM.RootUser
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 1.13
  PCI:
    - 8.3
    - 8.4
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Critical
Description: >
  Validates that Multi-Factor Authentication (MFA) is enabled for the AWS root account. The root account has complete unrestricted access to all AWS resources and is the highest-value target for attackers. Without MFA, accounts are vulnerable to phishing, credential stuffing, and password compromise attacks.
Runbook: |
  1. Query CloudTrail for ConsoleLogin events where userIdentity.type equals "Root" in the past 90 days to identify when the root account was last used and assess the risk window without MFA
  2. Sign in to AWS Console as root, navigate to Security Credentials, assign an MFA device (Virtual MFA, Hardware TOTP, or FIDO security key), and test the MFA configuration by logging out and back in
  3. Store the root password and MFA device in separate secure locations, document the MFA device details, and implement AWS Organizations SCPs to monitor root account console logins
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

Stages and Predicates

Flags AWS.IAM.RootUser resources when the condition below holds.

Condition

  • CredentialReport.MfaActive is not true

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReport.MfaActiveeqtrueexcludes:CredentialReport.MfaActive field:"CredentialReport.MfaActive" value:"true"

Indicators

These rows show field, operator, and value matches.

Response runbook

1. Query CloudTrail for ConsoleLogin events where userIdentity.type equals "Root" in the past 90 days to identify when the root account was last used and assess the risk window without MFA

2. Sign in to AWS Console as root, navigate to Security Credentials, assign an MFA device (Virtual MFA, Hardware TOTP, or FIDO security key), and test the MFA configuration by logging out and back in

3. Store the root password and MFA device in separate secure locations, document the MFA device details, and implement AWS Organizations SCPs to monitor root account console logins

AWS S3 Access Error

#
Severity
informational
Log types
AWS.S3ServerAccess
Tags
AWS, Security Control, Discovery:Cloud Storage Object Discovery
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks for errors during S3 Object access. This could be due to insufficient access permissions, non-existent buckets, or other reasons.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_base_helpers import pattern_match

# https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
HTTP_STATUS_CODES_TO_MONITOR = {
    403,  # Forbidden
    405,  # Method Not Allowed
}


def rule(event):
    if event.get("useragent", "").startswith("aws-internal"):
        return False

    return (
        pattern_match(event.get("operation", ""), "REST.*.OBJECT")
        and event.get("httpstatus") in HTTP_STATUS_CODES_TO_MONITOR
    )


def title(event):
    return f"{event.get('httpstatus')} errors found to S3 Bucket [{event.get('bucket')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_access_error.py
RuleID: "AWS.S3.ServerAccess.Error"
DisplayName: "AWS S3 Access Error"
DedupPeriodMinutes: 180
Threshold: 5
Enabled: true
LogTypes:
  - AWS.S3ServerAccess
Tags:
  - AWS
  - Security Control
  - Discovery:Cloud Storage Object Discovery
Reports:
  MITRE ATT&CK:
    - TA0007:T1619
Severity: Info
Description: >
  Checks for errors during S3 Object access.
  This could be due to insufficient access permissions, non-existent buckets, or other reasons.
Runbook: >
  Investigate the specific error and determine if it is an ongoing issue that needs to be addressed or a one off or transient error that can be ignored.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/ErrorCode.html
SummaryAttributes:
  - bucket
  - key
  - requester
  - remoteip
  - operation
  - errorCode

Stages and Predicates

Fires on AWS.S3ServerAccess events when all of the conditions below hold.

Condition

  • useragent does not start with aws-internal
  • operation matches the pattern REST.*.OBJECT
  • httpstatus is one of 403, 405
Alert cadence
alerts after 5 matches within 3h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
useragentstarts_withaws-internalexcludes:useragent field:"useragent" value:"aws-internal"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
httpstatusin
  • 403 transforms: number
  • 405 transforms: number
field:"httpstatus" kind:in
operationwildcard
  • REST.*.OBJECT
field:"operation" kind:wildcard value:"REST.*.OBJECT"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
httpstatus
bucket

Response runbook

Investigate the specific error and determine if it is an ongoing issue that needs to be addressed or a one off or transient error that can be ignored.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "bucket": "panther-auditlogs",
  "errorcode": "AccessDenied",
  "httpstatus": 403,
  "operation": "REST.GET.OBJECT",
  "remoteip": "10.106.38.245",
  "requester": "arn:aws:iam::162777425019:user/awslogsdelivery",
  "requestid": "5CDAB4038253B0E4",
  "time": "2020-04-22 07:48:45.000",
  "tlsversion": "TLSv1.2"
}

AWS S3 Access IP Allowlist

#
Severity
medium
Log types
AWS.S3ServerAccess
Tags
AWS, Configuration Required, Identity & Access Management, Collection:Data From Cloud Storage Object
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks that the remote IP accessing the S3 bucket is in the IP allowlist.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from ipaddress import IPv4Network, IPv6Network, ip_network

from panther_aws_helpers import aws_rule_context

BUCKETS_TO_MONITOR = {
    # Example bucket names to watch go here
}
ALLOWLIST_NETWORKS = {
    # IP addresses (in CIDR notation) indicating approved IP ranges for accessing S3 buckets}
    ip_network("10.0.0.0/8"),
}


def rule(event):
    if BUCKETS_TO_MONITOR:
        if event.get("bucket") not in BUCKETS_TO_MONITOR:
            return False

    if "remoteip" not in event:
        return False

    cidr_ip = ip_network(event.get("remoteip"))
    return not any(
        is_subnet(approved_ip_range, cidr_ip) for approved_ip_range in ALLOWLIST_NETWORKS
    )


def title(event):
    return f"Non-Approved IP access to S3 Bucket [{event.get('bucket', '<UNKNOWN_BUCKET>')}]"


def alert_context(event):
    return aws_rule_context(event)


def is_subnet(supernet: IPv4Network | IPv6Network, subnet: IPv4Network | IPv6Network) -> bool:
    """Return true if 'subnet' is a subnet of 'supernet'"""
    # We can't do a classic subnet comparison between v4 and v6 networks, so we have to explictly
    #   check for version mismatch first
    if supernet.network_address.version != subnet.network_address.version:
        return False
    # Else, do the subnet calculation
    return subnet.subnet_of(supernet)

Rule specification

AnalysisType: rule
Filename: aws_s3_access_ip_allowlist.py
RuleID: "AWS.S3.ServerAccess.IPWhitelist"
DisplayName: "AWS S3 Access IP Allowlist"
DedupPeriodMinutes: 60 # 1 hour
Enabled: false
LogTypes:
  - AWS.S3ServerAccess
Tags:
  - AWS
  - Configuration Required
  - Identity & Access Management
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Medium
Description: >
  Checks that the remote IP accessing the S3 bucket is in the IP allowlist.
Runbook: >
  Verify whether unapproved access of S3 objects occurred, and take appropriate steps to remediate damage (for example, informing related parties of unapproved access and potentially invalidating data that was accessed). Consider updating the access policies of the S3 bucket to prevent future unapproved access.
Reference: https://aws.amazon.com/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/
SummaryAttributes:
  - bucket
  - key
  - remoteip

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucket

Response runbook

Verify whether unapproved access of S3 objects occurred, and take appropriate steps to remediate damage (for example, informing related parties of unapproved access and potentially invalidating data that was accessed). Consider updating the access policies of the S3 bucket to prevent future unapproved access.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "bucket": "my-test-bucket",
  "remoteip": "11.0.0.1"
}

AWS S3 Bucket Action Restrictions

#
Severity
medium
Compliance
PCI 10.5.1, 10.5.2
Resource types
AWS.S3.Bucket
Tags
AWS, Identity & Access Management, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that the S3 bucket policy does not allow any action on the bucket, in accordance with the principal of least privilege.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

import json

from policyuniverse.expander_minimizer import minimize_statement_actions

BAD_ACTIONS = {
    "*",
    "s3:*",
}


def policy(resource):
    if resource["Policy"] is None:
        return True

    iam_policy = json.loads(resource["Policy"])
    for statement in iam_policy["Statement"]:
        # Only check statements granting access
        if statement["Effect"] != "Allow":
            continue

        minimized_actions = minimize_statement_actions(statement)
        if BAD_ACTIONS.intersection(minimized_actions):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_action_restrictions.py
PolicyID: "AWS.S3.Bucket.ActionRestrictions"
DisplayName: "AWS S3 Bucket Action Restrictions"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Identity & Access Management
  - Impact:Data Destruction
Reports:
  PCI:
    - 10.5.1
    - 10.5.2
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Medium
Description: >
  Ensures that the S3 bucket policy does not allow any action on the bucket, in accordance with the principal of least privilege.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-restricts-allowed-actions
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Policy is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_not_null
  • (no value, null check)
field:"Policy" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-restricts-allowed-actions

AWS S3 Bucket Encryption

#
Severity
high
Compliance
PCI 2.2.3, 3.4
Resource types
AWS.S3.Bucket
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that the S3 bucket has encryption enabled.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_base_helpers import deep_get


def policy(resource):
    for encryption_rule in resource["EncryptionRules"] or []:
        if encryption_rule.get("ApplyServerSideEncryptionByDefault", False):
            return (
                deep_get(encryption_rule, "ApplyServerSideEncryptionByDefault", "SSEAlgorithm")
                is not None
            )

    return False

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_encryption.py
PolicyID: "AWS.S3.Bucket.Encryption"
DisplayName: "AWS S3 Bucket Encryption"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 2.2.3
    - 3.4
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  Ensures that the S3 bucket has encryption enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-encryption-enabled
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-encryption-enabled

AWS S3 Bucket Lifecycle Configuration

#
Severity
low
Compliance
PCI 3.1, 10.7
Resource types
AWS.S3.Bucket
Tags
AWS, Configuration Required, Security Control
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

Verifies that the S3 Bucket Object Lifecycle configuration expires data within 90 and 365 days.

Detection logic

from panther_base_helpers import deep_get

MAX_RETENTION_PERIOD = 365
MIN_RETENTION_PERIOD = 90


def policy(resource):
    if resource.get("LifecycleRules") is None:
        return False

    for lifecycle_rule in resource.get("LifecycleRules", []):
        if lifecycle_rule.get("Status") != "Enabled":
            continue

        rule_retention_period_days = deep_get(lifecycle_rule, "Expiration", "Days")

        if not rule_retention_period_days:
            continue

        if MIN_RETENTION_PERIOD <= rule_retention_period_days <= MAX_RETENTION_PERIOD:
            return True

    return False

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_lifecycle_configuration.py
PolicyID: "AWS.S3.Bucket.LifecycleConfiguration"
DisplayName: "AWS S3 Bucket Lifecycle Configuration"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Configuration Required
  - Security Control
Reports:
  PCI:
    - 3.1
    - 10.7
Severity: Low
Description: >
  Verifies that the S3 Bucket Object Lifecycle configuration expires data within 90 and 365 days.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-lifecycle-configuration-expires-data
Reference: https://amzn.to/2visoXr

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • LifecycleRules is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LifecycleRulesis_not_null(no value, null check)excludes:LifecycleRules

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LifecycleRulesis_null
  • (no value, null check)
field:"LifecycleRules" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-lifecycle-configuration-expires-data

AWS S3 Bucket Logging

#
Severity
low
Resource types
AWS.S3.Bucket
Tags
AWS, Monitoring, Security Control, Defense Evasion:Impair Defenses
Reference
blog.runpanther.io
Source
github.com/panther-labs/panther-analysis

Ensures that a logging policy is set for the S3 bucket.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def policy(resource):
    return resource["LoggingPolicy"] is not None

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_logging.py
PolicyID: "AWS.S3.Bucket.Logging"
DisplayName: "AWS S3 Bucket Logging"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Monitoring
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Low
Description: >
  Ensures that a logging policy is set for the S3 bucket.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-logging-enabled
Reference: https://blog.runpanther.io/s3-bucket-access-logging/

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • LoggingPolicy is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LoggingPolicyis_not_null(no value, null check)excludes:LoggingPolicy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LoggingPolicyis_null
  • (no value, null check)
field:"LoggingPolicy" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-logging-enabled

AWS S3 Bucket MFA Delete

#
Severity
low
Resource types
AWS.S3.Bucket
Tags
AWS, Data Protection, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that MFA delete is enabled for a bucket so that all objects can only be deleted by users authenticated with MFA.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

def policy(resource):
    return resource["MFADelete"] == "Enabled"

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_mfa_delete.py
PolicyID: "AWS.S3.Bucket.MFADelete"
DisplayName: "AWS S3 Bucket MFA Delete"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Data Protection
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Low
Description: >
  Ensures that MFA delete is enabled for a bucket so that all objects can only be deleted by users authenticated with MFA.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-has-mfa-delete-enabled
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMFADelete.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • MFADelete is not Enabled

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
MFADeleteeqEnabledexcludes:MFADelete field:"MFADelete" value:"Enabled"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-has-mfa-delete-enabled

AWS S3 Bucket Name DNS Compliance

#
Severity
informational
Resource types
AWS.S3.Bucket
Tags
AWS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the AWS S3 bucket name is DNS compliant.

Detection logic

def policy(resource):
    return "." not in resource["Name"]

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_name_dns_compliance.py
PolicyID: "AWS.S3.Bucket.NameDNSCompliance"
DisplayName: "AWS S3 Bucket Name DNS Compliance"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
Severity: Info
Description: >
  This policy validates that the AWS S3 bucket name is DNS compliant.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-name-has-no-periods
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Name contains .

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Namecontains
  • .
field:"Name" kind:contains value:"."

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-name-has-no-periods

AWS S3 Bucket Object Lock Configured

#
Severity
low
Compliance
PCI 10.5.3
Resource types
AWS.S3.Bucket
Tags
AWS, S3, PCI, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that S3 buckets have an Object Lock configuration enabled. This should be used with specific suppression lists to ensure it is applied only to appropriate S3 buckets, such as those containing CloudTrail or other auditable records.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_base_helpers import deep_get

RETENTION_PERIOD_DAYS = 365


def policy(resource):

    object_lock = resource["ObjectLockConfiguration"]

    # Object lock configuration is not enabled, or enabled without a rule
    if not object_lock or object_lock["ObjectLockEnabled"] != "Enabled" or not object_lock["Rule"]:
        return False

    # Ensure ObjectLockConfiguration is in COMPLIANCE mode, not GOVERNANCE mode
    if deep_get(object_lock, "Rule", "DefaultRetention", "Mode") != "COMPLIANCE":
        return False

    return (
        deep_get(object_lock, "Rule", "DefaultRetention", "Days", default=0)
        >= RETENTION_PERIOD_DAYS
    )

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_object_lock_configured.py
PolicyID: "AWS.S3.BucketObjectLockConfigured"
DisplayName: "AWS S3 Bucket Object Lock Configured"
Enabled: false
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - S3
  - PCI
  - Impact:Data Destruction
Reports:
  PCI:
    - 10.5.3
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Low
Description: >
  This policy validates that S3 buckets have an Object Lock configuration enabled. This should be used with specific suppression lists to ensure it is applied only to appropriate S3 buckets, such as those containing CloudTrail or other auditable records.
Runbook: Create a new S3 bucket with an appropriate Object Lock configuration and point audit records to that bucket.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock.html

Stages and Predicates

Flags AWS.S3.Bucket resources when any of the conditions below holds.

Condition

  • any of:
    • ObjectLockConfiguration is empty
    • ObjectLockConfiguration.ObjectLockEnabled is not Enabled
    • ObjectLockConfiguration.Rule is empty
    • ObjectLockConfiguration.Rule.DefaultRetention.Mode is not COMPLIANCE
    • ObjectLockConfiguration.Rule.DefaultRetention.Days is less than 365

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ObjectLockConfigurationis_null(no value, null check)excludes:ObjectLockConfiguration
ObjectLockConfiguration.ObjectLockEnabledneEnabledexcludes:ObjectLockConfiguration.ObjectLockEnabled field:"ObjectLockConfiguration.ObjectLockEnabled" value:"Enabled"
ObjectLockConfiguration.Ruleis_null(no value, null check)excludes:ObjectLockConfiguration.Rule
ObjectLockConfiguration.Rule.DefaultRetention.Daysge365excludes:ObjectLockConfiguration.Rule.DefaultRetention.Days field:"ObjectLockConfiguration.Rule.DefaultRetention.Days" value:"365"
ObjectLockConfiguration.Rule.DefaultRetention.ModeeqCOMPLIANCEexcludes:ObjectLockConfiguration.Rule.DefaultRetention.Mode field:"ObjectLockConfiguration.Rule.DefaultRetention.Mode" value:"COMPLIANCE"

Indicators

These rows show field, operator, and value matches.

Response runbook

Create a new S3 bucket with an appropriate Object Lock configuration and point audit records to that bucket.

AWS S3 Bucket Policy Allow With Not Principal

#
Severity
high
Resource types
AWS.S3.Bucket
Tags
AWS, Identity & Access Management, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Prevents the use of a 'Not' principal in conjunction with an allow effect in an S3 bucket policy, which would allow global access for the resource besides the principals specified.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

import json

from policyuniverse.policy import Policy


def policy(resource):
    if resource["Policy"] is None:
        return True

    iam_policy = Policy(json.loads(resource["Policy"]))

    for statement in iam_policy.statements:
        if statement.effect == "Allow" and statement.uses_not_principal():
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_policy_allow_with_not_principal.py
PolicyID: "AWS.S3.Bucket.PolicyAllowWithNotPrincipal"
DisplayName: "AWS S3 Bucket Policy Allow With Not Principal"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Identity & Access Management
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  Prevents the use of a 'Not' principal in conjunction with an allow effect in an S3 bucket policy, which would allow global access for the resource besides the principals specified.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-does-not-use-allow-with-not-principal
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Policy is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_not_null
  • (no value, null check)
field:"Policy" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-does-not-use-allow-with-not-principal

AWS S3 Bucket Policy Modified

#
Severity
informational
Compliance
CIS 3.8; Stratus Red Team aws.defense-evasion.cloudtrail-lifecycle-rule, aws.exfiltration.s3-backdoor-bucket-policy
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An S3 Bucket was modified.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of KMS CMK Deletion
S3_POLICY_CHANGE_EVENTS = {
    "PutBucketAcl",
    "PutBucketPolicy",
    "PutBucketCors",
    "PutBucketLifecycle",
    "PutBucketReplication",
    "DeleteBucketPolicy",
    "DeleteBucketCors",
    "DeleteBucketLifecycle",
    "DeleteBucketReplication",
}


def rule(event):
    return event.get("eventName") in S3_POLICY_CHANGE_EVENTS and aws_cloudtrail_success(event)


def title(event):
    return f"S3 bucket modified by [{event.deep_get('userIdentity', 'arn')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_bucket_policy_modified.py
RuleID: "AWS.S3.BucketPolicyModified"
DisplayName: "AWS S3 Bucket Policy Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Exfiltration:Exfiltration Over Web Service
Reports:
  CIS:
    - 3.8
  MITRE ATT&CK:
    - TA0010:T1567

  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-lifecycle-rule
    - aws.exfiltration.s3-backdoor-bucket-policy
Severity: Info
DedupPeriodMinutes: 720 # 12 hours
Description: >
  An S3 Bucket was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-s3-bucket-policy-modified
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is one of PutBucketAcl, PutBucketPolicy, PutBucketCors, PutBucketLifecycle, PutBucketReplication (+4 more values, see Indicators below)
  • errorCode is empty
  • errorMessage is empty
Alert deduplication
repeat matches within 12h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DeleteBucketCors
  • DeleteBucketLifecycle
  • DeleteBucketPolicy
  • DeleteBucketReplication
  • PutBucketAcl
  • PutBucketCors
  • PutBucketLifecycle
  • PutBucketPolicy
  • PutBucketReplication
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-s3-bucket-policy-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "AuthenticationMethod": "AuthHeader",
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4"
  },
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "PutBucketAcl",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "acl": [
      ""
    ],
    "bucketName": "bucket",
    "host": [
      "bucket.s3.us-west-2.amazonaws.com"
    ],
    "x-amz-acl": [
      "private"
    ]
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

AWS S3 Bucket Principal Restrictions

#
Severity
high
Compliance
PCI 10.5.1, 10.5.2
Resource types
AWS.S3.Bucket
Tags
AWS, Identity & Access Management, Collection:Data From Cloud Storage Object
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that S3 Bucket access policies do not allow all users (Principal:"*") for a given action on the bucket, in accordance with the principle of least privilege.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

import json

from policyuniverse.policy import Policy

BAD_PRINCIPALS = {
    "*",
}


def policy(resource):
    if resource["Policy"] is None:
        return True

    iam_policy = Policy(json.loads(resource["Policy"]))

    for statement in iam_policy.statements:
        # Only apply to allow effects
        if statement.effect != "Allow":
            continue

        # Don't apply where there are strong conditions
        if statement.condition_entries:
            continue

        if BAD_PRINCIPALS.intersection(statement.principals):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_principal_restrictions.py
PolicyID: "AWS.S3.Bucket.PrincipalRestrictions"
DisplayName: "AWS S3 Bucket Principal Restrictions"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Identity & Access Management
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 10.5.1
    - 10.5.2
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  This policy validates that S3 Bucket access policies do not allow all users (Principal:"*") for a given action on the bucket, in accordance with the principle of least privilege.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-restricts-principal
Reference: https://aws.amazon.com/premiumsupport/knowledge-center/secure-s3-resources/

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Policy is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_not_null
  • (no value, null check)
field:"Policy" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-restricts-principal

AWS S3 Bucket Public Access Block

#
Severity
medium
Compliance
PCI 2.2.3
Resource types
AWS.S3.Bucket
Tags
AWS, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that a Public Access Block Configuration is set for the given S3 bucket.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

def policy(resource):
    return any((resource["PublicAccessBlockConfiguration"] or {}).values())

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_public_access_block.py
PolicyID: "AWS.S3.Bucket.PublicAccessBlock"
DisplayName: "AWS S3 Bucket Public Access Block"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 2.2.3
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Medium
Description: >
  Ensures that a Public Access Block Configuration is set for the given S3 bucket.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-public-access-block-enabled
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • PublicAccessBlockConfiguration is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PublicAccessBlockConfigurationis_not_null(no value, null check)excludes:PublicAccessBlockConfiguration

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-public-access-block-enabled

AWS S3 Bucket Public Read

#
Severity
high
Compliance
PCI 10.5.1
Resource types
AWS.S3.Bucket
Tags
AWS, Identity & Access Management, Data Protection, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that the S3 bucket is not publicly readable.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_base_helpers import deep_get

GRANTEES = {
    "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
    "http://acs.amazonaws.com/groups/global/AllUsers",
}
PERMISSIONS = {"READ"}


def policy(resource):
    for grant in resource["Grants"] or []:
        if deep_get(grant, "Grantee", "URI") in GRANTEES and grant.get("Permission") in PERMISSIONS:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_public_read.py
PolicyID: "AWS.S3.Bucket.PublicRead"
DisplayName: "AWS S3 Bucket Public Read"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Identity & Access Management
  - Data Protection
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 10.5.1
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: >
  Ensures that the S3 bucket is not publicly readable.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-not-publicly-readable
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • any element of Grants matches all of:
    • Grants.Grantee.URI is one of http://acs.amazonaws.com/groups/global/AuthenticatedUsers, http://acs.amazonaws.com/groups/global/AllUsers
    • Grants.Permission is one of READ

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-not-publicly-readable

AWS S3 Bucket Public Write

#
Severity
critical
Compliance
CIS S3.8; PCI 1.3.1, 7.1, 7.2.1
Resource types
AWS.S3.Bucket
Tags
AWS, Data Protection, Identity & Access Management, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures S3 buckets are not publicly writeable, preventing critical security vulnerabilities. Public write access allows attackers to upload malicious content, delete legitimate data, or consume storage for massive AWS bills. Attackers can use writable buckets for C2 infrastructure, malware distribution, or supply chain attacks.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_base_helpers import deep_get

GRANTEES = {
    "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
    "http://acs.amazonaws.com/groups/global/AllUsers",
}
PERMISSIONS = {"WRITE", "WRITE_ACP", "FULL_CONTROL"}


def policy(resource):
    for grant in resource["Grants"] or []:
        if deep_get(grant, "Grantee", "URI") in GRANTEES and grant.get("Permission") in PERMISSIONS:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_public_write.py
PolicyID: "AWS.S3.Bucket.PublicWrite"
DisplayName: "AWS S3 Bucket Public Write"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Data Protection
  - Identity & Access Management
  - Impact:Data Destruction
Reports:
  CIS:
    - S3.8
  PCI:
    - 1.3.1
    - 7.1
    - 7.2.1
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Critical
Description: >
  Ensures S3 buckets are not publicly writeable, preventing critical security vulnerabilities. Public write access allows attackers to upload malicious content, delete legitimate data, or consume storage for massive AWS bills. Attackers can use writable buckets for C2 infrastructure, malware distribution, or supply chain attacks.
Runbook: |
  1. Query CloudTrail for s3:PutObject, s3:DeleteObject, and s3:PutObjectAcl events on the bucket in the past 30 days to identify any unauthorized write operations from unfamiliar IP addresses or principals
  2. Remove public write permissions from the bucket's ACL and bucket policy, enable all four S3 Block Public Access settings, and quarantine or delete any malicious objects that were uploaded
  3. Enable S3 server access logging and CloudTrail S3 data events for ongoing monitoring, and review how the public write access was configured to prevent recurrence
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-iam-policies.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • any element of Grants matches all of:
    • Grants.Grantee.URI is one of http://acs.amazonaws.com/groups/global/AuthenticatedUsers, http://acs.amazonaws.com/groups/global/AllUsers
    • Grants.Permission is one of WRITE, WRITE_ACP, FULL_CONTROL

Response runbook

1. Query CloudTrail for s3:PutObject, s3:DeleteObject, and s3:PutObjectAcl events on the bucket in the past 30 days to identify any unauthorized write operations from unfamiliar IP addresses or principals

2. Remove public write permissions from the bucket's ACL and bucket policy, enable all four S3 Block Public Access settings, and quarantine or delete any malicious objects that were uploaded

3. Enable S3 server access logging and CloudTrail S3 data events for ongoing monitoring, and review how the public write access was configured to prevent recurrence

AWS S3 Bucket Secure Access

#
Severity
low
Compliance
PCI 2.2.3, 4.1
Resource types
AWS.S3.Bucket
Tags
AWS, Security Control, Collection:Data From Cloud Storage Object
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures access to S3 buckets is forced to use a secure (HTTPS) connection.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

import json

from panther_base_helpers import deep_get
from policyuniverse.policy import Policy

# According to AWS there should exist an explicit deny policy that contains
# "aws:SecureTransport": "false" for this check to be compliant
# https://aws.amazon.com/premiumsupport/knowledge-center/s3-bucket-policy-for-config-rule/
# This policy returns a LOW severity alert if the bucket policy is using an implicit deny
# of secure transport or a HIGH severity if secure transport is not enforced.

IMPLICIT_DENY = True


def policy(resource):
    if resource["Policy"] is None:
        return False

    explicit_deny = False
    global IMPLICIT_DENY  # pylint: disable=global-statement
    # Reset global to prevent it getting stepped on by reuse in the Lambda invocation
    IMPLICIT_DENY = True

    iam_policy = Policy(json.loads(resource["Policy"]))

    for statement in iam_policy.statements:
        if (
            statement.effect == "Deny"
            and deep_get(statement.statement, "Condition", "Bool", "aws:SecureTransport") != "true"
        ):
            explicit_deny = True
            break

        if (
            statement.effect == "Allow"
            and deep_get(statement.statement, "Condition", "Bool", "aws:SecureTransport") != "true"
        ):
            IMPLICIT_DENY = False

    return explicit_deny


def severity(_):
    if IMPLICIT_DENY:
        return "LOW"
    return "HIGH"


def title(resource):
    if IMPLICIT_DENY:
        return f"{resource.get('Name')} lacks an explicit deny policy for Secure Transport"
    return f"{resource.get('Name')} does not enforce Secure Transport"

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_secure_access.py
PolicyID: "AWS.S3.Bucket.SecureAccess"
DisplayName: "AWS S3 Bucket Secure Access"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Security Control
  - Collection:Data From Cloud Storage Object
Reports:
  PCI:
    - 2.2.3
    - 4.1
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: >
  Ensures access to S3 buckets is forced to use a secure (HTTPS) connection.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-enforces-secure-access
Reference: https://aws.amazon.com/premiumsupport/knowledge-center/secure-s3-resources/

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Policy is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_not_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_null
  • (no value, null check)
field:"Policy" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
Name

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-policy-enforces-secure-access

AWS S3 Bucket Versioning

#
Severity
low
Compliance
PCI 10.5.3, 10.5.5
Resource types
AWS.S3.Bucket
Tags
AWS, Security Control, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks that object versioning is enabled in the S3 bucket.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

def policy(resource):
    return resource["Versioning"] == "Enabled"

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_versioning.py
PolicyID: "AWS.S3.Bucket.Versioning"
DisplayName: "AWS S3 Bucket Versioning"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Security Control
  - Impact:Data Destruction
Reports:
  PCI:
    - 10.5.3
    - 10.5.5
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Low
Description: >
  Checks that object versioning is enabled in the S3 bucket.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-versioning-enabled
Reference: https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Versioning is not Enabled

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
VersioningeqEnabledexcludes:Versioning field:"Versioning" value:"Enabled"

Indicators

These rows show field, operator, and value matches.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-s3-bucket-versioning-enabled

AWS S3 Copy Object with Client-Side Encryption

#
Severity
medium
Compliance
Stratus Red Team aws.impact.s3-ransomware-client-side-encryption
Log types
AWS.CloudTrail
Tags
AWS, S3, CloudTrail, Ransomware, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This rule detects when objects are copied in an S3 bucket with client-side encryption. Such actions can be indicative of unauthorized data access or other suspicious activities.

MITRE ATT&CK coverage

TacticTechniques
ImpactNo specific technique

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "CopyObject"
        and event.deep_get("requestParameters", "x-amz-server-side-encryption-customer-algorithm")
        is not None
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"copied many objects on "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: AWS S3 Copy Object with Client-Side Encryption
Enabled: true
Filename: aws_s3_copy_object_with_client_side_encryption.py
RuleID: "AWS.S3.CopyObjectWithClientSideEncryption"
Severity: Medium
Threshold: 50
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - CloudTrail
  - Ransomware
  - Impact:Data Destruction
Reports:
  Stratus Red Team:
    - aws.impact.s3-ransomware-client-side-encryption
Description: >
  This rule detects when objects are copied in an S3 bucket with client-side encryption. Such actions can be indicative of unauthorized data access or other suspicious activities.
Runbook: |
  Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized copying of encrypted objects can lead to data exposure.
  Steps to investigate:
  1. Identify the user who performed the action.
  2. Verify if the action was authorized.
  3. Check for any other suspicious activities performed by the same user.
  4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/logging-with-cloudtrail.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is CopyObject
  • requestParameters.x-amz-server-side-encryption-customer-algorithm is present
Alert cadence
alerts after 50 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized copying of encrypted objects can lead to data exposure.

Steps to investigate:

1. Identify the user who performed the action.

2. Verify if the action was authorized.

3. Check for any other suspicious activities performed by the same user.

4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 0,
    "bytesTransferredOut": 0
  },
  "awsRegion": "us-east-1",
  "eventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "eventName": "CopyObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-10-01T12:34:56Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "EXAMPLE123456789",
  "requestParameters": {
    "bucketName": "example-bucket",
    "key": "example-object",
    "x-amz-server-side-encryption-customer-algorithm": "AES256"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::example-bucket/example-object",
      "type": "AWS::S3::Object"
    }
  ],
  "responseElements": null,
  "sharedEventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "aws-sdk-go/1.15.12 (go1.12.6; linux; amd64)",
  "userIdentity": {
    "accessKeyId": "EXAMPLEKEY",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/Alice",
    "principalId": "EXAMPLE",
    "type": "IAMUser",
    "userName": "Alice"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

AWS S3 Delete Object Detection

#
Severity
informational
Compliance
Stratus Red Team aws.impact.s3-ransomware-batch-deletion, aws.impact.s3-ransomware-client-side-encryption, aws.impact.s3-ransomware-individual-deletion
Log types
AWS.CloudTrail
Tags
AWS, S3, CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This rule detects when many objects are deleted from an S3 bucket. Such actions can be indicative of unauthorized data deletion or other suspicious activities.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "DeleteObject"
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"deleted many items from the "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="<UNKNOWN_BUCKET>"
    )
    return context

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: AWS S3 Delete Object Detection
Enabled: true
Filename: aws_s3_delete_object.py
RuleID: "AWS.S3.DeleteObject"
Threshold: 50
Severity: Info
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - CloudTrail
Reports:
  Stratus Red Team:
    - aws.impact.s3-ransomware-batch-deletion
    - aws.impact.s3-ransomware-client-side-encryption
    - aws.impact.s3-ransomware-individual-deletion
Description: >
  This rule detects when many objects are deleted from an S3 bucket. Such actions can be indicative of unauthorized data deletion or other suspicious activities.
Runbook: |
  Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized deletions can lead to data loss.
  Steps to investigate:
  1. Identify the user who performed the action.
  2. Verify if the action was authorized.
  3. Check for any other suspicious activities performed by the same user.
  4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/delete-objects.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is DeleteObject
Alert cadence
alerts after 50 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized deletions can lead to data loss.

Steps to investigate:

1. Identify the user who performed the action.

2. Verify if the action was authorized.

3. Check for any other suspicious activities performed by the same user.

4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 0,
    "bytesTransferredOut": 0
  },
  "awsRegion": "us-east-1",
  "eventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "eventName": "DeleteObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-10-01T12:34:56Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "EXAMPLE123456789",
  "requestParameters": {
    "bucketName": "example-bucket",
    "key": "example-object"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::example-bucket/example-object",
      "type": "AWS::S3::Object"
    }
  ],
  "responseElements": null,
  "sharedEventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "aws-sdk-go/1.15.12 (go1.12.6; linux; amd64)",
  "userIdentity": {
    "accessKeyId": "EXAMPLEKEY",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/Alice",
    "principalId": "EXAMPLE",
    "type": "IAMUser",
    "userName": "Alice"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

AWS S3 Delete Objects Detection

#
Severity
informational
Compliance
Stratus Red Team aws.impact.s3-ransomware-batch-deletion
Log types
AWS.CloudTrail
Tags
AWS, S3, CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This rule detects when multiple objects are deleted from an S3 bucket. Such actions can be indicative of unauthorized data deletion or other suspicious activities.

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") in ("DeleteObjects", "DeleteObjectVersion")
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"deleted many objects on "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="UNKNOWN_BUCKET"
    )
    return context

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: AWS S3 Delete Objects Detection
Enabled: true
Filename: aws_s3_delete_objects.py
RuleID: "AWS.S3.DeleteObjects"
Threshold: 1
Severity: Info
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - CloudTrail
Reports:
  Stratus Red Team:
    - aws.impact.s3-ransomware-batch-deletion
Description: >
  This rule detects when multiple objects are deleted from an S3 bucket. Such actions can be indicative of unauthorized data deletion or other suspicious activities.
Runbook: |
  Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized deletions can lead to data loss.
  Steps to investigate:
  1. Identify the user who performed the action.
  2. Verify if the action was authorized.
  3. Check for any other suspicious activities performed by the same user.
  4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/logging-with-cloudtrail.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is one of DeleteObjects, DeleteObjectVersion

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DeleteObjectVersion
  • DeleteObjects
field:"aws::eventName" kind:in
eventSourceeq
  • s3.amazonaws.com
field:"aws::eventSource" kind:eq value:"s3.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

Investigate the user and the actions performed on the S3 bucket to ensure they were authorized. Unauthorized deletions can lead to data loss.

Steps to investigate:

1. Identify the user who performed the action.

2. Verify if the action was authorized.

3. Check for any other suspicious activities performed by the same user.

4. If unauthorized, take necessary actions to secure the S3 bucket and prevent further unauthorized access.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 0,
    "bytesTransferredOut": 0
  },
  "awsRegion": "us-east-1",
  "eventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "eventName": "DeleteObjects",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-10-01T12:34:56Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "EXAMPLE123456789",
  "requestParameters": {
    "bucketName": "example-bucket",
    "key": "example-object"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::example-bucket/example-object",
      "type": "AWS::S3::Object"
    }
  ],
  "responseElements": null,
  "sharedEventID": "EXAMPLE-1234-5678-9012-EXAMPLE",
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "aws-sdk-go/1.15.12 (go1.12.6; linux; amd64)",
  "userIdentity": {
    "accessKeyId": "EXAMPLEKEY",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/Alice",
    "principalId": "EXAMPLE",
    "type": "IAMUser",
    "userName": "Alice"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

AWS S3 Insecure Access

#
Severity
low
Entities
aws_arns, ip_addresses
Log types
AWS.S3ServerAccess
Tags
AWS, Configuration Required, Security Control, Collection:Data From Cloud Storage Object
Reference
aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks if HTTP (unencrypted) was used to access objects in an S3 bucket, as opposed to HTTPS (encrypted).

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_aws_helpers import aws_rule_context
from panther_base_helpers import pattern_match


def rule(event):
    return pattern_match(event.get("operation", ""), "REST.*.OBJECT") and (
        not event.get("ciphersuite") or not event.get("tlsVersion")
    )


def title(event):
    return f"Insecure access to S3 Bucket [{event.get('bucket', '<UNKNOWN_BUCKET>')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_insecure_access.py
RuleID: "AWS.S3.ServerAccess.Insecure"
DisplayName: "AWS S3 Insecure Access"
DedupPeriodMinutes: 720 # 12 hours
Enabled: true
LogTypes:
  - AWS.S3ServerAccess
Tags:
  - AWS
  - Configuration Required
  - Security Control
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: >
  Checks if HTTP (unencrypted) was used to access objects in an S3 bucket, as opposed to HTTPS (encrypted).
Runbook: >
  Add a condition on the S3 bucket policy that denies access via http.
Reference: https://aws.amazon.com/premiumsupport/knowledge-center/s3-bucket-policy-for-config-rule/
SummaryAttributes:
  - bucket
  - key
  - operation
  - userAgent
  - remoteip
  - requester
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.S3ServerAccess events when all of the conditions below hold.

Condition

  • operation matches the pattern REST.*.OBJECT
  • any of:
    • ciphersuite is empty
    • tlsVersion is empty
Alert deduplication
repeat matches within 12h group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ciphersuiteis_null
  • (no value, null check)
field:"ciphersuite" kind:is_null
operationwildcard
  • REST.*.OBJECT
field:"operation" kind:wildcard value:"REST.*.OBJECT"
tlsVersionis_null
  • (no value, null check)
field:"tlsVersion" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucket

Response runbook

Add a condition on the S3 bucket policy that denies access via http.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "authenticationtype": "AuthHeader",
  "bucket": "cloudtrail",
  "bucketowner": "f16a9e81a6589df1c902c86f7982fd14a88787db",
  "hostheader": "cloudtrail.s3.us-east-1.amazonaws.com",
  "hostid": "neRpT/AXRsS3LMBqq/wND59opwPRWWKn7F6evEhdbS99me5fyIXpVI/MMIn6ECgU1YZAqwuF8Bw=",
  "httpstatus": 200,
  "key": "AWSLogs/o-wwwwwwgggg/234567890123/CloudTrail-Digest/ca-central-1/2020/02/14/234567890123_CloudTrail-Digest_ca-central-1_POrgTrail_us-east-1_20200214T001007Z.json.gz",
  "objectsize": 747,
  "operation": "REST.PUT.OBJECT",
  "p_any_aws_arns": [
    "arn:aws:sts::123456789012:assumed-role/eagle/regionalDeliverySession"
  ],
  "p_any_ip_addresses": [
    "55.99.86.234"
  ],
  "p_event_time": "2020-02-14 00:53:48.000000000",
  "p_log_type": "AWS.S3ServerAccess",
  "p_row_id": "8855aa99ff77abc8dcb0e36e0a",
  "remoteip": "127.0.0.1",
  "requester": "arn:aws:sts::123456789012:assumed-role/eagle/regionalDeliverySession",
  "requestid": "101B7403B9828743",
  "requesturi": "PUT /AWSLogs/o-wwwwwwgggg/234567890123/CloudTrail-Digest/ca-central-1/2020/02/14/234567890123_CloudTrail-Digest_ca-central-1_POrgTrail_us-east-1_20200214T001007Z.json.gz HTTP/1.1",
  "signatureversion": "SigV4",
  "time": "2020-02-14 00:53:48.000000000",
  "totaltime": 110,
  "turnaroundtime": 20,
  "useragent": "aws-internal/3 aws-sdk-java/1.11.714 Linux/4.9.184-0.1.ac.235.83.329.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.242-b08 java/1.8.0_242 vendor/Oracle_Corporation"
}

AWS S3 Large Download

#
Severity
informational
Tags
Beta, Data Exfiltration
Source
github.com/panther-labs/panther-analysis

Detects when a user (IAM User, AssumedRole, or FederatedUser) downloads more than the configured threshold of data from S3 buckets within a time window. Configurable thresholds and bucket filtering allow customization for different organizational needs. This may indicate unauthorized data exfiltration or bulk data downloads for analysis.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(_) -> bool:
    """Always return True since the query already filtered for violations"""
    return True


def title(event) -> str:
    user_arn = event.get("user_arn", "unknown")
    bucket_name = event.get("bucket_name", "unknown")
    total_mb = round(event.get("total_bytes_downloaded", 0) / (1024 * 1024), 2)

    return f"Large S3 download detected: {user_arn} downloaded {total_mb}MB from {bucket_name}"


def alert_context(event) -> dict:
    total_bytes = event.get("total_bytes_downloaded", 0)
    total_mb = round(total_bytes / (1024 * 1024), 2)

    return {
        "user_arn": event.get("user_arn"),
        "user_name": event.get("user_name"),
        "bucket_name": event.get("bucket_name"),
        "source_ip": event.get("source_ip"),
        "user_agent": event.get("user_agent"),
        "total_bytes_downloaded": total_bytes,
        "total_mb_downloaded": total_mb,
        "object_count": event.get("object_count"),
        "first_download_time": event.get("first_download_time"),
        "last_download_time": event.get("last_download_time"),
        "sample_objects": event.get("sample_objects", [])[:10],  # Show first 10 objects
    }

Rule specification

AnalysisType: scheduled_rule
Filename: aws_s3_large_download_specific_bucket.py
RuleID: "AWS.S3.LargeDownload"
DisplayName: "AWS S3 Large Download"
Enabled: false
CreateAlert: false
ScheduledQueries:
  - AWS S3 Large Download
Severity: Info
Tags:
  - Beta
  - Data Exfiltration
Reports:
  MITRE ATT&CK:
    - "TA0010:T1537"  # Exfiltration: Transfer Data to Cloud Account
Description: >
  Detects when a user (IAM User, AssumedRole, or FederatedUser) downloads more than the 
  configured threshold of data from S3 buckets within a time window. Configurable thresholds 
  and bucket filtering allow customization for different organizational needs. This may 
  indicate unauthorized data exfiltration or bulk data downloads for analysis.
DedupPeriodMinutes: 60
Runbook: |
  1. **Immediate Actions:**
     - Verify if the user activity is authorized
     - Check if the downloads were for legitimate business purposes
     - Consider temporarily restricting bucket access if suspicious
  
  2. **Investigation Steps:**
     - Review the user's recent authentication events
     - Check for any privilege escalation or credential compromise
     - Examine the downloaded objects to determine sensitivity
     - Review source IP and user agent for signs of automation
     - Check for other suspicious activities by the same user
  
  3. **Containment:**
     - If unauthorized: revoke user credentials immediately
     - Apply temporary S3 bucket policies to restrict access
     - Enable S3 MFA delete and additional monitoring
  
  4. **Recovery:**
     - Document the scope of data accessed
     - Review S3 access logs for complete activity timeline
     - Consider rotating any sensitive data that may have been accessed
  
  5. **Prevention:**
     - Implement S3 access monitoring and alerting
     - Review IAM policies for least privilege
     - Consider S3 Access Points for controlled access
     - Enable GuardDuty for enhanced threat detection

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query AWS S3 Large Download; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_arn
user_name
bucket_name
source_ip
user_agent
total_bytes_downloaded
object_count
first_download_time
last_download_time

Response runbook

1. Immediate Actions:

- Verify if the user activity is authorized

- Check if the downloads were for legitimate business purposes

- Consider temporarily restricting bucket access if suspicious

2. Investigation Steps:

- Review the user's recent authentication events

- Check for any privilege escalation or credential compromise

- Examine the downloaded objects to determine sensitivity

- Review source IP and user agent for signs of automation

- Check for other suspicious activities by the same user

3. Containment:

- If unauthorized: revoke user credentials immediately

- Apply temporary S3 bucket policies to restrict access

- Enable S3 MFA delete and additional monitoring

4. Recovery:

- Document the scope of data accessed

- Review S3 access logs for complete activity timeline

- Consider rotating any sensitive data that may have been accessed

5. Prevention:

- Implement S3 access monitoring and alerting

- Review IAM policies for least privilege

- Consider S3 Access Points for controlled access

- Enable GuardDuty for enhanced threat detection

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "account_id": "111111111111",
  "bucket_name": "sensitive-data-bucket",
  "first_download_time": "2025-01-15 14:30:00",
  "last_download_time": "2025-01-15 14:34:30",
  "object_count": 75,
  "sample_objects": [
    "logs/2025/01/15/file1.log",
    "logs/2025/01/15/file2.log"
  ],
  "source_ip": "3.3.3.3",
  "total_bytes_downloaded": 78643200,
  "user_agent": "aws-cli/2.0.0",
  "user_arn": "arn:aws:iam::111111111111:user/data-engineer",
  "user_name": "data-engineer",
  "user_type": "IAMUser"
}

AWS S3 Large Download

#
Tags
Beta, Data Exfiltration
Source
github.com/panther-labs/panther-analysis

Returns S3 GetObject events where a user has downloaded more than the configured threshold of data within the specified time window. Supports filtering by bucket patterns and user types.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "AWS S3 Large Download"
Enabled: false
Description: >
  Returns S3 GetObject events where a user has downloaded more than the configured threshold 
  of data within the specified time window. Supports filtering by bucket patterns and user types.
Tags:
  - Beta
  - Data Exfiltration
SnowflakeQuery: |-
  SELECT
    userIdentity:arn as user_arn,
    COALESCE(userIdentity:userName, userIdentity:sessionContext:sessionIssuer:userName, 'unknown') as user_name,
    userIdentity:type as user_type,
    requestParameters:bucketName as bucket_name,
    sourceIPAddress as source_ip,
    userAgent as user_agent,
    SUM(COALESCE(additionalEventData:bytesTransferredOut::int, 0)) as total_bytes_downloaded,
    COUNT(*) as object_count,
    MIN(p_event_time) as first_download_time,
    MAX(p_event_time) as last_download_time,
    ARRAY_AGG(DISTINCT requestParameters:key) as sample_objects
  FROM panther_logs.public.aws_cloudtrail
  WHERE eventName = 'GetObject'
    AND eventSource = 's3.amazonaws.com'
    AND userIdentity:type IN ('IAMUser', 'AssumedRole', 'FederatedUser')
    AND errorCode IS NULL
    AND p_event_time >= DATEADD(minute, -10, CURRENT_TIMESTAMP())
    AND userIdentity:arn NOT LIKE '%panther-snowflake-api%'
    -- AND (requestParameters:bucketName LIKE '%sensitive%' OR requestParameters:bucketName LIKE '%prod%')
  GROUP BY
    userIdentity:arn,
    COALESCE(userIdentity:userName, userIdentity:sessionContext:sessionIssuer:userName, 'unknown'),
    userIdentity:type,
    requestParameters:bucketName,
    sourceIPAddress,
    userAgent
  HAVING SUM(COALESCE(additionalEventData:bytesTransferredOut::int, 0)) >= 52428800  -- 50MB default
  ORDER BY total_bytes_downloaded DESC

DatabricksQuery: |-
  SELECT
    userIdentity:arn as user_arn,
    COALESCE(userIdentity:userName, userIdentity:sessionContext:sessionIssuer:userName, 'unknown') as user_name,
    userIdentity:type as user_type,
    requestParameters:bucketName as bucket_name,
    sourceIPAddress as source_ip,
    userAgent as user_agent,
    SUM(COALESCE(CAST(additionalEventData:bytesTransferredOut::int AS BIGINT), 0)) as total_bytes_downloaded,
    COUNT(*) as object_count,
    MIN(p_event_time) as first_download_time,
    MAX(p_event_time) as last_download_time,
    COLLECT_SET(requestParameters:key) as sample_objects
  FROM panther_logs.aws_cloudtrail
  WHERE eventName = 'GetObject'
    AND eventSource = 's3.amazonaws.com'
    AND userIdentity:type IN ('IAMUser', 'AssumedRole', 'FederatedUser')
    AND errorCode IS NULL
    AND p_event_time >= CURRENT_TIMESTAMP() - INTERVAL 10 MINUTES
    AND userIdentity:arn NOT LIKE '%panther-snowflake-api%'
    -- AND (requestParameters:bucketName LIKE '%sensitive%' OR requestParameters:bucketName LIKE '%prod%')
  GROUP BY
    userIdentity:arn,
    COALESCE(userIdentity:userName, userIdentity:sessionContext:sessionIssuer:userName, 'unknown'),
    userIdentity:type,
    requestParameters:bucketName,
    sourceIPAddress,
    userAgent
  HAVING SUM(COALESCE(CAST(additionalEventData:bytesTransferredOut::int AS BIGINT), 0)) >= 52428800  -- 50MB default
  ORDER BY total_bytes_downloaded DESC
Schedule:
  RateMinutes: 10
  TimeoutMinutes: 3

Stages and Predicates

Stage 1: source

Table
panther_logs.public.aws_cloudtrail

Stage 2: filter

  • eventName is GetObject
  • eventSource is s3.amazonaws.com
  • userIdentity:type is one of IAMUser, AssumedRole, FederatedUser
  • errorCode is empty
  • userIdentity:arn does not match the pattern *panther-snowflake-api*
Grouped by
userIdentity:arn, COALESCE, (userIdentity:userName , userIdentity:sessionContext:sessionIssuer:userName , 'unknown', userIdentity:type, requestParameters:bucketName, sourceIPAddress, userAgent

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Stage 3: having

Threshold
ge 52428800

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity:arnmatchpanther-snowflake-apiexcludes:userIdentity:arn field:"userIdentity:arn" value:"panther-snowflake-api"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_arnuserIdentity:arn
user_nameCOALESCE ( userIdentity:userName , userIdentity:sessionContext:sessionIssuer:userName , 'unknown' )
user_typeuserIdentity:type
bucket_namerequestParameters:bucketName
source_ipsourceIPAddress
user_agentuserAgent
total_bytes_downloadedSUM ( COALESCE ( additionalEventData:bytesTransferredOut :: int , 0 ) )
object_countCOUNT ( * )
first_download_timeMIN ( p_event_time )
last_download_timeMAX ( p_event_time )
sample_objectsARRAY_AGG ( DISTINCT requestParameters:key )

AWS S3 Object Copied to External Account Bucket

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration:Transfer Data to Cloud Account, Impact:Data Encrypted for Impact
Reference
www.trendmicro.com
Source
github.com/panther-labs/panther-analysis

Detects when an S3 object is copied from one bucket to another bucket in a different AWS account. This could indicate data exfiltration or a ransomware attack where data is copied to an attacker-controlled account.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def extract_resources(event):
    resources = event.get("resources", [])
    bucket_accounts = {}

    if len(resources) > 0:
        for resource in resources:
            if resource.get("type") == "AWS::S3::Bucket":
                bucket_name = resource.get("arn", "").split(":::")[-1]
                account_id = resource.get("accountId", "")
                bucket_accounts[bucket_name] = account_id
    return bucket_accounts


def rule(event):
    if event.get("eventName") != "CopyObject" or not aws_cloudtrail_success(event):
        return False

    bucket_accounts = extract_resources(event)

    # Need at least 2 buckets to compare accounts
    if len(bucket_accounts) < 2:
        return False

    # Check if buckets belong to different accounts
    account_ids = set(bucket_accounts.values())
    if len(account_ids) > 1:
        return True

    return False


def title(event):
    dest_bucket = event.deep_get(
        "requestParameters", "bucketName", default="<UNKNOWN_DESTINATION_BUCKET>"
    )
    source_bucket = event.deep_get(
        "requestParameters", "x-amz-copy-source", default="<UNKNOWN_SOURCE_BUCKET>"
    )
    actor = event.udm("actor_user")

    return (
        f"[AWS.CloudTrail] User [{actor}] copied objects to external AWS account "
        f"bucket [{dest_bucket}] from bucket [{source_bucket}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucket_accounts"] = extract_resources(event)
    context["dest_bucket"] = event.deep_get(
        "requestParameters", "bucketName", default="<UNKNOWN_DESTINATION_BUCKET>"
    )
    # Extract just the bucket name from x-amz-copy-source (format: bucket/key)
    source_path = event.deep_get(
        "requestParameters", "x-amz-copy-source", default="<UNKNOWN_SOURCE_BUCKET>"
    )
    context["bucketName"] = source_path.split("/")[0] if "/" in source_path else source_path
    return context

Rule specification

AnalysisType: rule
Filename: aws_s3_copy_object_to_external_account_bucket.py
RuleID: "AWS.S3.CopyObjectToExternalAccountBucket"
DisplayName: "AWS S3 Object Copied to External Account Bucket"
Enabled: true
CreateAlert: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration:Transfer Data to Cloud Account
  - Impact:Data Encrypted for Impact
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
    - TA0040:T1486
Severity: Medium
DedupPeriodMinutes: 60
Description: >
  Detects when an S3 object is copied from one bucket to another bucket in a different AWS account.
  This could indicate data exfiltration or a ransomware attack where data is copied to an attacker-controlled account.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after the alert to establish normal data access patterns
  2. Check if the destination account ID appears in any legitimate cross-account S3 operations in the past 90 days
  3. Find all CopyObject events to the same destination bucket from any user in the past 7 days to identify if this is part of a broader exfiltration campaign
Reference: https://www.trendmicro.com/en_us/research/25/k/s3-ransomware.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is CopyObject
  • errorCode is empty
  • errorMessage is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneCopyObjectexcludes:eventName field:"eventName" value:"CopyObject"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName
x-amz-copy-sourcerequestParameters.x-amz-copy-source

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after the alert to establish normal data access patterns

2. Check if the destination account ID appears in any legitimate cross-account S3 operations in the past 90 days

3. Find all CopyObject events to the same destination bucket from any user in the past 7 days to identify if this is part of a broader exfiltration campaign

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-222222222222",
  "eventName": "CopyObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T12:00:00Z",
  "eventType": "AwsApiCall",
  "recipientAccountId": "12948575929274",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "bucketName": "attacker-exfil-1764604156",
    "key": "customer-database.csv",
    "x-amz-copy-source": "ransomware-test-victim-1764604156/customer-database.csv"
  },
  "resources": [
    {
      "accountId": "111111111111",
      "arn": "arn:aws:s3:::sample-bucket-magical-ardinghelli",
      "type": "AWS::S3::Bucket"
    },
    {
      "arn": "arn:aws:s3:::sample-bucket-magical-ardinghelli/customer-database.csv",
      "type": "AWS::S3::Object"
    },
    {
      "accountId": "12948575929274",
      "arn": "arn:aws:s3:::sample-bucket-busy-carver",
      "type": "AWS::S3::Bucket"
    },
    {
      "arn": "arn:aws:s3:::sample-bucket-busy-carver/customer-database.csv",
      "type": "AWS::S3::Object"
    }
  ],
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "AKIA-MOCKACCESSKEYID-1",
    "accountId": "12948575929274",
    "arn": "arn:aws:sts::12948575929274:assumed-role/sample-role-keen-cohen/sample-role-hardcore-driscoll-role-brave-yalow-role-intelligent-brahmagupta-role-admiring-vaughan-role-sad-khayyam-role-eager-haslett",
    "principalId": "AIDAI1234567890EXAMPLE:user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "12948575929274",
        "arn": "arn:aws:iam::12948575929274:role/sample-role-keen-cohen",
        "principalId": "AIDAI1234567890EXAMPLE",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS S3 Object Exfiltration WITH Object Deletion

#
Severity
high
Time window
30h
Match by
p_alert_context.bucketName
Tags
AWS, Exfiltration:Transfer Data to Cloud Account, Impact:Data Encrypted for Impact
Reference
www.trendmicro.com
Source
github.com/panther-labs/panther-analysis

Detects a ransomware attack pattern where an attacker with compromised AWS credentials exfiltrates data from an S3 bucket to an external AWS account, followed by bulk deletion of objects from the source bucket within a short timeframe. This technique was notably used by the threat actor Bling Libra to extort victims by threatening data destruction or leaks.

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.S3.ObjectExfiltration.WITH.ObjectDeletion"
DisplayName: "AWS S3 Object Exfiltration WITH Object Deletion"
Enabled: false
Severity: High
Description: >
  Detects a ransomware attack pattern where an attacker with compromised AWS credentials exfiltrates data from an S3 bucket
  to an external AWS account, followed by bulk deletion of objects from the source bucket within a short timeframe.
  This technique was notably used by the threat actor Bling Libra to extort victims by threatening data destruction or leaks.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after the alert to establish the full scope of bucket access
  2. Verify if the destination account ID in the CopyObject events appears in any legitimate cross-account S3 operations in the past 90 days
  3. Check if the source IP addresses for both exfiltration and deletion events match known VPN endpoints, cloud provider ranges, or previously seen IPs for this user
  4. Find all other CopyObject and DeleteObject events to the same or other buckets from any user in the past 7 days to identify if this is part of a broader campaign 
Reference: https://www.trendmicro.com/en_us/research/25/k/s3-ransomware.html
Tags:
  - AWS
  - Exfiltration:Transfer Data to Cloud Account
  - Impact:Data Encrypted for Impact
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
    - TA0040:T1486
Detection:
    - Group:
        - ID: Bulk Exfiltration
          RuleID: AWS.S3.CopyObjectToExternalAccountBucket
          MinMatchCount: 10
        - ID: Bulk Deletion
          RuleID: AWS.S3.DeleteObject
          MinMatchCount: 10
      MatchCriteria:
        field_name:
          - GroupID: Bulk Exfiltration
            Match: p_alert_context.bucketName
          - GroupID: Bulk Deletion
            Match: p_alert_context.bucketName
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 10
      LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.bucketName. Each step needs one match unless a higher minimum is shown.

Stage 1: step Bulk Exfiltration

References detection AWS S3 Object Copied to External Account Bucket (min 10 matches).

Stage 2: step Bulk Deletion

References detection AWS S3 Delete Object Detection (min 10 matches).

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after the alert to establish the full scope of bucket access

2. Verify if the destination account ID in the CopyObject events appears in any legitimate cross-account S3 operations in the past 90 days

3. Check if the source IP addresses for both exfiltration and deletion events match known VPN endpoints, cloud provider ranges, or previously seen IPs for this user

4. Find all other CopyObject and DeleteObject events to the same or other buckets from any user in the past 7 days to identify if this is part of a broader campaign

AWS S3 Ransomware Note Upload Detection

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, S3, CloudTrail, Ransomware, DataSecurity
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This rule detects when files with names commonly associated with ransomware notes are uploaded to S3 buckets. Ransomware attackers often drop ransom notes with distinctive filenames like HOW_TO_DECRYPT_FILES.txt, RANSOM_NOTE.txt, FILES_ENCRYPTED.html, or similar patterns to inform victims about the encryption and provide payment instructions.

Detection logic

import re

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# Common ransomware note filename patterns
RANSOM_NOTE_PATTERNS = [
    # Explicit ransomware-related terms
    # RANSOM_NOTE.txt, PAYMENT_INFO.html
    r"(?i)(ransom|payment)[_-]?(note|info|instructions?).*\.(txt|html?)$",
    # Decrypt/restore with specific action words
    # HOW_TO_DECRYPT_FILES.txt
    r"(?i)how[_-]?to[_-]?(decrypt|restore|recover)[_-]?(your[_-]?)?files.*\.(txt|html?)$",
    # DECRYPT_INSTRUCTIONS.txt
    r"(?i)decrypt[_-]?(instructions?|guide|info|your[_-]?files).*\.(txt|html?)$",
    # RESTORE_INSTRUCTIONS.txt
    r"(?i)restore[_-]?(instructions?|guide|info|your[_-]?files).*\.(txt|html?)$",
    # RECOVERY_INSTRUCTIONS.txt
    r"(?i)recovery[_-]?(instructions?|key|guide).*\.(txt|html?)$",
    # Files encrypted/locked messages
    # FILES_ENCRYPTED.txt, ALL_FILES_HAVE_BEEN_ENCRYPTED.txt
    r"(?i)(all[_-]?)?files?[_-]?(have[_-]?been[_-]?)?(encrypted|locked).*\.(txt|html?)$",
    # YOUR_FILES_ARE_ENCRYPTED.txt
    r"(?i)your[_-]?files?[_-]?(are|have[_-]?been)[_-]?(encrypted|locked).*\.(txt|html?)$",
    # DATA_ENCRYPTED.txt
    r"(?i)data[_-]?(has[_-]?been[_-]?)?(encrypted|locked).*\.(txt|html?)$",
    # Unlock-related (common in ransomware)
    # UNLOCK_INSTRUCTIONS.txt
    r"(?i)unlock[_-]?(instructions?|guide|your[_-]?files).*\.(txt|html?)$",
    # Help decrypt/restore (specific to ransomware)
    # HELP_DECRYPT_YOUR_FILES.txt
    r"(?i)help[_-]?(restore|decrypt|recover)[_-]?(your[_-]?)?files.*\.(txt|html?)$",
]

COMPILED_PATTERNS = [re.compile(pattern) for pattern in RANSOM_NOTE_PATTERNS]


def extract_filename(event):
    key = event.deep_get("requestParameters", "key", default="")
    if not key:
        resources = event.get("resources", [])
        for resource in resources:
            if resource.get("type") == "AWS::S3::Object":
                arn = resource.get("arn", "")
                # Extract key from ARN (format: arn:aws:s3:::bucket/key)
                if "/" in arn:
                    key = arn.split("/", 1)[1]
                    break

    filename = key.split("/")[-1] if "/" in key else key
    return filename


def rule(event):

    if event.get("eventName") != "PutObject" or not aws_cloudtrail_success(event):
        return False

    filename = extract_filename(event)

    # Check if filename matches any ransomware note pattern
    return any(pattern.match(filename) for pattern in COMPILED_PATTERNS)


def title(event):
    bucket = event.deep_get("requestParameters", "bucketName", default="<UNKNOWN_BUCKET>")
    filename = extract_filename(event)

    return (
        f"[AWS.CloudTrail] Potential ransomware note uploaded to S3: "
        f"[{filename}] in bucket [{bucket}] by user [{event.udm('actor_user')}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    key = event.deep_get("requestParameters", "key", default="<UNKNOWN_KEY>")
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="<UNKNOWN_BUCKET>"
    )
    context["objectKey"] = key
    context["filename"] = extract_filename(event)
    return context

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: AWS S3 Ransomware Note Upload Detection
Enabled: true
Filename: aws_s3_ransomware_note_upload.py
RuleID: "AWS.S3.RansomwareNoteUpload"
Severity: Medium
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - CloudTrail
  - Ransomware
  - DataSecurity
Description: >
  This rule detects when files with names commonly associated with ransomware notes are uploaded to S3 buckets.
  Ransomware attackers often drop ransom notes with distinctive filenames like HOW_TO_DECRYPT_FILES.txt,
  RANSOM_NOTE.txt, FILES_ENCRYPTED.html, or similar patterns to inform victims about the encryption and
  provide payment instructions.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after this alert, focusing on DeleteObject, DeleteObjects, PutBucketEncryption, DeleteBucketEncryption, and PutBucketVersioning events from the same requestParameters:bucketName
  2. Check if the sourceIPAddress has accessed this S3 bucket in the past 90 days and compare the volume of operations to establish if this is anomalous activity
  3. Find other alerts with this rule ID or any S3 deletion/encryption rules for the same bucket or user in the past 7 days to identify if this is part of a coordinated ransomware attack pattern
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is PutObject
  • errorCode is empty
  • errorMessage is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNamenePutObjectexcludes:eventName field:"eventName" value:"PutObject"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucketNamerequestParameters.bucketName
actor_user

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn in the 24 hours before and after this alert, focusing on DeleteObject, DeleteObjects, PutBucketEncryption, DeleteBucketEncryption, and PutBucketVersioning events from the same requestParameters:bucketName

2. Check if the sourceIPAddress has accessed this S3 bucket in the past 90 days and compare the volume of operations to establish if this is anomalous activity

3. Find other alerts with this rule ID or any S3 deletion/encryption rules for the same bucket or user in the past 7 days to identify if this is part of a coordinated ransomware attack pattern

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "CipherSuite": "TLS_AES_128_GCM_SHA256",
    "SSEApplied": "SSE_S3",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 2048,
    "bytesTransferredOut": 0
  },
  "awsRegion": "us-west-2",
  "eventID": "z9y8x7w6-5432-10fe-dcba-EXAMPLE22222",
  "eventName": "PutObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2025-12-03T18:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "EXAMPLE987654321",
  "requestParameters": {
    "Host": "sample-bucket-hungry-buck.s3.us-west-2.amazonaws.com",
    "bucketName": "production-data",
    "key": "documents/financial/HOW_TO_DECRYPT_FILES.txt"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::sample-bucket-hungry-buck",
      "accountId": "111111111111",
      "type": "AWS::S3::Bucket"
    },
    {
      "ARN": "arn:aws:s3:::sample-bucket-hungry-buck/documents/financial/HOW_TO_DECRYPT_FILES.txt",
      "type": "AWS::S3::Object"
    }
  ],
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-sdk-python/1.26.0",
  "userIdentity": {
    "accessKeyId": "AKIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:iam::111111111111:user/compromised-user",
    "principalId": "AIDA-MOCKIAMUSERID-1",
    "type": "IAMUser",
    "userName": "compromised-user"
  }
}

AWS S3 Security Control Disabling

#
Status
Experimental
Severity
high
Group by
requestParameters.bucketName, userIdentity.arn
Log types
AWS.CloudTrail
Tags
AWS, S3, Ransomware
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects the disabling of 2 or more distinct S3 security controls (logging, versioning, and MFA delete protection) on the same bucket by the same actor within a short timeframe. Threshold is set to 2 rather than 3 because versioning and MFA delete can be disabled together in a single PutBucketVersioning API call, which produces only one unique value — making a threshold of 3 structurally unreachable in that scenario. This pattern is a strong indicator of preparation for ransomware or data destruction attacks, as attackers typically disable recovery mechanisms before encrypting or deleting data.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not aws_cloudtrail_success(event):
        return False
    if event.get("eventSource") != "s3.amazonaws.com":
        return False

    event_name = event.get("eventName")

    if event_name == "PutBucketLogging":
        return event.deep_get("requestParameters", "logging") == ""

    if event_name == "PutBucketVersioning":
        status = event.deep_get(
            "requestParameters", "VersioningConfiguration", "Status", default=""
        )
        mfa_delete = event.deep_get(
            "requestParameters", "VersioningConfiguration", "MfaDelete", default=""
        )
        return status in ("Suspended", "Disabled") or mfa_delete == "Disabled"

    return False


def unique(event):
    if event.get("eventName") == "PutBucketLogging":
        return "logging_disabled"

    # PutBucketVersioning — both versioning and MFA delete can be disabled in a single API call.
    # When that happens this event contributes only one unique value, capping the maximum
    # distinct values at 2 (this + logging_disabled). Threshold is set to 2 so the rule
    # fires correctly even when all three controls are disabled via just two API calls.
    status = event.deep_get("requestParameters", "VersioningConfiguration", "Status", default="")
    mfa_delete = event.deep_get(
        "requestParameters", "VersioningConfiguration", "MfaDelete", default=""
    )
    versioning_disabled = status in ("Suspended", "Disabled")
    mfa_disabled = mfa_delete == "Disabled"

    if versioning_disabled and mfa_disabled:
        return "versioning_and_mfa_disabled"
    if versioning_disabled:
        return "versioning_suspended"
    return "mfa_delete_disabled"


def dedup(event):
    bucket = event.deep_get("requestParameters", "bucketName", default="UNKNOWN_BUCKET")
    actor = event.deep_get("userIdentity", "arn", default="UNKNOWN_ACTOR")
    return f"{bucket}:{actor}"


def title(event):
    bucket = event.deep_get("requestParameters", "bucketName", default="UNKNOWN_BUCKET")
    actor = event.udm("actor_user")
    return f"[AWS.S3] Multiple security controls disabled on bucket [{bucket}] by [{actor}]"


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="UNKNOWN_BUCKET"
    )
    return context

Rule specification

AnalysisType: rule
RuleID: "AWS.S3.SecurityControlDisabling"
DisplayName: "AWS S3 Security Control Disabling"
Filename: aws_s3_security_control_disabling.py
Enabled: true
Status: Experimental
LogTypes:
  - AWS.CloudTrail
Severity: High
Threshold: 2
DedupPeriodMinutes: 90
Description: >
  Detects the disabling of 2 or more distinct S3 security controls (logging, versioning, and MFA
  delete protection) on the same bucket by the same actor within a short timeframe. Threshold is
  set to 2 rather than 3 because versioning and MFA delete can be disabled together in a single
  PutBucketVersioning API call, which produces only one unique value — making a threshold of 3
  structurally unreachable in that scenario. This pattern is a strong indicator of preparation for
  ransomware or data destruction attacks, as attackers typically disable recovery mechanisms before
  encrypting or deleting data.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the actor ARN on the affected bucket in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern
  2. Check for subsequent suspicious activities on the same bucket including DeleteObject, DeleteObjects, PutBucketEncryption, or GetObject events in the 6 hours after all three security controls were disabled
  3. Find all other S3 buckets where this actor ARN has disabled logging, versioning, or MFA delete in the past 7 days to determine if this is a widespread attack
Reference: https://rhinosecuritylabs.com/aws/s3-ransomware-part-1-attack-vector/
Tags:
  - AWS
  - S3
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0005:T1562 # Defense Evasion: Impair Defenses
    - TA0040:T1485 # Impact: Data Destruction

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • any of:
    • all of:
      • eventName is PutBucketLogging
      • requestParameters.logging is ""
    • all of:
      • eventName is not PutBucketLogging
      • eventName is PutBucketVersioning
      • any of:
        • requestParameters.VersioningConfiguration.Status is one of Suspended, Disabled
        • requestParameters.VersioningConfiguration.MfaDelete is Disabled
Alert cadence
alerts after 2 matches within 90m

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucketNamerequestParameters.bucketName
actor_user

Response runbook

1. Query CloudTrail for all S3 API calls by the actor ARN on the affected bucket in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern

2. Check for subsequent suspicious activities on the same bucket including DeleteObject, DeleteObjects, PutBucketEncryption, or GetObject events in the 6 hours after all three security controls were disabled

3. Find all other S3 buckets where this actor ARN has disabled logging, versioning, or MFA delete in the past 7 days to determine if this is a widespread attack

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "PutBucketLogging",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.11",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "BucketLoggingStatus": {
      "xmlns": "http://s3.amazonaws.com/doc/2006-03-01/"
    },
    "bucketName": "critical-data-bucket",
    "logging": ""
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/user_name",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS S3 Security Controls Disabled

#
Status
Deprecated
Severity
high
Time window
30h
Match by
p_alert_context.bucketName
Tags
AWS, S3, Ransomware
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects the disabling of multiple S3 security controls (logging, versioning and MFA delete protection) on the same bucket within a short timeframe. This pattern is a strong indicator of preparation for ransomware or data destruction attacks, as attackers typically disable recovery mechanisms before encrypting or deleting data. Alerting on this activity enables early intervention before actual data loss occurs.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.S3.Disable.Security.Controls"
DisplayName: "AWS S3 Security Controls Disabled"
Enabled: false
Status: Deprecated
Severity: High
Description: >
  Detects the disabling of multiple S3 security controls (logging, versioning and MFA delete protection)
  on the same bucket within a short timeframe. This pattern is a strong indicator of preparation
  for ransomware or data destruction attacks, as attackers typically disable recovery mechanisms
  before encrypting or deleting data. Alerting on this activity enables early intervention before
  actual data loss occurs.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the actor ARN on the affected bucket in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern
  2. Check for subsequent suspicious activities on the same bucket including DeleteObject, DeleteObjects, PutBucketEncryption, or GetObject events in the 6 hours after all three security controls were disabled
  3. Find all other S3 buckets where this actor ARN has disabled logging, versioning, or MFA delete in the past 7 days to determine if this is a widespread attack
Reference: https://rhinosecuritylabs.com/aws/s3-ransomware-part-1-attack-vector/
Tags:
  - AWS
  - S3
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0005:T1562 # Defense Evasion: Impair Defenses
Detection:
  - Group:
      - ID: Disable S3 Logging
        RuleID: AWS.S3.DisableBucketLogging
      - ID: Versioning Suspended
        RuleID: AWS.S3.SuspendVersioning
      - ID: MFA Delete Disabled
        RuleID: AWS.S3.DisableMfaDelete
    MatchCriteria:
      field_name:
        - GroupID: Disable S3 Logging
          Match: p_alert_context.bucketName
        - GroupID: Versioning Suspended
          Match: p_alert_context.bucketName
        - GroupID: MFA Delete Disabled
          Match: p_alert_context.bucketName
    LookbackWindowMinutes: 1800
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 10

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.bucketName. Each step needs one match unless a higher minimum is shown.

Stage 1: step Disable S3 Logging

References detection S3 Bucket Logging Disabled.

Stage 2: step Versioning Suspended

References detection S3 Bucket Versioning Suspended.

Stage 3: step MFA Delete Disabled

References detection S3 MFA Delete Disabled.

Response runbook

1. Query CloudTrail for all S3 API calls by the actor ARN on the affected bucket in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern

2. Check for subsequent suspicious activities on the same bucket including DeleteObject, DeleteObjects, PutBucketEncryption, or GetObject events in the 6 hours after all three security controls were disabled

3. Find all other S3 buckets where this actor ARN has disabled logging, versioning, or MFA delete in the past 7 days to determine if this is a widespread attack

AWS S3 Unauthenticated Access

#
Severity
low
Log types
AWS.S3ServerAccess
Tags
AWS, Configuration Required, Security Control, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Checks for S3 access attempts where the requester is not an authenticated AWS user.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from panther_aws_helpers import aws_rule_context

# A list of buckets where authenticated access is expected
AUTH_BUCKETS = {"example-bucket"}


def rule(event):
    return event.get("bucket") in AUTH_BUCKETS and not event.get("requester")


def title(event):
    return f"Unauthenticated access to S3 Bucket [{event.get('bucket', '<UNKNOWN_BUCKET')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_unauthenticated_access.py
RuleID: "AWS.S3.ServerAccess.Unauthenticated"
DisplayName: "AWS S3 Unauthenticated Access"
Enabled: false
LogTypes:
  - AWS.S3ServerAccess
Tags:
  - AWS
  - Configuration Required
  - Security Control
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: >
  Checks for S3 access attempts where the requester is not an authenticated AWS user.
Runbook: >
  If unauthenticated S3 access is not expected for this bucket, update its access policies.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-auth-workflow-bucket-operation.html
SummaryAttributes:
  - bucket
  - key
  - requester

Stages and Predicates

Fires on AWS.S3ServerAccess events when all of the conditions below hold.

Condition

  • bucket is one of example-bucket
  • requester is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
bucketin
  • example-bucket
field:"bucket" kind:in value:"example-bucket"
requesteris_null
  • (no value, null check)
field:"requester" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucket

Response runbook

If unauthenticated S3 access is not expected for this bucket, update its access policies.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "bucket": "example-bucket"
}

AWS S3 Unknown Requester

#
Severity
low
Compliance
Panther Data Access
Log types
AWS.S3ServerAccess
Tags
AWS, Configuration Required, Security Control, Collection:Data From Cloud Storage Object
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Validates that proper IAM entities are accessing sensitive data buckets.

MITRE ATT&CK coverage

TacticTechniques
Collection

Detection logic

from fnmatch import fnmatch

from panther_aws_helpers import aws_rule_context

# pylint: disable=line-too-long
BUCKET_ROLE_MAPPING = {
    "panther-bootstrap-processeddata-*": [
        "arn:aws:sts::*:assumed-role/panther-cloud-security-EventProcessorFunctionRole-*/panther-aws-event-processor",
        "arn:aws:sts::*:assumed-role/panther-log-analysis-AthenaApiFunctionRole-*/panther-athena-api",
        "arn:aws:sts::*:assumed-role/panther-log-analysis-RulesEngineFunctionRole-*/panther-rules-engine",
        "arn:aws:sts::*:assumed-role/panther-snowflake-logprocessing-role-*/snowflake",
        "arn:aws:sts::*:assumed-role/panther-data-replication-role-*/s3-replication",
    ]
}
# pylint: enable=line-too-long


def _unknown_requester_access(event):
    for bucket_pattern, role_patterns in BUCKET_ROLE_MAPPING.items():
        if not fnmatch(event.get("bucket", ""), bucket_pattern):
            continue
        if not any(
            (fnmatch(event.get("requester", ""), role_pattern) for role_pattern in role_patterns)
        ):
            return True
    return False


def rule(event):
    if event.get("errorcode"):
        return False

    return event.get("operation") == "REST.GET.OBJECT" and _unknown_requester_access(event)


def title(event):
    return (
        f"Unknown requester accessing data from S3 Bucket "
        f"[{event.get('bucket', '<UNKNOWN_BUCKET>')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_unknown_requester_get_object.py
RuleID: "AWS.S3.ServerAccess.UnknownRequester"
DisplayName: "AWS S3 Unknown Requester"
DedupPeriodMinutes: 60 # 1 hour
Enabled: false
LogTypes:
  - AWS.S3ServerAccess
Tags:
  - AWS
  - Configuration Required
  - Security Control
  - Collection:Data From Cloud Storage Object
Reports:
  Panther:
    - Data Access
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: Validates that proper IAM entities are accessing sensitive data buckets.
Runbook: If the S3 access is not expected for this bucket, investigate the requester's other traffic.
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/walkthrough1.html
SummaryAttributes:
  - bucket
  - key
  - operation
  - userAgent
  - remoteip
  - requester
  - p_any_aws_arns
  - p_any_aws_account_ids

Stages and Predicates

Fires on AWS.S3ServerAccess events when all of the conditions below hold.

Condition

  • errorcode is empty
  • operation is REST.GET.OBJECT

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorcodeis_null
  • (no value, null check)
field:"errorcode" kind:is_null
operationeq
  • REST.GET.OBJECT
field:"operation" kind:eq value:"REST.GET.OBJECT"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucket

Response runbook

If the S3 access is not expected for this bucket, investigate the requester's other traffic.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "authenticationtype": "AuthHeader",
  "bucket": "panther-bootstrap-processeddata-AF1341JAK",
  "bucketowner": "f16a9e81a6589df1c902c86f7982fd14a88787db",
  "ciphersuite": "ECDHE-RSA-AES128-SHA",
  "hostheader": "cloudtrail.s3.us-east-1.amazonaws.com",
  "hostid": "neRpT/AXRsS3LMBqq/wND59opwPRWWKn7F6evEhdbS99me5fyIXpVI/MMIn6ECgU1YZAqwuF8Bw=",
  "httpstatus": 200,
  "key": "AWSLogs/o-wwwwwwgggg/234567890123/CloudTrail-Digest/ca-central-1/2020/02/14/234567890123_CloudTrail-Digest_ca-central-1_POrgTrail_us-east-1_20200214T001007Z.json.gz",
  "objectsize": 747,
  "operation": "REST.GET.OBJECT",
  "remoteip": "127.0.0.1",
  "requester": "arn:aws:iam::123456789012:user/jim-bob",
  "requestid": "101B7403B9828743",
  "requesturi": "PUT /AWSLogs/o-wwwwwwgggg/234567890123/CloudTrail-Digest/ca-central-1/2020/02/14/234567890123_CloudTrail-Digest_ca-central-1_POrgTrail_us-east-1_20200214T001007Z.json.gz HTTP/1.1",
  "signatureversion": "SigV4",
  "time": "2020-02-14 00:53:48.000000000",
  "tlsVersion": "TLSv1.2",
  "totaltime": 110,
  "turnaroundtime": 20,
  "useragent": "aws-internal/3 aws-sdk-java/1.11.714 Linux/4.9.184-0.1.ac.235.83.329.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.242-b08 java/1.8.0_242 vendor/Oracle_Corporation"
}

AWS SAML Activity

#
Severity
medium
Entities
aws_account_ids, aws_arns, domain_names, trace_ids, usernames
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Identifies when SAML activity has occurred in AWS. An adversary could gain backdoor access via SAML.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

SAML_ACTIONS = ["UpdateSAMLProvider", "CreateSAMLProvider", "DeleteSAMLProvider"]


def rule(event):
    # Allow AWSSSO to manage
    if event.deep_get("userIdentity", "arn", default="").endswith(
        ":assumed-role/AWSServiceRoleForSSO/AWS-SSO"
    ):
        return False
    # Don't alert on errors such as EntityAlreadyExistsException and NoSuchEntity
    if event.get("errorCode"):
        return False
    return (
        event.get("eventSource") == "iam.amazonaws.com" and event.get("eventName") in SAML_ACTIONS
    )


def title(event):
    return (
        f"[{event.deep_get('userIdentity','arn')}] "
        f"performed [{event.get('eventName')}] "
        f"in account [{event.get('recipientAccountId')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: Identifies when SAML activity has occurred in AWS. An adversary could gain backdoor access via SAML.
DisplayName: "AWS SAML Activity"
Enabled: true
Filename: aws_saml_activity.py
Reference: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-managing-saml-idp-console.html
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.Suspicious.SAML.Activity"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • userIdentity.arn does not end with :assumed-role/AWSServiceRoleForSSO/AWS-SSO
  • errorCode is empty
  • eventSource is iam.amazonaws.com
  • eventName is one of UpdateSAMLProvider, CreateSAMLProvider, DeleteSAMLProvider

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.arnends_with:assumed-role/AWSServiceRoleForSSO/AWS-SSOexcludes:userIdentity.arn field:"userIdentity.arn" value:":assumed-role/AWSServiceRoleForSSO/AWS-SSO"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • CreateSAMLProvider
  • DeleteSAMLProvider
  • UpdateSAMLProvider
field:"aws::eventName" kind:in
eventSourceeq
  • iam.amazonaws.com
field:"aws::eventSource" kind:eq value:"iam.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "EID12345",
  "eventName": "CreateSAMLProvider",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2021-10-14 21:25:20",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "recipientAccountId": "0123456789",
  "requestID": "ABC1234",
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "cloudformation.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "ABCDEFGHIJK",
    "accountId": "0123456789",
    "arn": "arn:aws:sts::0123456789:assumed-role/role/account",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "0123456789:AWSCloudFormation",
    "sessionContext": {
      "attributes": {
        "creationDate": "2021-10-14T21:25:20Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "0123456789",
        "arn": "arn:aws:iam::0123456789:role/ServiceRole",
        "principalId": "ABCDEFGI0123",
        "type": "Role",
        "userName": "ServiceRole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Secrets Manager Batch Retrieve Secrets

#
Status
Experimental
Severity
informational
Compliance
Stratus Red Team aws.credential-access.secretsmanager-batch-retrieve-secrets
Log types
AWS.CloudTrail
Tags
AWS, Credential Access, Stratus Red Team
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventName") == "BatchGetSecretValue":
        return True
    return False


def title(event):
    user = event.udm("actor_user")
    return (
        f"[{user}] attempted to batch retrieve a large number of secrets from AWS Secrets Manager"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_secretsmanager_retrieve_secrets_batch.py
RuleID: "AWS.SecretsManager.BatchRetrieveSecrets"
DisplayName: "AWS Secrets Manager Batch Retrieve Secrets"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Credential Access
  - Stratus Red Team
Status: Experimental
Reports:
  MITRE ATT&CK:
    - TA0006:T1552 # Credentials from Password Stores 
  Stratus Red Team:
    - aws.credential-access.secretsmanager-batch-retrieve-secrets
Severity: Info
Description: >
  An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). 
  An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets.
Runbook: https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.secretsmanager-batch-retrieve-secrets/
Threshold: 5
DedupPeriodMinutes: 1440
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is BatchGetSecretValue
Alert cadence
alerts after 5 matches within 1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "eventName": "BatchGetSecretValue",
  "eventSource": "secretsmanager.amazonaws.com",
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "012345678901",
  "requestParameters": {
    "filters": [
      {
        "key": "tag-key",
        "values": [
          "StratusRedTeam"
        ]
      }
    ]
  },
  "responseElements": null
}

AWS Secrets Manager Batch Retrieve Secrets Catch-All

#
Status
Experimental
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS, Credential Access, Stratus Red Team
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets. Although BatchGetSecretValue requires a list of secret IDs or a filter, an attacker may use a catch-all filter to retrieve all secrets by batch. This rule identifies BatchGetSecretValue events with a catch-all filter.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if event.get("eventName") != "BatchGetSecretValue":
        return False

    filters = event.deep_get("requestParameters", "filters", default=[])
    for filt in filters:
        if filt.get("key") != "tag-key":
            return False
        if any(not value.startswith("!") for value in filt.get("values")):
            return False

    return True


def title(event):
    user = event.udm("actor_user")
    return (
        f"[{user}] attempted to batch retrieve secrets from "
        "AWS Secrets Manager with a catch-all filter"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_secretsmanager_retrieve_secrets_catchall.py
RuleID: "AWS.SecretsManager.BatchRetrieveSecretsCatchAll"
DisplayName: "AWS Secrets Manager Batch Retrieve Secrets Catch-All"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Credential Access
  - Stratus Red Team
Status: Experimental
Reports:
  MITRE ATT&CK:
    - TA0006:T1552 # Credentials from Password Stores 
Severity: Info
Description: >
  An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). 
  An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets.
  Although BatchGetSecretValue requires a list of secret IDs or a filter, an attacker may use a catch-all filter to retrieve all secrets by batch.
  This rule identifies BatchGetSecretValue events with a catch-all filter.
Runbook: https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.secretsmanager-batch-retrieve-secrets/
Threshold: 1
DedupPeriodMinutes: 1440
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is BatchGetSecretValue

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "eventName": "BatchGetSecretValue",
  "eventSource": "secretsmanager.amazonaws.com",
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "012345678901",
  "requestParameters": {
    "filters": [
      {
        "key": "tag-key",
        "values": [
          "!tagKeyThatWillNeverExist"
        ]
      }
    ]
  },
  "responseElements": null
}

AWS Secrets Manager Retrieve Secrets Multi-Region

#
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS, Credential Access, Stratus Red Team
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets. This rule identifies BatchGetSecretValue events for multiple regions in a short period of time.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return event.get("eventName") == "BatchGetSecretValue"


def unique(event):
    return event.get("awsRegion", "")


def title(event):
    user = event.udm("actor_user")
    return f"[{user}] attempted to retrieve secrets from AWS Secrets Manager in multiple regions"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_secretsmanager_retrieve_secrets_multiregion.py
RuleID: "AWS.SecretsManager.RetrieveSecretsMultiRegion"
DisplayName: "AWS Secrets Manager Retrieve Secrets Multi-Region"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Credential Access
  - Stratus Red Team
Reports:
  MITRE ATT&CK:
    - TA0006:T1552 # Credentials from Password Stores 
Severity: Info
Description: >
  An attacker attempted to retrieve a high number of Secrets Manager secrets by batch, through secretsmanager:BatchGetSecretValue (released Novemeber 2023). 
  An attacker may attempt to retrieve a high number of secrets by batch, to avoid detection and generate fewer calls. Note that the batch size is limited to 20 secrets.
  This rule identifies BatchGetSecretValue events for multiple regions in a short period of time.
Runbook: https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.secretsmanager-batch-retrieve-secrets/
Threshold: 5
DedupPeriodMinutes: 10
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is BatchGetSecretValue
Alert cadence
alerts after 5 matches within 10m

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

https://aws.amazon.com/blogs/security/how-to-use-the-batchgetsecretsvalue-api-to-improve-your-client-side-applications-with-aws-secrets-manager/

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "BatchGetSecretValue",
  "eventSource": "secretsmanager.amazonaws.com",
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "012345678901",
  "requestParameters": {
    "filters": [
      {
        "key": "tag-key",
        "values": [
          "!tagKeyThatWillNeverExist"
        ]
      }
    ]
  },
  "responseElements": null
}

AWS Security Group - Only DMZ Publicly Accessible

#
Severity
medium
Compliance
PCI 1.3.1, 1.1.4
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
en.wikipedia.org
Source
github.com/panther-labs/panther-analysis

This policy validates that only Security Groups designated as DMZs allow inbound traffic from public IP space. This helps ensure no traffic is bypassing the DMZ.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

import json
from ipaddress import ip_network
from unittest.mock import MagicMock

# NOTE: Make sure to adjust DMZ_TAGS

DMZ_TAGS = [
    # ["environment", "dmz"]
]


# Defaults to False to assume something is not a DMZ if it is not tagged
def is_dmz_tags(resource, dmz_tags):
    """This function determines whether a given resource is tagged as existing in a DMZ."""
    if resource["Tags"] is None:
        return False
    for key, value in dmz_tags:
        if resource["Tags"].get(key) == value:
            return True
    return False


def policy(resource):
    # If this security group allows no inbound connections, it is secure
    if resource["IpPermissions"] is None:
        return True

    # DMZ security groups can have inbound permissions from the internet
    global DMZ_TAGS  # pylint: disable=global-statement
    if isinstance(DMZ_TAGS, MagicMock):
        DMZ_TAGS = {tuple(kv) for kv in json.loads(DMZ_TAGS())}  # pylint: disable=not-callable
    if is_dmz_tags(resource, DMZ_TAGS):
        return True

    for permission in resource["IpPermissions"]:
        # Check if any traffic is allowed from public IP space
        for ip_range in permission["IpRanges"] or []:
            if ip_range["CidrIp"] == "0.0.0.0/0" or not ip_network(ip_range["CidrIp"]).is_private:
                return False
        for ip_range in permission["Ipv6Ranges"] or []:
            if ip_range["CidrIpv6"] == "::/0" or not ip_network(ip_range["CidrIpv6"]).is_private:
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_only_dmz_security_groups_publicly_accessible.py
PolicyID: "AWS.SecurityGroup.OnlyDMZPubliclyAccessible"
DisplayName: "AWS Security Group - Only DMZ Publicly Accessible"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 1.3.1
    - 1.1.4
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  This policy validates that only Security Groups designated as DMZs allow inbound traffic from public IP space. This helps ensure no traffic is bypassing the DMZ.
Runbook: >
  Remove the IP permissions in non-DMZ Security Groups that are allowing inbound traffic from public IP space.
Reference: https://en.wikipedia.org/wiki/DMZ_(computing)

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when all of the conditions below hold.

Condition

  • IpPermissions is present
  • Tags is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Tagsis_not_null(no value, null check)excludes:Tags
IpPermissionsis_null(no value, null check)excludes:IpPermissions

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsis_not_null
  • (no value, null check)
field:"IpPermissions" kind:is_not_null
Tagsis_null
  • (no value, null check)
field:"Tags" kind:is_null

Response runbook

Remove the IP permissions in non-DMZ Security Groups that are allowing inbound traffic from public IP space.

AWS Security Group Administrative Ingress

#
Severity
high
Compliance
CIS 4.1, 4.2
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, Security Control, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that AWS Security Groups don't allow unrestricted inbound traffic on port 3389 or 22, ports commonly used for the remote access protocols RDP and SSH respectively.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

RESTRICTED_PORTS = [22, 3389]


# Returns true if at least one of the ports in check_ports are between from_port and to_port
# Returns true if from_port or to_port is None as that indicates an unrestricted range of ports
def port_checker(check_ports, from_port, to_port):
    if from_port is None and to_port is None:
        return True

    for port in check_ports:
        if from_port <= port <= to_port:
            return True
    return False


def policy(resource):
    if resource["IpPermissions"] is None:
        return True

    for permission in resource["IpPermissions"]:
        src_open = False
        for ip_range in permission["IpRanges"] or []:
            if ip_range["CidrIp"] == "0.0.0.0/0":
                src_open = True
                break
        for ipv6_range in permission["Ipv6Ranges"] or []:
            if src_open or ipv6_range["CidrIpv6"] == "::/0":
                src_open = True
                break
        if src_open and port_checker(
            RESTRICTED_PORTS, permission["FromPort"], permission["ToPort"]
        ):
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_administrative_ingress.py
PolicyID: "AWS.SecurityGroup.AdministrativeIngress"
DisplayName: "AWS Security Group Administrative Ingress"
Enabled: true
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - Security Control
  - Initial Access:Exploit Public-Facing Application
Reports:
  CIS:
    - 4.1
    - 4.2
  MITRE ATT&CK:
    - TA0001:T1190
Severity: High
Description: >
  This policy validates that AWS Security Groups don't allow unrestricted inbound traffic on
  port 3389 or 22, ports commonly used for the remote access protocols RDP and SSH respectively.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-securitygroup-restricts-ingress-on-administrative-ports
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissions is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsis_null(no value, null check)excludes:IpPermissions

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsis_not_null
  • (no value, null check)
field:"IpPermissions" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-securitygroup-restricts-ingress-on-administrative-ports

AWS Security Group Restricts Access To CDE

#
Severity
medium
Compliance
PCI 1.3.2, 1.3.5
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that are considered part of the PCI CDE do not allow any access from public IP space.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from ipaddress import ip_network


def policy(resource):

    for permission in resource["IpPermissions"] or []:
        # Check if any traffic is allowed from public IP space
        for ip_range in permission["IpRanges"] or []:
            if ip_range["CidrIp"] == "0.0.0.0/0" or not ip_network(ip_range["CidrIp"]).is_private:
                return False
        for ip_range in permission["Ipv6Ranges"] or []:
            if ip_range["CidrIpv6"] == "::/0" or not ip_network(ip_range["CidrIpv6"]).is_private:
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_restricts_access_to_cde.py
PolicyID: "AWS.SecurityGroup.RestrictsAccessToCDE"
DisplayName: "AWS Security Group Restricts Access To CDE"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 1.3.2
    - 1.3.5
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  This policy validates that are considered part of the PCI CDE do not allow any access from public IP space.
Runbook: >
  Remove any IP permissions allowing inbound traffic from Public IP space to the CDE.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Remove any IP permissions allowing inbound traffic from Public IP space to the CDE.

AWS Security Group Restricts Inbound Traffic

#
Severity
medium
Compliance
PCI 1.1.4, 1.3.5
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Security Groups have some restrictions on inbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

# This is a generic policy that checks inbound permissions on a Security Group.
# You may wish to add additional logic specific to your use cases.


def policy(resource):

    if resource["IpPermissions"] is None:
        return True

    for permission in resource["IpPermissions"]:
        # Check if the permission is set to "All Ports"
        if permission["FromPort"] is None or permission["ToPort"] is None:
            return False
        # Check if the permission is set to "All TCP" or "All UDP" ports
        if permission["FromPort"] == 0 and permission["ToPort"] == 65535:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_restricts_inbound_traffic.py
PolicyID: "AWS.SecurityGroup.RestrictsInboundTraffic"
DisplayName: "AWS Security Group Restricts Inbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 1.1.4
    - 1.3.5
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  This policy validates that Security Groups have some restrictions on inbound traffic.
Runbook: >
  Add appropriate restrictions on inbound traffic via IP permissions.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissions is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsis_null(no value, null check)excludes:IpPermissions

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsis_not_null
  • (no value, null check)
field:"IpPermissions" kind:is_not_null

Response runbook

Add appropriate restrictions on inbound traffic via IP permissions.

AWS Security Group Restricts Inter-SG Traffic

#
Severity
low
Compliance
PCI 1.2.1
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Lateral Movement:Exploitation of Remote Services
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Security Groups have restrictions on inter Security Group traffic. Administrators may assume there is an implicit level of trust between Security Groups in the same account, but this is not always a good assumption in cases one Security Group contains far more sensitive data that another.

MITRE ATT&CK coverage

TacticTechniques
Lateral Movement

Detection logic

def policy(resource):

    if resource["IpPermissions"] is None:
        return True

    for permission in resource["IpPermissions"]:
        # Only check Security Group -> Security Group permissions
        if not permission["UserIdGroupPairs"]:
            continue
        # Check if the permission is set to "All Ports"
        if permission["FromPort"] is None or permission["ToPort"] is None:
            return False
        # Check if the permission is set to "All TCP" or "All UDP" ports
        if permission["FromPort"] == 0 and permission["ToPort"] == 65535:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_restricts_inter_security_group_traffic.py
PolicyID: "AWS.SecurityGroup.RestrictsInterSecurityGroupTraffic"
DisplayName: "AWS Security Group Restricts Inter-SG Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Lateral Movement:Exploitation of Remote Services
Reports:
  PCI:
    - 1.2.1
  MITRE ATT&CK:
    - TA0008:T1210
Severity: Low
Description: >
  This policy validates that Security Groups have restrictions on inter Security Group traffic. Administrators may assume there is an implicit level of trust between Security Groups in the same account, but this is not always a good assumption in cases one Security Group contains far more sensitive data that another.
Runbook: >
  Add appropriate restrictions to the Security Group peering connection.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissions is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsis_null(no value, null check)excludes:IpPermissions

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsis_not_null
  • (no value, null check)
field:"IpPermissions" kind:is_not_null

Response runbook

Add appropriate restrictions to the Security Group peering connection.

AWS Security Group Restricts Outbound Traffic

#
Severity
low
Compliance
PCI 1.1.4, 1.3.2
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Security Groups have some restrictions on outbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

# This is a generic policy that checks outbound permissions on Security Groups.
# You may wish to add additional logic specific to your use case.


def policy(resource):

    if resource["IpPermissionsEgress"] is None:
        return True

    for permission in resource["IpPermissionsEgress"]:
        # Check if the permission is set to "All Ports"
        if permission["FromPort"] is None or permission["ToPort"] is None:
            return False
        # Check if the permission is set to "All TCP" or "All UDP" ports
        if permission["FromPort"] == 0 and permission["ToPort"] == 65535:
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_restricts_outbound_traffic.py
PolicyID: "AWS.SecurityGroup.RestrictsOutboundTraffic"
DisplayName: "AWS Security Group Restricts Outbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Exfiltration:Exfiltration Over Web Service
Reports:
  PCI:
    - 1.1.4
    - 1.3.2
  MITRE ATT&CK:
    - TA0010:T1567
Severity: Low
Description: >
  This policy validates that Security Groups have some restrictions on outbound traffic.
Runbook: >
  Add appropriate restrictions to outbound traffic via IP permissions.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissionsEgress is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsEgressis_null(no value, null check)excludes:IpPermissionsEgress

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsEgressis_not_null
  • (no value, null check)
field:"IpPermissionsEgress" kind:is_not_null

Response runbook

Add appropriate restrictions to outbound traffic via IP permissions.

AWS Security Group Restricts Traffic Leaving CDE

#
Severity
medium
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that there are restrictions on what type of traffic may leave Security Groups that are considered with the scope of the PCI CDE. These restrictions help ensure that cardholder data does not leave the CDE.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

from ipaddress import ip_network


def policy(resource):

    for permission in resource["IpPermissionsEgress"] or []:
        # Check if any traffic can leave this security group to public IP space
        for ip_range in permission["IpRanges"] or []:
            if ip_range["CidrIp"] == "0.0.0.0/0" or not ip_network(ip_range["CidrIp"]).is_private:
                return False
        for ip_range in permission["Ipv6Ranges"] or []:
            if ip_range["CidrIpv6"] == "::/0" or not ip_network(ip_range["CidrIpv6"]).is_private:
                return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_restricts_traffic_leaving_cde.py
PolicyID: "AWS.SecurityGroup.RestrictsTrafficLeavingCDE"
DisplayName: "AWS Security Group Restricts Traffic Leaving CDE"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Exfiltration:Exfiltration Over Web Service
Reports:
  MITRE ATT&CK:
    - TA0010:T1567
Severity: Medium
Description: >
  This policy validates that there are restrictions on what type of traffic may leave Security Groups that are considered with the scope of the PCI CDE. These restrictions help ensure that cardholder data does not leave the CDE.
Runbook: >
  Add appropriate restrictions to traffic leaving Security Groups considered a part of the CDE.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Add appropriate restrictions to traffic leaving Security Groups considered a part of the CDE.

AWS Security Group Tightly Restricts Inbound Traffic

#
Severity
low
Compliance
PCI 1.1.4, 1.2.1, 8.2.1
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Security Groups have restrictive permission sets that both limit the total number of open ports, as well as limiting ports typically associated with insecure protocols.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

MAX_PORTS_PER_PERMISSION = 10
RESTRICTED_PORTS = [
    21,  # FTP default
    22,  # SSH default
    23,  # Telnet default
    3389,  # RDP default
]


def policy(resource):

    if resource["IpPermissions"] is None:
        return True

    for permission in resource["IpPermissions"]:
        # Check if the permission is set to "All Ports"
        if permission["FromPort"] is None or permission["ToPort"] is None:
            return False
        # Check if the permission allows too many ports. Alternatively, this can be modified to sum
        # open ports to have one running total.
        if permission["ToPort"] - permission["FromPort"] > MAX_PORTS_PER_PERMISSION:
            return False
        if any(permission["FromPort"] <= port <= permission["ToPort"] for port in RESTRICTED_PORTS):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_tightly_restricts_inbound_traffic.py
PolicyID: "AWS.SecurityGroup.TightlyRestrictsInboundTraffic"
DisplayName: "AWS Security Group Tightly Restricts Inbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 1.1.4
    - 1.2.1
    - 8.2.1
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Low
Description: >
  This policy validates that Security Groups have restrictive permission sets that both limit the total number of open ports, as well as limiting ports typically associated with insecure protocols.
Runbook: >
  Add appropriate restrictions to inbound traffic on Security Groups via IP permissions.
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissions is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsis_null(no value, null check)excludes:IpPermissions

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsis_not_null
  • (no value, null check)
field:"IpPermissions" kind:is_not_null

Response runbook

Add appropriate restrictions to inbound traffic on Security Groups via IP permissions.

AWS Security Group Tightly Restricts Outbound Traffic

#
Severity
low
Compliance
PCI 1.1.4
Resource types
AWS.EC2.SecurityGroup
Tags
AWS, PCI, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that Security Groups have restrictive controls on outbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Detection logic

MAX_PORTS_PER_PERMISSION = 10
RESTRICTED_PORTS = [
    21,  # FTP default
    22,  # SSH default
    23,  # Telnet default
    3389,  # RDP default
]


def policy(resource):
    if resource["IpPermissionsEgress"] is None:
        return True

    for permission in resource["IpPermissionsEgress"]:
        # Check if the permission is set to "All Ports"
        if permission["FromPort"] is None or permission["ToPort"] is None:
            return False
        # Check if the permission allows too many ports
        if permission["ToPort"] - permission["FromPort"] > MAX_PORTS_PER_PERMISSION:
            return False
        # Check if the permission allows too many ports. Alternatively, this can be modified to sum
        # open ports to have one running total.
        if permission["ToPort"] - permission["FromPort"] > MAX_PORTS_PER_PERMISSION:
            return False
        if any(permission["FromPort"] <= port <= permission["ToPort"] for port in RESTRICTED_PORTS):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_security_group_tightly_restricts_outbound_traffic.py
PolicyID: "AWS.SecurityGroup.TightlyRestrictsOutboundTraffic"
DisplayName: "AWS Security Group Tightly Restricts Outbound Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.SecurityGroup
Tags:
  - AWS
  - PCI
  - Exfiltration:Exfiltration Over Web Service
Reports:
  PCI:
    - 1.1.4
  MITRE ATT&CK:
    - TA0010:T1567
Severity: Low
Description: >
  This policy validates that Security Groups have restrictive controls on outbound traffic.
Runbook: >
  Add appropriate restrictions to outbound traffic on Security Groups via IP permissions
Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-services-ec2-sg.html

Stages and Predicates

Flags AWS.EC2.SecurityGroup resources when the condition below holds.

Condition

  • IpPermissionsEgress is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpPermissionsEgressis_null(no value, null check)excludes:IpPermissionsEgress

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IpPermissionsEgressis_not_null
  • (no value, null check)
field:"IpPermissionsEgress" kind:is_not_null

Response runbook

Add appropriate restrictions to outbound traffic on Security Groups via IP permissions

AWS SecurityHub Finding Evasion

#
Severity
high
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detections modification of findings in SecurityHub

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

EVASION_OPERATIONS = ["BatchUpdateFindings", "DeleteInsight", "UpdateFindings", "UpdateInsight"]


def rule(event):
    if (
        event.get("eventSource", "") == "securityhub.amazonaws.com"
        and event.get("eventName", "") in EVASION_OPERATIONS
    ):
        return True
    return False


def title(event):
    return (
        "SecurityHub Findings have been modified in account: "
        f"[{event.get('recipientAccountId','')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: Detections modification of findings in SecurityHub
DisplayName: "AWS SecurityHub Finding Evasion"
Enabled: true
Filename: aws_securityhub_finding_evasion.py
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Reference: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-insights-view-take-action.html
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.SecurityHub.Finding.Evasion"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is securityhub.amazonaws.com
  • eventName is one of BatchUpdateFindings, DeleteInsight, UpdateFindings, UpdateInsight

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • BatchUpdateFindings
  • DeleteInsight
  • UpdateFindings
  • UpdateInsight
field:"aws::eventName" kind:in
eventSourceeq
  • securityhub.amazonaws.com
field:"aws::eventSource" kind:eq value:"securityhub.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "3dabcebf-35b0-443f-a1a2-26e186ce23bf",
  "eventName": "DeleteInsight",
  "eventSource": "securityhub.amazonaws.com",
  "eventTime": "2018-11-25T01:02:18Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "012345678901",
  "requestID": "c0fffccd-f04d-11e8-93fc-ddcd14710066",
  "requestParameters": {
    "Filters": {},
    "Name": "Test Insight",
    "ResultField": "ResourceId"
  },
  "responseElements": {
    "InsightArn": "arn:aws:securityhub:us-west-2:0123456789010:insight/custom/f4c4890b-ac6b-4c26-95f9-e62cc46f3055"
  },
  "sourceIPAddress": "205.251.233.179",
  "userAgent": "aws-cli/1.11.76 Python/2.7.10 Darwin/17.7.0 botocore/1.5.39",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "012345678901",
    "arn": "arn:aws:iam::012345678901:user/TestUser",
    "principalId": "AIDAJK6U5DS22IAVUI7BW",
    "type": "IAMUser",
    "userName": "TestUser"
  }
}

AWS Snapshot Made Public

#
Severity
medium
Compliance
Stratus Red Team aws.exfiltration.ec2-share-ebs-snapshot
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration:Transfer Data to Cloud Account
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An AWS storage snapshot was made public.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from collections.abc import Mapping

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_base_helpers import deep_get

IS_SINGLE_USER_SHARE = False  # Used to adjust severity


def rule(event):
    if not aws_cloudtrail_success(event):
        return False

    # EC2 Volume snapshot made public
    if event.get("eventName") == "ModifySnapshotAttribute":
        parameters = event.get("requestParameters", {})
        if parameters.get("attributeType") != "CREATE_VOLUME_PERMISSION":
            return False

        items = deep_get(parameters, "createVolumePermission", "add", "items", default=[])
        for item in items:
            if not isinstance(item, (Mapping, dict)):
                continue
            if item.get("userId") or item.get("group") == "all":
                global IS_SINGLE_USER_SHARE  # pylint: disable=global-statement
                IS_SINGLE_USER_SHARE = "userId" in item  # Used for dynamic severity
                return True
        return False

    return False


def severity(_):
    # Set severity to INFO if only shared with a single user
    if IS_SINGLE_USER_SHARE:
        return "INFO"
    return "DEFAULT"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_snapshot_made_public.py
RuleID: "AWS.CloudTrail.SnapshotMadePublic"
DisplayName: "AWS Snapshot Made Public"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Reports:
  MITRE ATT&CK:
    - TA0010:T1537

  Stratus Red Team:
    - aws.exfiltration.ec2-share-ebs-snapshot
Description: An AWS storage snapshot was made public.
Reference: 
  https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-modifying-snapshot-permissions.html
Runbook: Adjust the snapshot configuration so that it is no longer public.
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
Tags:
  - AWS
  - Exfiltration:Transfer Data to Cloud Account

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is ModifySnapshotAttribute
  • requestParameters.attributeType is CREATE_VOLUME_PERMISSION

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Adjust the snapshot configuration so that it is no longer public.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1111",
  "eventName": "ModifySnapshotAttribute",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "attributeType": "CREATE_VOLUME_PERMISSION",
    "createVolumePermission": {
      "add": {
        "items": [
          {
            "group": "all"
          }
        ]
      }
    },
    "snapshotId": "snap-1111"
  },
  "responseElements": {
    "_return": true,
    "requestId": "1111"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla/2.0 (compatible; NEWT ActiveX; Win32)",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS Software Discovery

#
Severity
informational
Group by
actor_user
Entities
aws_account_ids, aws_arns, ip_addresses
Log types
AWS.CloudTrail
Tags
Configuration Required
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

A user is obtaining a list of security software, configurations, defensive tools, and sensors that are in AWS.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

PlatformRecord / event type
AWSCloudTrail event DescribeSecurityGroups: Returns information about one or more EC2 security groups, including their inbound and outbound rules.
AWSCloudTrail event DescribeSecurityGroupReferences: [EC2-VPC only] Describes the VPCs on the other side of a VPC peering connection that are referencing the security groups you've specified in this request.
AWSCloudTrail event DescribeSecurityGroupRules: Describes one or more of your security group rules.
AWSCloudTrail event DescribeSubnets: Describes one or more of your subnets.
AWSCloudTrail event DescribeFirewall: Returns the data objects for the specified firewall.
AWSCloudTrail event DescribeFirewallPolicy: Returns the data objects for the specified firewall policy.
AWSCloudTrail event DescribeRuleGroup: Returns the data objects for the specified rule group.
AWSCloudTrail event ListFirewallPolicies: Retrieves the metadata for the firewall policies that you have defined.
AWSCloudTrail event ListFirewalls: Retrieves the metadata for the firewalls that you have defined.
AWSCloudTrail event DescribeProducts: Returns information about product integrations in Security Hub CSPM.
AWSCloudTrail event DescribeStandards: Returns a list of the available standards in Security Hub CSPM.
AWSCloudTrail event DescribeStandardsControls: Returns a list of security standards controls.
AWSCloudTrail event DescribeInstanceInformation: Provides information about one or more of your managed nodes, including the operating system platform, SSM Agent version, association status, and IP address.

Detection logic

from panther_aws_helpers import aws_rule_context

DISCOVERY_EVENTS = [
    "ListDocuments",
    "ListMembers",
    "DescribeProducts",
    "DescribeStandards",
    "DescribeStandardsControls",
    "DescribeInstanceInformation",
    "DescribeSecurityGroups",
    "DescribeSecurityGroupRules",
    "DescribeSecurityGroupReferences",
    "DescribeSubnets",
    "DescribeHub",
    "ListFirewalls",
    "ListRuleGroups",
    "ListFirewallPolicies",
    "DescribeFirewall",
    "DescribeFirewallPolicy",
    "DescribeLoggingConfiguration",
    "DescribeResourcePolicy",
    "DescribeRuleGroup",
]


def rule(event):
    return event.get("eventName") in DISCOVERY_EVENTS


def title(event):
    return (
        f"User [{event.udm('actor_user')}] "
        f"performed a [{event.get('eventName')}] "
        f"action in AWS account [{event.get('recipientAccountId')}]."
    )


def dedup(event):
    return event.udm("actor_user")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: A user is obtaining a list of security software, configurations, defensive tools, and sensors that are in AWS.
DisplayName: "AWS Software Discovery"
Enabled: false
Filename: aws_software_discovery.py
Reference: https://attack.mitre.org/techniques/T1518/001/
Tags:
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0007:T1518
Severity: Info
CreateAlert: false
DedupPeriodMinutes: 360 # 6 hours
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.Software.Discovery"
Threshold: 50

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is one of ListDocuments, ListMembers, DescribeProducts, DescribeStandards, DescribeStandardsControls (+14 more values, see Indicators below)
Alert cadence
alerts after 50 matches within 6h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DescribeFirewall
  • DescribeFirewallPolicy
  • DescribeHub
  • DescribeInstanceInformation
  • DescribeLoggingConfiguration
  • DescribeProducts
  • DescribeResourcePolicy
  • DescribeRuleGroup
  • DescribeSecurityGroupReferences
  • DescribeSecurityGroupRules
  • DescribeSecurityGroups
  • DescribeStandards
  • DescribeStandardsControls
  • DescribeSubnets
  • ListDocuments
  • ListFirewallPolicies
  • ListFirewalls
  • ListMembers
  • ListRuleGroups
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "6faccad9-b6ae-4549-8e39-03430cbce2aa",
  "eventName": "DescribeSecurityGroups",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2021-10-19 01:06:59",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "123456789012"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::123456789012:role/ExampleRole-us-east-2",
    "arn:aws:sts::123456789012:assumed-role/ExampleRole-us-east-2/153151351351351"
  ],
  "p_any_ip_addresses": [
    "12.34.56.78"
  ],
  "p_event_time": "2021-10-19 01:06:59",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2021-10-19 01:11:10.412",
  "p_row_id": "eabaceda7842c2b2e7a398f40cc150",
  "p_source_id": "5f9f0f60-9c56-4027-b93a-8bab3019f0f1",
  "p_source_label": "Hosted - Cloudtrail - XYZ",
  "readOnly": true,
  "recipientAccountId": "123456789012",
  "requestID": "43efad11-bb40-43df-ad25-c8e7f0bfdc7a",
  "requestParameters": {
    "filterSet": {},
    "securityGroupIdSet": {
      "items": [
        {
          "groupId": "sg-01e29ae063f5f63a0"
        }
      ]
    },
    "securityGroupSet": {}
  },
  "sourceIPAddress": "12.34.56.78",
  "userAgent": "aws-sdk-go/1.40.21 (go1.17; linux; amd64) exec-env/AWS_Lambda_go1.x",
  "userIdentity": {
    "accessKeyId": "ASIA153151351351351",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/ExampleRole-us-east-2/153151351351351",
    "principalId": "AROA4NOI7P47OHH3NQORX:153151351351351",
    "sessionContext": {
      "attributes": {
        "creationDate": "2021-10-19T01:05:18Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/ExampleRole-us-east-2",
        "principalId": "AROA153151351351351",
        "type": "Role",
        "userName": "ExampleRole-us-east-2"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS SSM Distributed Command

#
Status
Experimental
Severity
informational
Compliance
Stratus Red Team aws.execution.ssm-send-command
Log types
AWS.CloudTrail
Tags
AWS CloudTrail, AWS SSM, AWS EC2, Execution: Exploitation for Client Execution
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect an attacker utilizing AWS Systems Manager (SSM) to execute commands through SendCommand on multiple EC2 instances.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import datetime as dt
import json

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent
from panther_detection_helpers.caching import get_string_set, put_string_set

# Determine how separate instances need be commanded in order to trigger an alert
INSTANCE_THRESHOLD = 2

all_instance_ids = set()


def rule(event: PantherEvent) -> bool:
    # Exclude events of the wrong type
    if event.get("eventName") != "SendCommand":
        return False

    # Determine if this actor accessed any other params in this account
    key = get_cache_key(event)
    cached_ids = get_cached_instance_ids(key)
    target_instance_ids = set(event.deep_get("requestParameters", "instanceIds", default=[]))

    # Determine if the cache needs updating with new entries
    global all_instance_ids  # pylint: disable=global-statement
    all_instance_ids = cached_ids | target_instance_ids
    if all_instance_ids - cached_ids:
        # Only set the TTL if this is the first time we're adding to the cache
        #   Otherwise we'll be perpetually extending the lifespan of the cached data every time we
        #   add more.
        put_string_set(key, all_instance_ids, epoch_seconds=(3600 if not cached_ids else None))

    # Check combined number of params
    return len(all_instance_ids) > INSTANCE_THRESHOLD


def title(event: PantherEvent) -> str:
    actor = event.udm("actor_user")
    account_name = event.get("recipientAccountId")
    return f"Commands distributed to many EC2 instances by [{actor}] in [{account_name}]"


def severity(event: PantherEvent) -> str:
    # Demote to LOW if attempt was denied
    if not aws_cloudtrail_success(event):
        return "LOW"
    return "DEFAULT"


def alert_context(event: PantherEvent) -> dict:
    global all_instance_ids
    context = aws_rule_context(event)
    context.update({"instanceIds": list(all_instance_ids)})
    return context


def get_cache_key(event) -> str:
    """Use the field values in the event to generate a cache key unique to this actor and
    account ID."""
    offset = (
        dt.datetime.fromisoformat(event.get("p_event_time", "1970-01-01T00:00:00")).timestamp()
        // 3600
        * 3600
    )
    actor = event.udm("actor_user")
    account = event.get("recipientAccountId")
    rule_id = "AWS.SSM.DistributedCommand"
    return f"{rule_id}-{account}-{actor}-{offset}"


def get_cached_instance_ids(key: str) -> set[str]:
    """Get any previously cached parameter names. Included automatic converstion from string in
    the case of a unit test mock."""
    cached_ids = get_string_set(key, force_ttl_check=True)
    if isinstance(cached_ids, str):
        # This is a unit test
        cached_ids = set(json.loads(cached_ids))
    return cached_ids

Rule specification

AnalysisType: rule
Filename: aws_ssm_distributed_command.py
RuleID: "AWS.SSM.DistributedCommand"
DisplayName: AWS SSM Distributed Command
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0002:T1203 # Execution: Exploitation for Client Execution

  Stratus Red Team:
    - aws.execution.ssm-send-command
Description: >
  Detect an attacker utilizing AWS Systems Manager (SSM) to execute commands through SendCommand on multiple EC2 instances.
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ssm-send-command/
Runbook: >
  Detetmine who issued the command, the command content and arguments, and which EC2 instances were affected. Determine the risk of an attacker creating a persistent point of access within one of the instances. Review behaviour logs for the EC2 instances (and their associated IAM roles).
SummaryAttributes:
  - p_any_aws_account_ids
  - p_any_aws_arns
  - p_any_aws_instance_ids
  - p_any_ip_addresses
  - p_any_usernames
Tags:
  - AWS CloudTrail
  - AWS SSM
  - AWS EC2
  - 'Execution: Exploitation for Client Execution'
Status: Experimental

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is SendCommand

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

Detetmine who issued the command, the command content and arguments, and which EC2 instances were affected. Determine the risk of an attacker creating a persistent point of access within one of the instances. Review behaviour logs for the EC2 instances (and their associated IAM roles).

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "ceaea85a-6db9-4595-9842-d904fce2f047",
  "eventName": "SendCommand",
  "eventSource": "ssm.amazonaws.com",
  "eventTime": "2025-02-19 16:32:39.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_event_time": "2025-02-19 16:32:39.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-02-19 16:35:54.509896629",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "c41499e3-c04c-48e7-9da9-ef63d0868553",
  "requestParameters": {
    "documentName": "AWS-RunShellScript",
    "instanceIds": [
      "i-006e4c07b5fba8ad2",
      "i-01bf673280e708b0d",
      "i-020001cca1a2f2628"
    ],
    "interactive": false,
    "parameters": "HIDDEN_DUE_TO_SECURITY_REASONS"
  },
  "responseElements": {
    "command": {
      "alarmConfiguration": {
        "alarms": [],
        "ignorePollAlarmFailure": false
      },
      "clientName": "",
      "clientSourceId": "",
      "cloudWatchOutputConfig": {
        "cloudWatchLogGroupName": "",
        "cloudWatchOutputEnabled": false
      },
      "commandId": "f49a1fe5-d12b-4ac0-98bc-0e4bd83d70c0",
      "comment": "",
      "completedCount": 0,
      "deliveryTimedOutCount": 0,
      "documentName": "AWS-RunShellScript",
      "documentVersion": "$DEFAULT",
      "errorCount": 0,
      "expiresAfter": "Feb 19, 2025, 6:32:39 PM",
      "hasCancelCommandSignature": false,
      "hasSendCommandSignature": false,
      "instanceIds": [
        "i-006e4c07b5fba8ad2",
        "i-01bf673280e708b0d",
        "i-020001cca1a2f2628"
      ],
      "interactive": false,
      "maxConcurrency": "50",
      "maxErrors": "0",
      "notificationConfig": {
        "notificationArn": "",
        "notificationEvents": [],
        "notificationType": ""
      },
      "outputS3BucketName": "",
      "outputS3KeyPrefix": "",
      "outputS3Region": "us-west-2",
      "parameters": "HIDDEN_DUE_TO_SECURITY_REASONS",
      "requestedDateTime": "Feb 19, 2025, 4:32:39 PM",
      "serviceRole": "",
      "status": "Pending",
      "statusDetails": "Pending",
      "targetCount": 3,
      "targets": [],
      "timeoutSeconds": 3600,
      "triggeredAlarms": []
    }
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "sample-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY_ID",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-02-19T16:29:24Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS SSM Multiple Sessions

#
Severity
informational
Tags
AWS CloudTrail, AWS SSM, Execution: Exploitation for Client Execution, Beta
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detect when an actor launches multiple distinct SSM sessions within a single hour period.

MITRE ATT&CK coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context, get_actor_user
from panther_core import PantherEvent


def rule(_) -> bool:
    return True


def title(event: PantherEvent) -> str:
    actor = get_actor_user(event)
    aws_account = event.get("recipientAccountId")
    return f"Multiple SSM Sessions Started by {actor} in {aws_account}"


def alert_context(event: PantherEvent) -> dict:
    return aws_rule_context(event)

Rule specification

AnalysisType: scheduled_rule
Filename: aws_ssm_multiple_sessions.py
RuleID: "AWS.SSM.MultipleSessions"
DisplayName: "AWS SSM Multiple Sessions"
Enabled: true
ScheduledQueries:
  - AWS SSM Multiple Sessions
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0002:T1203 # Execution: Exploitation for Client Execution
Description: >
  Detect when an actor launches multiple distinct SSM sessions within a single hour period.
Reference: >
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ssm-start-session/
Runbook:
  Identify the instances for which sessions were started. Monitor instance and session activity. If possible, reach out to the user to determine the reason for multiple sessions.
SummaryAttributes:
  - requestParameters.target
Tags:
    - AWS CloudTrail
    - AWS SSM
    - 'Execution: Exploitation for Client Execution'
    - Beta

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query AWS SSM Multiple Sessions; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Identify the instances for which sessions were started. Monitor instance and session activity. If possible, reach out to the user to determine the reason for multiple sessions.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "c61c9a5d-4d9c-45ee-8c46-9845ea001e97",
  "eventName": "StartSession",
  "eventSource": "ssm.amazonaws.com",
  "eventTime": "2025-02-19 20:13:49",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "p_event_time": "2025-02-19 20:13:49",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2025-02-19 20:15:54.4",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "8e6cef5f-2610-43fb-898b-48acd3aa6240",
  "requestParameters": {
    "target": "i-047fce8bf4806e5ee"
  },
  "responseElements": {
    "sessionId": "bobson.dugnutt-4njnyxxl8yn676nsla8j6l4bra",
    "tokenValue": "Value hidden due to security reasons."
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "sample-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY_ID",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
    "principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
    "sessionContext": {
      "attributes": {
        "creationDate": "2025-02-19T16:29:24Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS SSM Multiple Sessions

#
Tags
AWS CloudTrail, AWS SSM, AWS EC2
Source
github.com/panther-labs/panther-analysis

Returns StartSession events by users who triggered more than 2 StartSession events over the past hour.

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "AWS SSM Multiple Sessions"
Enabled: false
Description: >
  Returns StartSession events by users who triggered more than 2 StartSession events over the past hour.
Tags:
    - AWS CloudTrail
    - AWS SSM
    - AWS EC2
SnowflakeQuery: |-
  select * from panther_logs.public.aws_cloudtrail
  where eventName = 'StartSession'
  and p_occurs_since(1h, , p_parse_time)
  qualify count(distinct requestParameters:target) over (partition by userIdentity:arn) > 2

DatabricksQuery: |-
  SELECT * FROM (
    SELECT
      *,
      count(distinct requestParameters:target) over (partition by userIdentity:arn) AS session_count
    FROM panther_logs.aws_cloudtrail
    WHERE eventName = 'StartSession'
      AND p_occurs_since(1h, , p_parse_time)
  ) WHERE session_count > 2
Schedule:
  RateMinutes: 60
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
panther_logs.public.aws_cloudtrail

Stage 2: filter

  • eventName is StartSession
Window
1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

AWS SSO Access Token Retrieved by Unauthenticated IP

#
Severity
medium
Time window
30h
Match by
sourceIPAddress
Reference
blog.christophetd.fr
Source
github.com/panther-labs/panther-analysis

When using AWS in an enterprise environment, best practices dictate to use a single sign-on service for identity and access management. AWS SSO is a popular solution, integrating with third-party providers such as Okta and allowing to centrally manage roles and permissions in multiple AWS accounts. In this post, we demonstrate that AWS SSO is vulnerable by design to device code authentication phishing – just like any identity provider implementing OpenID Connect device code authentication. This technique was first demonstrated by Dr. Nestori Syynimaa for Azure AD. The feature provides a powerful phishing vector for attackers, rendering ineffective controls such as MFA (including Yubikeys) or IP allow-listing at the IdP level.

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.SSO.Access.Token.Retrieved.by.Unauthenticated.IP.Group"
DisplayName: "AWS SSO Access Token Retrieved by Unauthenticated IP"
Enabled: false
Severity: Medium
Description: |-
    When using AWS in an enterprise environment, best practices dictate to use a single sign-on service for identity and access management. AWS SSO is a popular solution, integrating with third-party providers such as Okta and allowing to centrally manage roles and permissions in multiple AWS accounts.

    In this post, we demonstrate that AWS SSO is vulnerable by design to device code authentication phishing – just like any identity provider implementing OpenID Connect device code authentication. This technique was first demonstrated by Dr. Nestori Syynimaa for Azure AD. The feature provides a powerful phishing vector for attackers, rendering ineffective controls such as MFA (including Yubikeys) or IP allow-listing at the IdP level.
Reference: https://blog.christophetd.fr/phishing-for-aws-credentials-via-aws-sso-device-code-authentication/
Detection:
    - Group:
        - ID: Absent CLI Prompt
          RuleID: Sign-in.with.AWS.CLI.prompt
          Absence: true
        - ID: SSO Access Token Retrieved
          RuleID: Retrieve.SSO.access.token
      MatchCriteria:
        field_name:
          - GroupID: Absent CLI Prompt
            Match: sourceIPAddress
          - GroupID: SSO Access Token Retrieved
            Match: sourceIPAddress
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5
      LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by sourceIPAddress. Each step needs one match unless a higher minimum is shown.

Stage 1: step Absent CLI Prompt (negated)

References detection SIGNAL - Sign-in with AWS CLI prompt.

Stage 2: step SSO Access Token Retrieved

References detection SIGNAL - Retrieve SSO access token.

AWS STS GetCallerIdentity via TruffleHog

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, AWS STS, Discovery:Account Discovery
Reference
github.com
Source
github.com/panther-labs/panther-analysis

Detects AWS STS GetCallerIdentity calls made by TruffleHog, a credential scanning tool. Threat actors use TruffleHog to validate whether leaked or stolen AWS access keys are still active. A GetCallerIdentity call with a TruffleHog user agent indicates that credentials from this account have been discovered externally and are being tested for validity.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return (
        event.get("eventSource") == "sts.amazonaws.com"
        and event.get("eventName") == "GetCallerIdentity"
        and "trufflehog" in event.get("userAgent", "").lower()
    )


def title(event):
    arn = event.deep_get("userIdentity", "arn", default="<unknown>")
    ip_addr = event.get("sourceIPAddress", "<unknown>")
    return f"TruffleHog credential validation detected from [{ip_addr}] as [{arn}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_sts_getcalleridentity_trufflehog.py
RuleID: "AWS.STS.GetCallerIdentity.TruffleHog"
DisplayName: "AWS STS GetCallerIdentity via TruffleHog"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - AWS STS
  - Discovery:Account Discovery
Reports:
  MITRE ATT&CK:
    - TA0007:T1087.004
Severity: Medium
Description: >
  Detects AWS STS GetCallerIdentity calls made by TruffleHog, a credential scanning
  tool. Threat actors use TruffleHog to validate whether leaked or stolen AWS access
  keys are still active. A GetCallerIdentity call with a TruffleHog user agent
  indicates that credentials from this account have been discovered externally and
  are being tested for validity.
Runbook: |
  1. Query CloudTrail for all API calls using the same userIdentity:accessKeyId in the 24 hours before and after this alert to assess if the credential has been used for unauthorized actions
  2. Check if sourceIPAddress appears in threat intelligence feeds or is associated with known scanning infrastructure
  3. Find all other alerts associated with this userIdentity:arn in the past 7 days to determine the scope of potential credential compromise
Reference: https://github.com/trufflesecurity/trufflehog
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is sts.amazonaws.com
  • eventName is GetCallerIdentity
  • userAgent contains trufflehog (case-insensitive)

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

1. Query CloudTrail for all API calls using the same userIdentity:accessKeyId in the 24 hours before and after this alert to assess if the credential has been used for unauthorized actions

2. Check if sourceIPAddress appears in threat intelligence feeds or is associated with known scanning infrastructure

3. Find all other alerts associated with this userIdentity:arn in the past 7 days to determine the scope of potential credential compromise

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventName": "GetCallerIdentity",
  "eventSource": "sts.amazonaws.com",
  "eventTime": "2024-01-15T10:30:00Z",
  "eventType": "AwsApiCall",
  "recipientAccountId": "123456789012",
  "sourceIPAddress": "198.51.100.42",
  "userAgent": "TruffleHog",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/compromised-user",
    "type": "IAMUser",
    "userName": "compromised-user"
  }
}

AWS STS GetSessionToken by IAM User

#
Status
Experimental
Severity
low
Log types
AWS.CloudTrail
Tags
AWS, AWS STS, Lateral Movement:Use Alternate Authentication Material, Privilege Escalation:Abuse Elevation Control Mechanism
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects an IAM user calling STS GetSessionToken to obtain temporary credentials. Attackers who have compromised long-term IAM credentials may use GetSessionToken to generate short-lived session tokens for lateral movement or to bypass IP-based policies that apply only to long-term credentials.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not aws_cloudtrail_success(event):
        return False
    return (
        event.get("eventSource") == "sts.amazonaws.com"
        and event.get("eventName") == "GetSessionToken"
        and event.deep_get("userIdentity", "type") == "IAMUser"
    )


def title(event):
    user = event.deep_get("userIdentity", "userName", default="<unknown>")
    account = event.get("recipientAccountId", "<unknown>")
    return f"IAM user [{user}] called GetSessionToken in account [{account}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_sts_getsessiontoken_misuse.py
RuleID: "AWS.STS.GetSessionToken.Misuse"
DisplayName: "AWS STS GetSessionToken by IAM User"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - AWS STS
  - Lateral Movement:Use Alternate Authentication Material
  - Privilege Escalation:Abuse Elevation Control Mechanism
Reports:
  MITRE ATT&CK:
    - TA0008:T1550.001
    - TA0004:T1548
Status: Experimental
Severity: Low
Description: >
  Detects an IAM user calling STS GetSessionToken to obtain temporary credentials.
  Attackers who have compromised long-term IAM credentials may use GetSessionToken
  to generate short-lived session tokens for lateral movement or to bypass IP-based
  policies that apply only to long-term credentials.
Runbook: |
  1. Query CloudTrail for all API calls by userIdentity:arn in the 6 hours before and after this alert to establish what the session token was used for
  2. Check if sourceIPAddress is associated with known corporate network ranges, VPN endpoints, or cloud provider IP ranges
  3. Find other alerts for this userIdentity:userName in the past 30 days to determine if GetSessionToken usage is part of normal workflow
Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is sts.amazonaws.com
  • eventName is GetSessionToken
  • userIdentity.type is IAMUser

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
userNameuserIdentity.userName

Response runbook

1. Query CloudTrail for all API calls by userIdentity:arn in the 6 hours before and after this alert to establish what the session token was used for

2. Check if sourceIPAddress is associated with known corporate network ranges, VPN endpoints, or cloud provider IP ranges

3. Find other alerts for this userIdentity:userName in the past 30 days to determine if GetSessionToken usage is part of normal workflow

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "abc-123",
  "eventName": "GetSessionToken",
  "eventSource": "sts.amazonaws.com",
  "eventTime": "2024-01-15T10:30:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "recipientAccountId": "123456789012",
  "requestID": "req-123",
  "sourceIPAddress": "203.0.113.50",
  "userAgent": "aws-cli/2.15.0",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/example-user",
    "principalId": "AIDACKCEVSQ6C2EXAMPLE",
    "type": "IAMUser",
    "userName": "example-user"
  }
}

AWS Trusted IPSet Modified

#
Severity
high
Entities
aws_instance_ids
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects creation and updates of the list of trusted IPs used by GuardDuty and WAF. Potentially to disable security alerts against malicious IPs.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

IPSET_ACTIONS = ["CreateIPSet", "UpdateIPSet"]


def rule(event):
    if (
        event.get("eventSource", "") == "guardduty.amazonaws.com"
        or event.get("eventSource", "") == "wafv2.amazonaws.com"
    ):
        if event.get("eventName", "") in IPSET_ACTIONS:
            return True
    return False


def title(event):
    return "IPSet was modified in " f"[{event.get('recipientAccountId','')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: "Detects creation and updates of the list of trusted IPs used by GuardDuty and WAF. Potentially to disable security alerts against malicious IPs."
DisplayName: "AWS Trusted IPSet Modified"
Enabled: true
Filename: aws_ipset_modified.py
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
Reference: https://docs.aws.amazon.com/managedservices/latest/ctref/management-monitoring-guardduty-ip-set-update-review-required.html
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.IPSet.Modified"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • any of:
    • eventSource is guardduty.amazonaws.com
    • eventSource is wafv2.amazonaws.com
  • eventName is one of CreateIPSet, UpdateIPSet

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CreateIPSet
  • UpdateIPSet
field:"aws::eventName" kind:in
eventSourceeq
  • guardduty.amazonaws.com
  • wafv2.amazonaws.com
field:"aws::eventSource" kind:eq

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsregion": "us-east-1",
  "eventid": "abc-123",
  "eventname": "CreateIPSet",
  "eventsource": "guardduty.amazonaws.com",
  "eventtime": "2022-07-17 04:50:23",
  "eventtype": "AwsApiCall",
  "eventversion": "1.08",
  "p_any_aws_instance_ids": [
    "testinstanceid"
  ],
  "p_event_time": "2022-07-17 04:50:23",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-07-17 04:55:11.788",
  "recipientAccountId": "123456789012"
}

AWS Unsuccessful MFA attempt

#
Severity
high
Log types
AWS.CloudTrail
Tags
Configuration Required
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

Monitor application logs for suspicious events including repeated MFA failures that may indicate user's primary credentials have been compromised.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    if (
        event.get("eventSource") != "signin.amazonaws.com"
        and event.get("eventName") != "ConsoleLogin"
    ):
        return False

    mfa_used = event.deep_get("additionalEventData", "MFAUsed", default="")
    console_login = event.deep_get("responseElements", "ConsoleLogin", default="")

    if mfa_used == "Yes" and console_login == "Failure":
        return True
    return False


def title(event):
    arn = event.deep_get("userIdenity", "arn", default="No ARN")
    username = event.deep_get("userIdentity", "userName", default="No Username")

    return f"Failed MFA login from [{arn}] [{username}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: Monitor application logs for suspicious events including repeated MFA failures that may indicate user's primary credentials have been compromised.
DisplayName: "AWS Unsuccessful MFA attempt"
Enabled: false
Filename: aws_cloudtrail_unsuccessful_mfa_attempt.py
Reference: https://attack.mitre.org/techniques/T1621/
Tags:
  - Configuration Required # configure threshold for multiple MFA failures
Reports:
  MITRE ATT&CK:
    - TA0006:T1621
Severity: High
DedupPeriodMinutes: 15
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.Unsuccessful.MFA.attempt"
Threshold: 2

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • any of:
    • eventSource is signin.amazonaws.com
    • eventName is ConsoleLogin
  • additionalEventData.MFAUsed is Yes
  • responseElements.ConsoleLogin is Failure
Alert cadence
alerts after 2 matches within 15m

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
eventNameneConsoleLoginexcludes:eventName field:"eventName" value:"ConsoleLogin"
eventSourcenesignin.amazonaws.comexcludes:eventSource field:"eventSource" value:"signin.amazonaws.com"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdenity.arn
userNameuserIdentity.userName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/home?state=hashArgs%23&isauthcode=true",
    "MFAUsed": "Yes",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-1",
  "errorMessage": "Failed authentication",
  "eventID": "d38ce1b3-4575-4cb8-a632-611b8243bfc3",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2022-11-10T16:24:34Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "recipientAccountId": "111122223333",
  "requestParameters": null,
  "responseElements": {
    "ConsoleLogin": "Failure"
  },
  "sourceIPAddress": "192.0.2.0",
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
  "userIdentity": {
    "accessKeyId": "",
    "accountId": "111122223333",
    "principalId": "AIDACKCEVSQ6C2EXAMPLE",
    "type": "IAMUser",
    "userName": "anaya"
  }
}

AWS Unused Access Key

#
Severity
low
Compliance
CIS 1.3; PCI 8.1.4
Resource types
AWS.IAM.User
Tags
AWS, Identity & Access Management
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that IAM user access keys are used at least once every 90 days.

Detection logic

import datetime

from panther_base_helpers import resolve_timestamp_string

TIMEOUT_DAYS = datetime.timedelta(days=90)
DEFAULT_TIME = "0001-01-01T00:00:00Z"


def aged_out(timestamp):
    if not timestamp:
        return False
    datetime_ts = resolve_timestamp_string(timestamp)

    if not datetime_ts:
        return True
    return (datetime.datetime.now() - datetime_ts) > TIMEOUT_DAYS


def policy(resource):
    # If a user is less than 4 hours old, it may not have a credential report generated yet.
    # It will be re-scanned periodically until a credential report is found, at which point this
    # policy will be properly evaluated.
    report = resource.get("CredentialReport")
    if not report:
        return True

    if report.get("AccessKey1Active"):
        if report.get("AccessKey1LastUsedDate") != DEFAULT_TIME and aged_out(
            report.get("AccessKey1LastUsedDate")
        ):
            return False
        if report.get("AccessKey1LastUsedDate") == DEFAULT_TIME and aged_out(
            report.get("AccessKey1LastRotated")
        ):
            return False
    if report.get("AccessKey2Active"):
        if report.get("AccessKey2LastUsedDate") != DEFAULT_TIME and aged_out(
            report.get("AccessKey2LastUsedDate")
        ):
            return False
        if report.get("AccessKey2LastUsedDate") == DEFAULT_TIME and aged_out(
            report.get("AccessKey2LastRotated")
        ):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_access_key_unused.py
PolicyID: "AWS.AccessKey.Unused"
DisplayName: "AWS Unused Access Key"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
Tags:
  - AWS
  - Identity & Access Management
Reports:
  CIS:
    - 1.3
  PCI:
    - 8.1.4
Severity: Low
Description: >
  This policy validates that IAM user access keys are used at least once every 90 days.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-used-every-90-days
Reference: >
  https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html

Stages and Predicates

Flags AWS.IAM.User resources when the condition below holds.

Condition

  • CredentialReport is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CredentialReportis_null(no value, null check)excludes:CredentialReport

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CredentialReportis_not_null
  • (no value, null check)
field:"CredentialReport" kind:is_not_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-access-keys-used-every-90-days

AWS User API Key Created

#
Severity
medium
Group by
userIdentity.arn
Compliance
Stratus Red Team aws.persistence.iam-backdoor-user, aws.persistence.iam-create-admin-user
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS API key creation for a user by another user. Backdoored users can be used to obtain persistence in the AWS environment.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "iam.amazonaws.com"
        and event.get("eventName") == "CreateAccessKey"
        and (
            not event.deep_get("userIdentity", "arn", default="").endswith(
                f"user/{event.deep_get('responseElements', 'accessKey', 'userName', default='')}"
            )
        )
    )


def title(event):
    return (
        f"[{event.deep_get('userIdentity','arn')}]"
        " created API keys for "
        f"[{event.deep_get('responseElements','accessKey','userName', default = '')}]"
    )


def dedup(event):
    return f"{event.deep_get('userIdentity','arn')}"


def alert_context(event):
    base = aws_rule_context(event)
    base["ip_accessKeyId"] = (
        event.get("sourceIpAddress", "<NO_IP_ADDRESS>")
        + ":"
        + event.deep_get(
            "responseElements", "accessKey", "accessKeyId", default="<NO_ACCESS_KEY_ID>"
        )
    )
    base["request_username"] = event.deep_get(
        "requestParameters", "userName", default="USERNAME_NOT_FOUND"
    )
    return base

Rule specification

AnalysisType: rule
Description: Detects AWS API key creation for a user by another user. Backdoored users can be used to obtain persistence in the AWS environment.
DisplayName: "AWS User API Key Created"
Enabled: true
Filename: aws_iam_user_key_created.py
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
    - TA0005:T1108
    - TA0005:T1550
    - TA0008:T1550

  Stratus Red Team:
    - aws.persistence.iam-backdoor-user
    - aws.persistence.iam-create-admin-user
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
Severity: Medium

DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.IAM.Backdoor.User.Keys"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is iam.amazonaws.com
  • eventName is CreateAccessKey

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn
userNameresponseElements.accessKey.userName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "12345",
  "eventName": "CreateAccessKey",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2022-09-27 17:09:18",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789",
  "requestParameters": {
    "userName": "user2"
  },
  "responseElements": {
    "accessKey": {
      "accessKeyId": "ABCDEFG",
      "createDate": "Sep 27, 2022 5:09:18 PM",
      "status": "Active",
      "userName": "user2"
    }
  },
  "sourceIPAddress": "cloudformation.amazonaws.com",
  "userAgent": "cloudformation.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "ABCDEFGH",
    "accountId": "123456789",
    "arn": "arn:aws:iam::123456789:user/user1",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "ABCDEFGH",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-27T17:08:35Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {},
      "webIdFederationData": {}
    },
    "type": "IAMUser",
    "userName": "user1"
  }
}

AWS User Login Profile Created or Modified

#
Severity
low
Compliance
Stratus Red Team aws.persistence.iam-create-user-login-profile, aws.privilege-escalation.iam-update-user-login-profile
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An attacker with iam:UpdateLoginProfile permission on other users can change the password used to login to the AWS console. May be legitimate account administration.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

PROFILE_EVENTS = {
    "UpdateLoginProfile",
    "CreateLoginProfile",
    "DeleteLoginProfile",
}


def rule(event):
    # Only look for successes
    if not aws_cloudtrail_success(event):
        return False

    # Check when someone other than the user themselves creates or modifies a login profile
    # with no password reset needed
    return (
        event.get("eventSource", "") == "iam.amazonaws.com"
        and event.get("eventName", "") in PROFILE_EVENTS
        and not event.deep_get("requestParameters", "passwordResetRequired", default=False)
        and not event.deep_get("userIdentity", "arn", default="").endswith(
            f"/{event.deep_get('requestParameters', 'userName', default='')}"
        )
    )


def title(event):
    return (
        f"[{event.deep_get('userIdentity', 'arn')}] "
        f"changed the password for "
        f"[{event.deep_get('requestParameters','userName')}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["ip_and_username"] = event.get(
        "sourceIPAddress", "<MISSING_SOURCE_IP>"
    ) + event.deep_get("requestParameters", "userName", default="<MISSING_USER_NAME>")
    return context

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_loginprofilecreatedormodified.py
RuleID: "AWS.CloudTrail.LoginProfileCreatedOrModified"
DisplayName: "AWS User Login Profile Created or Modified"
Enabled: true
LogTypes:
    - AWS.CloudTrail
Severity: Low

Reports:
  MITRE ATT&CK:
    - TA0003:T1098
    - TA0005:T1108
    - TA0005:T1550
    - TA0008:T1550

  Stratus Red Team:
    - aws.persistence.iam-create-user-login-profile
    - aws.privilege-escalation.iam-update-user-login-profile

Description: An attacker with iam:UpdateLoginProfile permission on other users can change the password used to login to the AWS console. May be legitimate account administration.
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-pass-accesskeys-ssh.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is iam.amazonaws.com
  • eventName is one of UpdateLoginProfile, CreateLoginProfile, DeleteLoginProfile
  • requestParameters.passwordResetRequired is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn
userNamerequestParameters.userName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "1234",
  "eventName": "UpdateLoginProfile",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2022-09-15 13:45:24",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "987654321",
  "requestParameters": {
    "passwordResetRequired": false,
    "userName": "bob"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ABC1234",
    "accountId": "987654321",
    "arn": "arn:aws:sts::98765432:assumed-role/IAM/alice",
    "principalId": "ABCDE:alice",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-15T13:36:47Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "987654321",
        "arn": "arn:aws:iam::9876432:role/IAM",
        "principalId": "1234ABC",
        "type": "Role",
        "userName": "IAM"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS User Takeover Via Password Reset

#
Severity
high
Time window
30h
Match by
p_alert_context.ip_and_username
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.User.Takeover.Via.Password.Reset.Group"
DisplayName: "AWS User Takeover Via Password Reset"
Enabled: false
Severity: High
Reports:
  MITRE ATT&CK:
    - TA0004:T1098.001 # Additional Cloud Credentials
Detection:
    - Group:
        - ID: Password Reset
          RuleID: AWS.CloudTrail.LoginProfileCreatedOrModified
        - ID: Login
          RuleID: AWS.Console.Login
      MatchCriteria:
        field_name:
          - GroupID: Password Reset
            Match: p_alert_context.ip_and_username
          - GroupID: Login
            Match: p_alert_context.ip_and_username
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 10
      LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.ip_and_username. Each step needs one match unless a higher minimum is shown.

Stage 1: step Password Reset

References detection AWS User Login Profile Created or Modified.

Stage 2: step Login

References detection AWS Console Login.

AWS VPC Default Network ACL Restricts All Traffic

#
Severity
low
Compliance
PCI 1.2.1
Resource types
AWS.EC2.VPC
Tags
AWS, PCI, Initial Access:External Remote Services
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the default Network ACL for a given AWS VPC is restricting all inbound and outbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import BadLookup, resource_lookup


def policy(resource):

    # pylint: disable=line-too-long
    default_id = f"arn:aws:ec2:{resource['Region']}:{resource['AccountId']}:network-acl/{resource['DefaultNetworkAclId']}"
    try:
        default_acl = resource_lookup(default_id)
    except BadLookup:
        return True
    return not default_acl["Entries"]

Rule specification

AnalysisType: policy
Filename: aws_vpc_default_network_acl_restricts_all_traffic.py
PolicyID: "AWS.VPC.DefaultNetworkACLRestrictsAllTraffic"
DisplayName: "AWS VPC Default Network ACL Restricts All Traffic"
Enabled: false
ResourceTypes:
  - AWS.EC2.VPC
Tags:
  - AWS
  - PCI
  - Initial Access:External Remote Services
Reports:
  PCI:
    - 1.2.1
  MITRE ATT&CK:
    - TA0001:T1133
Severity: Low
Description: >
  This policy validates that the default Network ACL for a given AWS VPC is restricting all inbound and outbound traffic.
Runbook: >
  Remove all entries allowing traffic from the default Network ACL in each VPC.
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html
#Tests:
#  -
#    Name: Default Network ACL Has IP Permissions
#    ExpectedResult: false
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": null,
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc123",
#                    "GroupName": null,
#                    "PeeringStatus": null,
#                    "UserId": "1122334455",
#                    "VpcId": null,
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "IpPermissionsEgress": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": [
#                  {
#                    "CidrIp": "0.0.0.0/0",
#                    "Description": null
#                  }
#                ],
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#            "Tags": {
#              "environment": "pci",
#            },
#        "VpcId": "vpc-1234321"
#      }
#  -
#    Name: Default Network ACL Has Inbound IP Permissions
#    ExpectedResult: false
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": null,
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc123",
#                    "GroupName": null,
#                    "PeeringStatus": null,
#                    "UserId": "1122334455",
#                    "VpcId": null,
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "IpPermissionsEgress": null,
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#            "Tags": {
#              "environment": "pci",
#            },
#        "VpcId": "vpc-1234321"
#      }
#  -
#    Name: Default Network ACL Has No IP Permissions
#    ExpectedResult: true
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": null,
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": null,
#            "IpPermissionsEgress": null,
#            "OwnerId": "112233445566",
#            "Tags": {
#              "environment": "pci",
#            },
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#            "Tags": {
#              "environment": "pci",
#            },
#        "VpcId": "vpc-1234321"
#      }

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Remove all entries allowing traffic from the default Network ACL in each VPC.

AWS VPC Default Security Group Restrictions

#
Severity
low
Compliance
CIS 4.3; PCI 1.2.1
Resource types
AWS.EC2.VPC
Tags
AWS, Security Control, Initial Access:External Remote Services
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that the default Security Group for a given AWS VPC is restricting all inbound and outbound traffic.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import BadLookup, resource_lookup


def policy(resource):
    # pylint: disable=line-too-long
    default_id = f"arn:aws:ec2:{resource['Region']}:{resource['AccountId']}:security-group/{resource['DefaultSecurityGroupId']}"
    try:
        default_sg = resource_lookup(default_id)
    except BadLookup:
        return True
    return default_sg["IpPermissions"] is None and default_sg["IpPermissionsEgress"] is None

Rule specification

AnalysisType: policy
Filename: aws_vpc_default_security_restrictions.py
PolicyID: "AWS.VPC.DefaultSecurityGroup.Restrictions"
DisplayName: "AWS VPC Default Security Group Restrictions "
Enabled: true
ResourceTypes:
  - AWS.EC2.VPC
Tags:
  - AWS
  - Security Control
  - Initial Access:External Remote Services
Reports:
  CIS:
    - 4.3
  PCI:
    - 1.2.1
  MITRE ATT&CK:
    - TA0001:T1133
Severity: Low
Description: >
  This policy validates that the default Security Group for a given AWS VPC is restricting all inbound and outbound traffic.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-vpc-default-security-group-restricts-all-traffic
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html
#Tests:
#  -
#    Name: Default Security Group Has IP Permissions
#    ExpectedResult: false
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": null,
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": null,
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc123",
#                    "GroupName": null,
#                    "PeeringStatus": null,
#                    "UserId": "1122334455",
#                    "VpcId": null,
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "IpPermissionsEgress": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": [
#                  {
#                    "CidrIp": "0.0.0.0/0",
#                    "Description": null
#                  }
#                ],
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#        "Tags": null,
#        "VpcId": "vpc-1234321"
#      }
#  -
#    Name: Default Security Group Has Inbound IP Permissions
#    ExpectedResult: false
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": null,
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": null,
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc123",
#                    "GroupName": null,
#                    "PeeringStatus": null,
#                    "UserId": "1122334455",
#                    "VpcId": null,
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "IpPermissionsEgress": null,
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#        "Tags": null,
#        "VpcId": "vpc-1234321"
#      }
#  -
#    Name: Default Security Group Has No IP Permissions
#    ExpectedResult: true
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": null,
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": null,
#            "IpPermissionsEgress": null,
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#        "Tags": null,
#        "VpcId": "vpc-1234321"
#      }
#  -
#    Name: Default Security Group Has Outbound IP Permissions
#    ExpectedResult: false
#    Resource:
#      {
#        "CidrBlock": "172.31.0.0/16",
#        "CidrBlockAssociationSet": [
#          {
#            "AssociationId": "vpc-cidr-assoc-112233",
#            "CidrBlock": "172.0.0.0/16",
#            "CidrBlockState": {
#              "State": "associated",
#              "StatusMessage": null
#            }
#          }
#        ],
#        "DhcpOptionsId": "dopt-1122e3344",
#        "FlowLogs": null,
#        "InstanceTenancy": "default",
#        "Ipv6CidrBlockAssociationSet": null,
#        "IsDefault": true,
#        "NetworkAcls": [
#          {
#            "Associations": [
#              {
#                "NetworkAclAssociationId": "aclassoc-1122abc444",
#                "NetworkAclId": "acl-123abc",
#                "SubnetId": "subnet-123abc"
#              },
#              {
#                "NetworkAclAssociationId": "aclassoc-123abc",
#                "NetworkAclId": "acl-1122abc",
#                "SubnetId": "subnet-112233aabb"
#              }
#            ],
#            "Entries": [
#              {
#                "CidrBlock": "0.0.0.0/0",
#                "Egress": false,
#                "IcmpTypeCode": null,
#                "Ipv6CidrBlock": null,
#                "PortRange": null,
#                "Protocol": "-1",
#                "RuleAction": "deny",
#                "RuleNumber": 12345
#              }
#            ],
#            "IsDefault": true,
#            "NetworkAclId": "acl-123aabb",
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-1234321"
#          }
#        ],
#        "OwnerId": "112233445566",
#        "RouteTables": [
#          {
#            "Associations": [
#              {
#                "Main": true,
#                "RouteTableAssociationId": "rtbassoc-1122aa33",
#                "RouteTableId": "rtb-1122abc33",
#                "SubnetId": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "PropagatingVgws": null,
#            "RouteTableId": "rtb-11223344",
#            "Routes": [
#              {
#                "DestinationCidrBlock": "0.0.0.0/0",
#                "DestinationIpv6CidrBlock": null,
#                "DestinationPrefixListId": null,
#                "EgressOnlyInternetGatewayId": null,
#                "GatewayId": "igw-abc123",
#                "InstanceId": null,
#                "InstanceOwnerId": null,
#                "NatGatewayId": null,
#                "NetworkInterfaceId": null,
#                "Origin": "CreateRoute",
#                "State": "active",
#                "TransitGatewayId": null,
#                "VpcPeeringConnectionId": null
#              }
#            ],
#            "Tags": null,
#            "VpcId": "vpc-11223344"
#          }
#        ],
#        "SecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc123",
#            "GroupName": "default",
#            "IpPermissions": null,
#            "IpPermissionsEgress": [
#              {
#                "FromPort": null,
#                "IpProtocol": "-1",
#                "IpRanges": [
#                  {
#                    "CidrIp": "0.0.0.0/0",
#                    "Description": null
#                  }
#                ],
#                "Ipv6Ranges": null,
#                "PrefixListIds": null,
#                "ToPort": null,
#                "UserIdGroupPairs": null
#              }
#            ],
#            "OwnerId": "112233445566",
#            "Tags": null,
#            "VpcId": "vpc-123454321"
#          }
#        ],
#        "StaleSecurityGroups": [
#          {
#            "Description": "default VPC security group",
#            "GroupId": "sg-abc567",
#            "GroupName": "default",
#            "StaleIpPermissions": null,
#            "StaleIpPermissionsEgress": [
#              {
#                "FromPort": 5555,
#                "IpProtocol": "tcp",
#                "IpRanges": null,
#                "PrefixListIds": null,
#                "ToPort": 5555,
#                "UserIdGroupPairs": [
#                  {
#                    "Description": null,
#                    "GroupId": "sg-abc567",
#                    "GroupName": "default",
#                    "PeeringStatus": "deleted",
#                    "UserId": "123456789012",
#                    "VpcId": "vpc-abc111222333444",
#                    "VpcPeeringConnectionId": null
#                  }
#                ]
#              }
#            ],
#            "VpcId": "vpc-abc123"
#          }
#        ],
#        "State": "available",
#        "Tags": null,
#        "VpcId": "vpc-1234321"
#      }

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-vpc-default-security-group-restricts-all-traffic

AWS VPC Flow Logs

#
Severity
medium
Compliance
CIS 2.9
Resource types
AWS.EC2.VPC
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that AWS VPCs (Virtual Private Clouds) have network flow logging enabled.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def policy(resource):
    if resource["FlowLogs"] is None:
        return False

    for flow in resource["FlowLogs"]:
        if flow["FlowLogStatus"] == "ACTIVE" and (
            flow["TrafficType"] == "REJECT" or flow["TrafficType"] == "ALL"
        ):
            return True

    return False

Rule specification

AnalysisType: policy
Filename: aws_vpc_flow_logs.py
PolicyID: "AWS.VPC.FlowLogs"
DisplayName: "AWS VPC Flow Logs"
Enabled: true
ResourceTypes:
  - AWS.EC2.VPC
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 2.9
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Medium
Description: >
  This policy validates that AWS VPCs (Virtual Private Clouds) have network flow logging enabled.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-vpc-flow-logging-enabled
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html

Stages and Predicates

Flags AWS.EC2.VPC resources when any of the conditions below holds.

Condition

  • any of:
    • FlowLogs is empty
    • no element of FlowLogs matches all of:
      • FlowLogs.FlowLogStatus is ACTIVE
      • any of:
        • FlowLogs.TrafficType is REJECT
        • FlowLogs.TrafficType is ALL

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
FlowLogsarray_any(no value, null check)excludes:FlowLogs
FlowLogsis_not_null(no value, null check)excludes:FlowLogs

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FlowLogsis_null
  • (no value, null check)
field:"FlowLogs" kind:is_null

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-vpc-flow-logging-enabled

AWS VPC Flow Logs Removed

#
Status
Experimental
Severity
informational
Compliance
Stratus Red Team aws.defense-evasion.vpc-remove-flow-logs
Log types
AWS.CloudTrail
Tags
AWS, Cloudtrail, Defense Evasion, Impair Defenses, Disable or Modify Cloud Logs, Defense Evasion:Impair Defenses, Security Control
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when logs for a VPC have been removed.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") == "DeleteFlowLogs"


def title(event):
    account = event.deep_get("userIdentity", "accountId", default="<UNKNOWN ACCOUNT>")
    region = event.get("awsRegion", "<UNKNOWN REGION>")
    return f"VPC Flow logs have been deleted in {account} in {region}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_flow_logs_deleted.py
RuleID: "AWS.VPCFlow.LogsDeleted"
DisplayName: "AWS VPC Flow Logs Removed"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Info
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.008 # Defense Evasion: Disable or Modify Cloud Logs

  Stratus Red Team:
    - aws.defense-evasion.vpc-remove-flow-logs
Description: "Detects when logs for a VPC have been removed."
Reference: 
  https://stratus-red-team.cloud/attack-techniques/AWS/aws.defense-evasion.vpc-remove-flow-logs/
Runbook: Look for an accompanying 'DeleteVpc' event, and confirm that they are related. if there is no matching VPC Deletion event, followup with the log removal to determine if it is legitimate.
Tags:
  - AWS
  - Cloudtrail
  - Defense Evasion
  - Impair Defenses
  - Disable or Modify Cloud Logs
  - Defense Evasion:Impair Defenses
  - Security Control
Status: Experimental

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is DeleteFlowLogs

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
accountIduserIdentity.accountId

Response runbook

Look for an accompanying 'DeleteVpc' event, and confirm that they are related. if there is no matching VPC Deletion event, followup with the log removal to determine if it is legitimate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "d5e6d49c-0be9-4c53-ab8a-c7ca86edd130",
  "eventName": "DeleteFlowLogs",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2024-11-26 19:29:38.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.10",
  "managementEvent": true,
  "p_event_time": "2024-11-26 19:29:38.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-11-26 19:35:54.358700257",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "1dcd7d36-72be-4aad-9e9d-93b88e0135ea",
  "requestParameters": {
    "DeleteFlowLogsRequest": {
      "FlowLogId": {
        "content": "fl-0ef673ef70c4f07cc",
        "tag": 1
      }
    }
  },
  "responseElements": {
    "DeleteFlowLogsResponse": {
      "requestId": "1dcd7d36-72be-4aad-9e9d-93b88e0135ea",
      "unsuccessful": "",
      "xmlns": "http://ec2.amazonaws.com/doc/2016-11-15/"
    }
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "ec2.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "sample-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins",
    "principalId": "SAMPLE_PRINCIPAL_ID:leroy.jenkins",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-11-26T17:05:50Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "SAMPLE_PRINCIPAL_ID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

AWS VPC Healthy Log Status

#
Severity
informational
Log types
AWS.VPCFlow, OCSF.NetworkActivity
Tags
AWS, DataModel, Security Control
Reference
www.reddit.com
Source
github.com/panther-labs/panther-analysis

Checks for the log status SKIPDATA, which indicates that data was lost either to an internal server error or due to capacity constraints.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return event.udm("log_status") == "SKIPDATA"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_healthy_log_status.py
RuleID: "AWS.VPC.HealthyLogStatus"
DisplayName: "AWS VPC Healthy Log Status"
Enabled: true
LogTypes:
  - AWS.VPCFlow
  - OCSF.NetworkActivity
Tags:
  - AWS
  - DataModel
  - Security Control
Severity: Info
CreateAlert: false
DedupPeriodMinutes: 1440
Description: >
  Checks for the log status `SKIPDATA`, which indicates that data was lost either to an internal server error or due to capacity constraints.
Reference: https://www.reddit.com/r/aws/comments/zt1xhg/vpc_flow_logs_when_is_logstatus_skipdata_a_concern/?rdt=41505
Runbook: >
  Determine if the cause of the issue is capacity constraints, and consider adjusting VPC Flow Log configurations accordingly.

Stages and Predicates

Fires on AWS.VPCFlow, OCSF.NetworkActivity events when the condition below holds.

Condition

  • log_status is SKIPDATA
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Determine if the cause of the issue is capacity constraints, and consider adjusting VPC Flow Log configurations accordingly.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "p_log_type": "AWS.VPCFlow",
  "status": "SKIPDATA"
}

AWS WAF Disassociation

#
Severity
critical
Entities
aws_account_ids, aws_arns, domain_names, trace_ids
Log types
AWS.CloudTrail
Tags
AWS, Web Application Firewall, Defense Evasion, Impair Defenses, Network Security
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when AWS WAF is disassociated from protected resources such as Application Load Balancers, API Gateway, CloudFront, or AppSync. Removing WAF protection exposes applications to SQL injection, XSS, DDoS attacks, and OWASP Top 10 vulnerabilities. Attackers often disable WAF before launching attacks, or this may indicate misconfiguration or unauthorized changes.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def rule(event):
    return event.get("eventName") == "DisassociateWebACL"


def title(event):
    return (
        f"AWS Account ID [{event.get('recipientAccountId')}] "
        f"disassociated WebACL [{event.deep_get('requestParameters', 'resourceArn')}]"
    )


def alert_context(event):
    return {
        "awsRegion": event.get("awsRegion"),
        "eventName": event.get("eventName"),
        "recipientAccountId": event.get("recipientAccountId"),
        "requestID": event.get("requestID"),
        "actor_user": event.udm("actor_user"),
        "requestParameters": event.deep_get("requestParameters", "resourceArn"),
        "userIdentity": event.deep_get("userIdentity", "principalId"),
    }

Rule specification

AnalysisType: rule
Description: >
  Detects when AWS WAF is disassociated from protected resources such as Application Load Balancers, API Gateway, CloudFront, or AppSync. Removing WAF protection exposes applications to SQL injection, XSS, DDoS attacks, and OWASP Top 10 vulnerabilities. Attackers often disable WAF before launching attacks, or this may indicate misconfiguration or unauthorized changes.
DisplayName: "AWS WAF Disassociation"
Enabled: true
Filename: aws_waf_disassociation.py
Reference: https://docs.aws.amazon.com/waf/latest/APIReference/API_DisassociateWebACL.html
Runbook: |
  1. Query ALB access logs, CloudFront logs, or API Gateway logs for requestParameters.resourceArn in the timeframe between this DisassociateWebACL event and WAF re-association to identify attack traffic that reached the unprotected resource
  2. Search for SQL injection patterns, XSS payloads, or unusual request volumes in the application logs that may indicate exploitation while WAF protection was removed
  3. Review CloudTrail for other security-related API calls by userIdentity.arn in the 6 hours around this event to identify if other defensive controls were modified or disabled
Severity: Critical
Tags:
  - AWS
  - Web Application Firewall
  - Defense Evasion
  - Impair Defenses
  - Network Security
Reports:
  MITRE ATT&CK:
    - TA0005:T1562.004
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.WAF.Disassociation"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when the condition below holds.

Condition

  • eventName is DisassociateWebACL

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
awsRegion
eventName
recipientAccountId
requestID
actor_user
requestParametersrequestParameters.resourceArn
userIdentityuserIdentity.principalId

Response runbook

1. Query ALB access logs, CloudFront logs, or API Gateway logs for requestParameters.resourceArn in the timeframe between this DisassociateWebACL event and WAF re-association to identify attack traffic that reached the unprotected resource

2. Search for SQL injection patterns, XSS payloads, or unusual request volumes in the application logs that may indicate exploitation while WAF protection was removed

3. Review CloudTrail for other security-related API calls by userIdentity.arn in the 6 hours around this event to identify if other defensive controls were modified or disabled

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "apiVersion": "2019-04-23",
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "94678efc-2176-462c-b0c9-a612881a39ed",
  "eventName": "DisassociateWebACL",
  "eventSource": "wafv2.amazonaws.com",
  "eventTime": "2022-09-29 23:04:35",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_aws_account_ids": [
    "012345678910"
  ],
  "p_any_aws_arns": [
    "arn:aws:elasticloadbalancing:us-west-2:012345678910:loadbalancer/app/web/84dc5457e450dba5",
    "arn:aws:iam::012345678910:role/DevAdministrator",
    "arn:aws:sts::012345678910:assumed-role/DevAdministrator/example_user"
  ],
  "p_any_domain_names": [
    "AWS Internal"
  ],
  "p_any_trace_ids": [
    "ASIARLIVEKVNJNSTUSF6"
  ],
  "p_event_time": "2022-09-29 23:04:35",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2022-09-29 23:08:26.172",
  "p_row_id": "5ad3a83ca88f938cbff8fdd913d1ce1d",
  "p_source_id": "125a8146-e3ea-454b-aed7-9e08e735b670",
  "p_source_label": "Panther Identity Org CloudTrail",
  "readOnly": false,
  "recipientAccountId": "012345678910",
  "requestID": "e4d47992-90f1-47f0-bff7-de18a8277005",
  "requestParameters": {
    "resourceArn": "arn:aws:elasticloadbalancing:us-west-2:012345678910:loadbalancer/app/web/84dc5457e450dba5"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ASIARLIVEKVNJNSTUSF6",
    "accountId": "012345678910",
    "arn": "arn:aws:sts::012345678910:assumed-role/DevAdministrator/example_user",
    "principalId": "AROARLIVEKVNIRVGDLJWJ:example_user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-29T22:51:13Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "012345678910",
        "arn": "arn:aws:iam::012345678910:role/DevAdministrator",
        "principalId": "AROARLIVEKVNIRVGDLJWJ",
        "type": "Role",
        "userName": "DevAdministrator"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

AWS WAF Has XSS Predicate

#
Severity
medium
Compliance
PCI 6.5.7
Resource types
AWS.WAF.Regional.WebACL, AWS.WAF.WebACL
Tags
AWS, PCI, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all WAF's have at least one rule with a predicate matching on and blocking XSS attacks.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_base_helpers import deep_get


def policy(resource):

    for rule in resource["Rules"] or []:
        # Must block the XSS
        if deep_get(rule, "Action", "Type") != "BLOCK":
            continue

        # Only passes if there is an XSS matching predicate
        for predicate in rule["Predicates"]:
            if predicate["Type"] == "XssMatch":
                return True

    return False

Rule specification

AnalysisType: policy
Filename: aws_waf_has_xss_predicate.py
PolicyID: "AWS.WAF.HasXSSPredicate"
DisplayName: "AWS WAF Has XSS Predicate"
Enabled: false
ResourceTypes:
  - AWS.WAF.Regional.WebACL
  - AWS.WAF.WebACL
Tags:
  - AWS
  - PCI
  - Initial Access:Exploit Public-Facing Application
Reports:
  PCI:
    - 6.5.7
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  This policy validates that all WAF's have at least one rule with a predicate matching on and blocking XSS attacks.
Runbook: >
  Configure a web ACL rule with a XSS matching predicate and add it to the WAF.
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-xss-conditions.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Configure a web ACL rule with a XSS matching predicate and add it to the WAF.

AWS WAF Logging Configured

#
Severity
high
Compliance
PCI 10.5.5
Resource types
AWS.WAF.Regional.WebACL, AWS.WAF.WebACL
Tags
AWS, Monitoring, Logging, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that AWS WAF logging is enabled and that the logs are being sent to a valid destination (S3, CloudWatch, or Kinesis Firehose). Without logging, visibility into WAF activity is severely limited, increasing the risk of undetected attacks.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Detection logic

def is_valid_arn(arn, service):
    if service == "logs":
        return arn.startswith("arn:aws:logs:") and ":log-group:" in arn
    if service == "s3":
        return arn.startswith("arn:aws:s3:::") and len(arn.split(":")) == 6
    if service == "firehose":
        return arn.startswith("arn:aws:firehose:") and ":deliverystream/" in arn
    return False


def policy(resource):
    # Check if WAF logging configuration exists
    logging_config = resource.get("LoggingConfiguration")
    if not logging_config:
        return False

    # Get the logging destinations
    destinations = logging_config.get("LogDestinationConfigs", [])

    # Validate the ARNs for CloudWatch Logs, S3, or Kinesis Firehose
    for destination in destinations:
        if (
            is_valid_arn(destination, "logs")
            or is_valid_arn(destination, "s3")
            or is_valid_arn(destination, "firehose")
        ):
            return True

    return False

Rule specification

AnalysisType: policy
Filename: aws_waf_logging_configured.py
PolicyID: "AWS.WAF.LoggingConfigured"
DisplayName: "AWS WAF Logging Configured"
Enabled: true
ResourceTypes:
  - AWS.WAF.Regional.WebACL
  - AWS.WAF.WebACL
Tags:
  - AWS
  - Monitoring
  - Logging
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  PCI:
    - 10.5.5
  MITRE ATT&CK:
    - TA0005:T1562
Severity: High
Description: >
  Ensures that AWS WAF logging is enabled and that the logs are being sent to a valid destination (S3, CloudWatch, or Kinesis Firehose). Without logging, visibility into WAF activity is severely limited, increasing the risk of undetected attacks.
Runbook: >
  Ensure AWS WAF logging is configured to at least one valid destination such as an Amazon S3 bucket, Amazon CloudWatch Logs, or Amazon Kinesis Data Firehose. Refer to the AWS WAF logging documentation for setup instructions.
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/logging.html

Stages and Predicates

Flags AWS.WAF.Regional.WebACL, AWS.WAF.WebACL resources when the condition below holds.

Condition

  • LoggingConfiguration is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
LoggingConfigurationis_not_null(no value, null check)excludes:LoggingConfiguration

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LoggingConfigurationis_null
  • (no value, null check)
field:"LoggingConfiguration" kind:is_null

Response runbook

Ensure AWS WAF logging is configured to at least one valid destination such as an Amazon S3 bucket, Amazon CloudWatch Logs, or Amazon Kinesis Data Firehose. Refer to the AWS WAF logging documentation for setup instructions.

AWS WAF Managed Admin Protection Passthrough Rule

#
Severity
high
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF Admin Protection managed rule group matches. Blocks external access to exposed administrative pages such as /admin, /wp-admin, and similar paths.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesAdminProtectionRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF Admin Protection: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_admin_protection.py
RuleID: "AWS.WAF.Managed.AdminProtection"
DisplayName: "AWS WAF Managed Admin Protection Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Initial Access:Exploit Public-Facing Application
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
    - TA0007:T1083
Severity: High
Description: >
  Detects AWS WAF Admin Protection managed rule group matches. Blocks external access to exposed
  administrative pages such as /admin, /wp-admin, and similar paths.
Runbook: |
  1. Find all requests from httpRequest:clientIp targeting admin-related URI paths in the 6 hours before and after this alert
  2. Check if httpRequest:clientIp is associated with known corporate network ranges, VPNs, or threat intelligence feeds
  3. Search for successful authentication events or other WAF alerts from httpRequest:clientIp across all httpSourceIds in the past 24 hours
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-admin

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all requests from httpRequest:clientIp targeting admin-related URI paths in the 6 hours before and after this alert

2. Check if httpRequest:clientIp is associated with known corporate network ranges, VPNs, or threat intelligence feeds

3. Search for successful authentication events or other WAF alerts from httpRequest:clientIp across all httpSourceIds in the past 24 hours

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "GET",
    "uri": "/wp-admin/"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesAdminProtectionRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed Anti-DDoS Passthrough Rule

#
Severity
low
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, DDoS, Impact:Endpoint Denial of Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF Anti-DDoS managed rule group matches. Rules include ChallengeAllDuringEvent, ChallengeDDoSRequests, and DDoSRequests which activate during detected DDoS events to challenge or block suspicious traffic to protected resources.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesAntiDDoSRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF Anti-DDoS: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_anti_ddos.py
RuleID: "AWS.WAF.Managed.AntiDDoS"
DisplayName: "AWS WAF Managed Anti-DDoS Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - DDoS
  - Impact:Endpoint Denial of Service
Reports:
  MITRE ATT&CK:
    - TA0040:T1499
Severity: Low
Description: >
  Detects AWS WAF Anti-DDoS managed rule group matches. Rules include ChallengeAllDuringEvent,
  ChallengeDDoSRequests, and DDoSRequests which activate during detected DDoS events to challenge
  or block suspicious traffic to protected resources.
Runbook: |
  1. Find all requests from httpRequest:clientIp and its /24 CIDR range in the 1 hour before and after this alert to assess traffic volume and patterns
  2. Correlate with AWS Shield Advanced events and other anti-DDoS alerts targeting the same httpSourceId in the past 6 hours to determine if this is part of an active DDoS event
  3. Check if httpRequest:clientIp appears in threat intelligence feeds associated with known DDoS botnets or attack tools
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-anti-ddos.html

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all requests from httpRequest:clientIp and its /24 CIDR range in the 1 hour before and after this alert to assess traffic volume and patterns

2. Correlate with AWS Shield Advanced events and other anti-DDoS alerts targeting the same httpSourceId in the past 6 hours to determine if this is part of an active DDoS event

3. Check if httpRequest:clientIp appears in threat intelligence feeds associated with known DDoS botnets or attack tools

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "GET",
    "uri": "/"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesAntiDDoSRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed Bot Control Passthrough Rule

#
Severity
informational
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Bot Detection, Reconnaissance
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF Bot Control managed rule group matches. Covers automated browser detection, HTTP library user agents, scraping frameworks, known bot data centers, and targeted bot protections including token abuse and coordinated activity.

MITRE ATT&CK coverage

TacticTechniques
Reconnaissance

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesBotControlRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF Bot Control: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_bot_control.py
RuleID: "AWS.WAF.Managed.BotControl"
DisplayName: "AWS WAF Managed Bot Control Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Bot Detection
  - Reconnaissance
Reports:
  MITRE ATT&CK:
    - TA0043:T1595
Severity: Info
Description: >
  Detects AWS WAF Bot Control managed rule group matches. Covers automated browser detection,
  HTTP library user agents, scraping frameworks, known bot data centers, and targeted bot
  protections including token abuse and coordinated activity.
Runbook: |
  1. Find all requests from httpRequest:clientIp in the 24 hours before and after this alert to determine request volume and targeted URI patterns
  2. Check if the user agent string and httpRequest:clientIp are associated with legitimate bot services (search engines, monitoring) or known malicious automation
  3. Search for other WAF bot control alerts from the same httpRequest:clientIp or user agent in the past 7 days to identify persistent automated activity
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-bot.html

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all requests from httpRequest:clientIp in the 24 hours before and after this alert to determine request volume and targeted URI patterns

2. Check if the user agent string and httpRequest:clientIp are associated with legitimate bot services (search engines, monitoring) or known malicious automation

3. Search for other WAF bot control alerts from the same httpRequest:clientIp or user agent in the past 7 days to identify persistent automated activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "GET",
    "uri": "/api/data"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesBotControlRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed Core Rule Set Passthrough Rule

#
Severity
medium
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF Core Rule Set (CRS) managed rule group matches. Covers XSS, LFI, RFI, SSRF, size restrictions, restricted extensions, and bad bot user agents across all WAF sources.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesCommonRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF Core Rule Set: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_core_rule_set.py
RuleID: "AWS.WAF.Managed.CoreRuleSet"
DisplayName: "AWS WAF Managed Core Rule Set Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Initial Access:Exploit Public-Facing Application
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
Severity: Medium
Description: >
  Detects AWS WAF Core Rule Set (CRS) managed rule group matches. Covers XSS, LFI, RFI, SSRF,
  size restrictions, restricted extensions, and bad bot user agents across all WAF sources.
Runbook: |
  1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify attack patterns or scanning behavior
  2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known proxy/VPN services
  3. Search for other WAF alerts targeting the same httpRequest:uri or httpSourceId in the past 7 days to determine if this is part of a broader campaign
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify attack patterns or scanning behavior

2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known proxy/VPN services

3. Search for other WAF alerts targeting the same httpRequest:uri or httpSourceId in the past 7 days to determine if this is part of a broader campaign

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "POST",
    "uri": "/api/endpoint"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesCommonRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed IP Reputation Passthrough Rule

#
Severity
informational
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Initial Access:Exploit Public-Facing Application, Reconnaissance, Defense Evasion
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF IP Reputation and Anonymous IP List managed rule group matches. Flags requests from IPs on Amazon threat intelligence lists including known bots, reconnaissance sources, DDoS participants, TOR nodes, temporary proxies, and hosting/cloud provider IPs.

MITRE ATT&CK coverage

TacticTechniques
Reconnaissance
Initial Access
Command & Control

Detection logic

import re

from panther_aws_helpers import waf_alert_context, waf_get_matched_rule, waf_rule_group_matches

STATIC_ASSET_PATTERN = re.compile(
    r"\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|ico|map)$", re.IGNORECASE
)

RULE_GROUPS = [
    "AWSManagedRulesAmazonIpReputationList",
    "AWSManagedRulesAnonymousIpList",
]


def rule(event):
    if not waf_rule_group_matches(event, RULE_GROUPS):
        return False

    # Skip requests to static assets
    uri = event.deep_get("httpRequest", "uri", default="")

    if STATIC_ASSET_PATTERN.search(uri):
        return False

    return True


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUPS)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF IP Reputation: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUPS)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_ip_reputation.py
RuleID: "AWS.WAF.Managed.IPReputation"
DisplayName: "AWS WAF Managed IP Reputation Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Initial Access:Exploit Public-Facing Application
  - Reconnaissance
  - Defense Evasion
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
    - TA0043:T1595
    - TA0005:T1090
Severity: Info
Description: >
  Detects AWS WAF IP Reputation and Anonymous IP List managed rule group matches. Flags requests
  from IPs on Amazon threat intelligence lists including known bots, reconnaissance sources, DDoS
  participants, TOR nodes, temporary proxies, and hosting/cloud provider IPs.
Runbook: |
  1. Find all requests from httpRequest:clientIp across all WAF sources in the 24 hours before and after this alert to assess the scope of activity
  2. Check if httpRequest:clientIp is associated with known threat actors, botnets, or DDoS infrastructure in threat intelligence feeds
  3. Search for other alerts from httpRequest:clientIp across all detection rules in the past 7 days to identify correlated malicious behavior
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all requests from httpRequest:clientIp across all WAF sources in the 24 hours before and after this alert to assess the scope of activity

2. Check if httpRequest:clientIp is associated with known threat actors, botnets, or DDoS infrastructure in threat intelligence feeds

3. Search for other alerts from httpRequest:clientIp across all detection rules in the past 7 days to identify correlated malicious behavior

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "GET",
    "uri": "/"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesAmazonIpReputationList",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed Known Bad Inputs Passthrough Rule

#
Severity
high
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Initial Access:Exploit Public-Facing Application, Execution:Command and Scripting Interpreter
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF Known Bad Inputs managed rule group matches. Covers Log4Shell (CVE-2021-44228), Java deserialization RCE, localhost Host header abuse, PROPFIND method, and exploitable paths.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesKnownBadInputsRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF Known Bad Inputs: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_known_bad_inputs.py
RuleID: "AWS.WAF.Managed.KnownBadInputs"
DisplayName: "AWS WAF Managed Known Bad Inputs Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Initial Access:Exploit Public-Facing Application
  - Execution:Command and Scripting Interpreter
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
    - TA0002:T1059
Severity: High
Description: >
  Detects AWS WAF Known Bad Inputs managed rule group matches. Covers Log4Shell (CVE-2021-44228),
  Java deserialization RCE, localhost Host header abuse, PROPFIND method, and exploitable paths.
Runbook: |
  1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify exploitation attempts across multiple endpoints
  2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known exploit infrastructure
  3. If the action was ALLOW, search application and backend server logs for the targeted httpRequest:uri in the 1 hour after the alert to determine if exploitation was successful
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-known-bad-inputs

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify exploitation attempts across multiple endpoints

2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known exploit infrastructure

3. If the action was ALLOW, search application and backend server logs for the targeted httpRequest:uri in the 1 hour after the alert to determine if exploitation was successful

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "POST",
    "uri": "/api/endpoint"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesKnownBadInputsRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Managed SQL Database Passthrough Rule

#
Severity
high
Log types
AWS.WAFWebACL
Tags
AWS, WAF, Managed Rules, Initial Access:Exploit Public-Facing Application
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF SQL Database managed rule group matches. Covers SQL injection patterns in query arguments, request body, cookies, and URI path, including extended patterns not covered by the Core Rule Set.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Detection logic

from panther_aws_helpers import (
    waf_alert_context,
    waf_get_matched_rule,
    waf_rule_group_matches,
    waf_severity,
)

RULE_GROUP = "AWSManagedRulesSQLiRuleSet"


def rule(event):
    return waf_rule_group_matches(event, RULE_GROUP)


def title(event):
    matched = waf_get_matched_rule(event, RULE_GROUP)
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF SQL Database: {matched} - {action} from {client_ip} via {source}"


def alert_context(event):
    return waf_alert_context(event, RULE_GROUP)


def severity(event):
    return waf_severity(event)

Rule specification

AnalysisType: rule
Filename: aws_waf_managed_sql_database.py
RuleID: "AWS.WAF.Managed.SQLDatabase"
DisplayName: "AWS WAF Managed SQL Database Passthrough Rule"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - Managed Rules
  - Initial Access:Exploit Public-Facing Application
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
Severity: High
Description: >
  Detects AWS WAF SQL Database managed rule group matches. Covers SQL injection patterns in
  query arguments, request body, cookies, and URI path, including extended patterns not covered
  by the Core Rule Set.
Runbook: |
  1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify SQL injection attempts across multiple endpoints
  2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known automated scanning tools
  3. If the action was ALLOW, search application and database logs for the targeted httpRequest:uri in the 1 hour after the alert to determine if injection was successful
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when the condition below holds.

Condition

  • any element of ruleGroupList matches:
    • any of:
      • ruleGroupList.terminatingRule.ruleId is present
      • any element of ruleGroupList.nonTerminatingMatchingRules matches:
        • ruleGroupList.nonTerminatingMatchingRules.ruleId is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
matched_ruleterminatingRuleId
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Find all WAF log entries from httpRequest:clientIp in the 6 hours before and after this alert to identify SQL injection attempts across multiple endpoints

2. Check if httpRequest:clientIp appears in threat intelligence feeds or is associated with known automated scanning tools

3. If the action was ALLOW, search application and database logs for the targeted httpRequest:uri in the 1 hour after the alert to determine if injection was successful

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "GET",
    "uri": "/api/search"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "AWS-AWSManagedRulesSQLiRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF ReactJS RCE Attempt via Body

#
Severity
high
Log types
AWS.WAFWebACL
Tags
AWS, WAF, React2Shell, Initial Access:Exploit Public-Facing Application, Execution:Command and Scripting Interpreter
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects AWS WAF ReactJSRCE_BODY managed rule matches indicating React2Shell (CVE-2025-55182) ReactJS RCE attempts via HTTP body. Monitors all WAF sources: ALB, CloudFront, API Gateway, AppSync.

MITRE ATT&CK coverage

Detection logic

RULE_ID = "ReactJSRCE_BODY"


def rule(event):
    # Direct check of terminating rule ID
    if RULE_ID in event.get("terminatingRuleId", ""):
        return True

    # Check non-terminating rules
    for matching_rule in event.get("nonTerminatingMatchingRules", []) or []:
        if RULE_ID in matching_rule.get("ruleId", ""):
            return True

    # Check rule groups
    for group in event.get("ruleGroupList", []) or []:
        terminating = group.get("terminatingRule") or {}
        if RULE_ID in terminating.get("ruleId", ""):
            return True

        for matching_rule in group.get("nonTerminatingMatchingRules", []) or []:
            if RULE_ID in matching_rule.get("ruleId", ""):
                return True

    return False


def title(event):
    client_ip = event.deep_get("httpRequest", "clientIp", default="<UNKNOWN_CLIENT_IP>")
    action = event.get("action", default="<UNKNOWN_ACTION>")
    source = event.get("httpSourceName", default="<UNKNOWN_SOURCE>")
    return f"AWS WAF {RULE_ID} Match - {action} from {client_ip} via {source}"


def alert_context(event):
    http_request = event.get("httpRequest", {})
    headers = http_request.get("headers", [])
    user_agent = next(
        (h.get("value") for h in headers if h.get("name", "").lower() == "user-agent"), None
    )

    context = {
        "client_ip": http_request.get("clientIp"),
        "country": http_request.get("country"),
        "http_method": http_request.get("httpMethod"),
        "uri": http_request.get("uri"),
        "user_agent": user_agent,
        "action": event.get("action"),
        "source": event.get("httpSourceName"),
        "source_id": event.get("httpSourceId"),
        "terminating_rule_id": event.get("terminatingRuleId"),
        "terminating_rule_type": event.get("terminatingRuleType"),
    }

    # Add matched data if available
    terminating_matches = event.get("terminatingRuleMatchDetails", [])
    if terminating_matches:
        context["matched_data"] = [
            {
                "condition_type": m.get("conditionType"),
                "location": m.get("location"),
                "matched_strings": m.get("matchedData", []),
            }
            for m in terminating_matches
        ]

    return context


def severity(event):
    action = event.get("action", "")
    if action == "ALLOW":
        return "CRITICAL"
    if action == "BLOCK":
        return "HIGH"
    if action == "COUNT":
        return "MEDIUM"
    return "DEFAULT"

Rule specification

AnalysisType: rule
Filename: aws_waf_reactjsrce_body.py
RuleID: "AWS.WAF.ReactJSRCE.Body"
DisplayName: "AWS WAF ReactJS RCE Attempt via Body"
Enabled: true
LogTypes:
  - AWS.WAFWebACL
Tags:
  - AWS
  - WAF
  - React2Shell
  - Initial Access:Exploit Public-Facing Application
  - Execution:Command and Scripting Interpreter
Reports:
  MITRE ATT&CK:
    - TA0001:T1190
    - TA0002:T1059
Severity: High
Description: >
  Detects AWS WAF ReactJSRCE_BODY managed rule matches indicating React2Shell (CVE-2025-55182)
  ReactJS RCE attempts via HTTP body. Monitors all WAF sources: ALB, CloudFront, API Gateway, AppSync.
Runbook: |
  1. Review alert context for source IP, URI, and body content
  2. Check action: BLOCK (HIGH) or ALLOW (CRITICAL - investigate immediately)
  3. If allowed, check target application for compromise
  4. Review backend server logs for suspicious activity
  5. Block repeat offender IPs and validate ReactJS input handling
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html

DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.WAFWebACL events when any of the conditions below holds.

Condition

  • any of:
    • terminatingRuleId contains ReactJSRCE_BODY
    • any element of nonTerminatingMatchingRules matches:
      • nonTerminatingMatchingRules.ruleId contains ReactJSRCE_BODY

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
client_iphttpRequest.clientIp
countryhttpRequest.country
http_methodhttpRequest.httpMethod
urihttpRequest.uri
action
sourcehttpSourceName
source_idhttpSourceId
terminating_rule_idterminatingRuleId
terminating_rule_typeterminatingRuleType

Response runbook

1. Review alert context for source IP, URI, and body content

2. Check action: BLOCK (HIGH) or ALLOW (CRITICAL - investigate immediately)

3. If allowed, check target application for compromise

4. Review backend server logs for suspicious activity

5. Block repeat offender IPs and validate ReactJS input handling

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "BLOCK",
  "httpRequest": {
    "clientIp": "203.0.113.45",
    "country": "US",
    "httpMethod": "POST",
    "uri": "/api/endpoint"
  },
  "httpSourceName": "ALB",
  "terminatingRuleId": "ReactJSRCE_BODY",
  "timestamp": "2024-03-20T10:30:00.000Z",
  "webaclId": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/a1b2c3d4"
}

AWS WAF Rule Ordering

#
Severity
high
Resource types
AWS.WAF.Regional.WebACL, AWS.WAF.WebACL
Tags
AWS, Configuration Required, Security Control
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that all WAF's have the correct rule ordering. Incorrect rule ordering could lead to less restrictive rules being matched and allowing traffic through before more restrictive rules that should have blocked the traffic.

Detection logic

# ORDERING is a dictionary that describes the required ordering of Web ACL rules for a
# given web acl ID. Map the Web ACL ID to an ordered tuple of Web ACL rule IDs
# Example usage:
# ORDERING{
#   'WebAclId-123': ('FirstRuleId', 'SecondRuleId', 'ThirdRuleId'),
# }
ORDERING = {
    "EXAMPLE_WEB_ACL_ID": ("EXAMPLE_RULE_1_ID", "EXAMPLE_RULE_2_ID"),
}


def policy(resource):
    # Check if Web ACL rule ordering is being enforced
    if resource["WebACLId"] not in ORDERING:
        return True
    web_acl_rules = resource["Rules"]

    # Check that the Web ACL has the correct number of rules
    if len(ORDERING[resource["WebACLId"]]) != len(web_acl_rules):
        return False

    # Confirm that each rule is ordered correctly
    for web_acl_rule in web_acl_rules:
        # Rules are not necessarily listed in their priority order in the rules list.
        # This determines their priority order, and offsets by one to be indexed starting at 0.
        priority_order = web_acl_rule["Priority"] - 1
        if web_acl_rule["RuleId"] != ORDERING[resource["WebACLId"]][priority_order]:
            return False

    # The rules all matched correctly, return True
    return True

Rule specification

AnalysisType: policy
Filename: aws_waf_rule_ordering.py
PolicyID: "AWS.WAF.RuleOrdering"
DisplayName: "AWS WAF Rule Ordering"
Enabled: false
ResourceTypes:
  - AWS.WAF.Regional.WebACL
  - AWS.WAF.WebACL
Tags:
  - AWS
  - Configuration Required
  - Security Control
Severity: High
Description: >
  This policy validates that all WAF's have the correct rule ordering. Incorrect rule ordering could lead to less restrictive rules being matched and allowing traffic through before more restrictive rules that should have blocked the traffic.
Runbook: >
  https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-waf-has-correct-rule-ordering
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-rules.html

Stages and Predicates

Flags AWS.WAF.Regional.WebACL, AWS.WAF.WebACL resources when the condition below holds.

Condition

  • WebACLId is one of EXAMPLE_WEB_ACL_ID

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
WebACLIdeqEXAMPLE_WEB_ACL_IDexcludes:WebACLId field:"WebACLId" value:"EXAMPLE_WEB_ACL_ID"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
WebACLIdin
  • EXAMPLE_WEB_ACL_ID
field:"WebACLId" kind:in value:"EXAMPLE_WEB_ACL_ID"

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-policies/aws-waf-has-correct-rule-ordering

AWS WAF WebACL Has Associated Resources

#
Severity
medium
Resource types
AWS.WAF.Regional.WebACL, AWS.WAF.WebACL
Tags
AWS, Security Control, Optimization
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy ensures that AWS WAF WebACLs are associated with at least one resource (ALB, CloudFront Distribution, or API Gateway). If a WebACL is not associated with any resources, it is inactive and not providing any protection.

Detection logic

def policy(resource):
    # Check if the WebACL has any associated resources
    associations = resource.get("AssociatedResources", [])
    return len(associations) > 0

Rule specification

AnalysisType: policy
Filename: aws_waf_webacl_has_associated_resources.py
PolicyID: "AWS.WAF.WebACLHasAssociatedResources"
DisplayName: "AWS WAF WebACL Has Associated Resources"
Enabled: true
ResourceTypes:
  - AWS.WAF.Regional.WebACL
  - AWS.WAF.WebACL
Tags:
  - AWS
  - Security Control
  - Optimization
Severity: Medium
Description: >
  This policy ensures that AWS WAF WebACLs are associated with at least one resource (ALB, CloudFront Distribution, or API Gateway). If a WebACL is not associated with any resources, it is inactive and not providing any protection.
Runbook: >
  Associate the WAF WebACL with at least one resource, such as an Application Load Balancer (ALB), CloudFront Distribution, or API Gateway. WebACLs that are not associated with resources do not protect any traffic.
Reference: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-associate.html

Stages and Predicates

Flags AWS.WAF.Regional.WebACL, AWS.WAF.WebACL resources when the condition below holds.

Condition

  • AssociatedResources has length of at most 0

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AssociatedResourceslength_compare0excludes:AssociatedResources field:"AssociatedResources" value:"0"

Response runbook

Associate the WAF WebACL with at least one resource, such as an Application Load Balancer (ALB), CloudFront Distribution, or API Gateway. WebACLs that are not associated with resources do not protect any traffic.

AWS.CloudTrail.UserAccessKeyAuth

#
Severity
informational
Entities
actor_ids, aws_account_ids, aws_arns, ip_addresses, trace_ids, usernames
Log types
AWS.CloudTrail
Source
github.com/panther-labs/panther-analysis

Detection logic

def rule(event):
    # Only look for successes
    if event.get("errorCode") or event.get("errorMessage"):
        return False
    # Reference: https://awsteele.com/blog/2020/09/26/aws-access-key-format.html
    return event.deep_get("userIdentity", "accessKeyId", default="").startswith("AKIA")


def title(event):
    arn = event.deep_get("userIdentity", "arn")
    key = event.deep_get("userIdentity", "accessKeyId")
    return f"User {arn} signed in with access key {key}"


def alert_context(event):
    return {
        "ip_accessKeyId": event.get("sourceIpAddress", "{not found}")
        + ":"
        + event.deep_get("userIdentity", "accessKeyId", default="{not found}")
    }

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_useraccesskeyauth.py
RuleID: "AWS.CloudTrail.UserAccessKeyAuth"
DisplayName: "AWS.CloudTrail.UserAccessKeyAuth"
Enabled: true
LogTypes:
    - AWS.CloudTrail
Severity: Info
DedupPeriodMinutes: 60
Threshold: 1
InlineFilters:
    - All: []
CreateAlert: false

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • userIdentity.accessKeyId starts with AKIA

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
arnuserIdentity.arn
accessKeyIduserIdentity.accessKeyId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "4c24450d-007e-4849-9e1b-4954622dbb08",
  "eventName": "GetCallerIdentity",
  "eventSource": "sts.amazonaws.com",
  "eventTime": "2024-06-02 20:16:22.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "p_any_actor_ids": [
    "AIDASXP6SDP2AJQPWVFII"
  ],
  "p_any_aws_account_ids": [
    "187901811700"
  ],
  "p_any_aws_arns": [
    "arn:aws:iam::187901811700:user/exposed.user"
  ],
  "p_any_ip_addresses": [
    "73.252.165.138"
  ],
  "p_any_trace_ids": [
    "AKIASXP6SDP2F3JQERZ2"
  ],
  "p_any_usernames": [
    "exposed.user"
  ],
  "p_event_time": "2024-06-02 20:16:22.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-06-02 20:25:54.289981899",
  "p_row_id": "f26228572e3f9acd88e0cadc1fc88e0a",
  "p_schema_version": 0,
  "p_source_file": {
    "aws_s3_bucket": "threat-research-trail-trail-bucket-0ipb5nzxam",
    "aws_s3_key": "AWSLogs/187901811700/CloudTrail/us-east-1/2024/06/02/187901811700_CloudTrail_us-east-1_20240602T2020Z_12tLndRWeXi4IWKg.json.gz"
  },
  "p_source_id": "d0a1e235-6548-4e7f-952a-35063b304007",
  "p_source_label": "threat-research-trail-us-east-1",
  "readOnly": true,
  "recipientAccountId": "187901811700",
  "requestID": "c77ed3ee-8480-41df-98cb-9cf52ce04a1c",
  "sourceIPAddress": "73.252.165.138",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "sts.us-east-1.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "aws-cli/2.15.59 md/awscrt#0.19.19 ua/2.0 os/macos#22.6.0 md/arch#arm64 lang/python#3.11.11 md/pyimpl#CPython cfg/retry-mode#standard md/installer#source md/prompt#off md/command#sts.get-caller-identity",
  "userIdentity": {
    "accessKeyId": "AKIASXP6SDP2F3JQERZ2",
    "accountId": "187901811700",
    "arn": "arn:aws:iam::187901811700:user/exposed.user",
    "principalId": "AIDASXP6SDP2AJQPWVFII",
    "type": "IAMUser",
    "userName": "exposed.user"
  }
}

CloudTrail EC2 StopInstances

#
Severity
informational
Log types
AWS.CloudTrail
Tags
panther-signal
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A CloudTrail instances were stopped. It makes further changes of instances possible

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return all(
        [
            not event.get("errorCode"),
            not event.get("errorMessage"),
            event.get("eventName") == "StopInstances",
        ]
    )


def title(event):
    instances = [
        instance["instanceId"]
        for instance in event.deep_get("requestParameters", "instancesSet", "items", default=[])
    ]
    account = event.get("recipientAccountId")
    return f"EC2 instances {instances} stopped in account {account}."


def alert_context(event):
    context = aws_rule_context(event)
    context["instance_ids"] = [
        instance["instanceId"]
        for instance in event.deep_get("requestParameters", "instancesSet", "items", default=[])
    ]
    return context

Rule specification

AnalysisType: rule
RuleID: "AWS.EC2.StopInstances"
DisplayName: "CloudTrail EC2 StopInstances"
Enabled: true
CreateAlert: false
Filename: aws_ec2_stopinstances.py
LogTypes:
  - AWS.CloudTrail
Tags:
  - panther-signal
Severity: Info
Description: >
  A CloudTrail instances were stopped. It makes further changes of instances possible
Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-examples.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is StopInstances

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "9357a8cc-a0eb-46a1-b67e-EXAMPLE19b14",
  "eventName": "StopInstances",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2023-07-19T21:14:20Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "777788889999",
  "requestID": "c308a950-e43e-444e-afc1-EXAMPLE73e49",
  "requestParameters": {
    "force": false,
    "instancesSet": {
      "items": [
        {
          "instanceId": "i-EXAMPLE56126103cb"
        },
        {
          "instanceId": "i-EXAMPLEaff4840c22"
        }
      ]
    }
  },
  "responseElements": {
    "instancesSet": {
      "items": [
        {
          "currentState": {
            "code": 64,
            "name": "stopping"
          },
          "instanceId": "i-EXAMPLE56126103cb",
          "previousState": {
            "code": 16,
            "name": "running"
          }
        },
        {
          "currentState": {
            "code": 64,
            "name": "stopping"
          },
          "instanceId": "i-EXAMPLEaff4840c22",
          "previousState": {
            "code": 16,
            "name": "running"
          }
        }
      ]
    },
    "requestId": "c308a950-e43e-444e-afc1-EXAMPLE73e49"
  },
  "sessionCredentialFromConsole": "true",
  "sourceIPAddress": "192.0.2.0",
  "tlsDetails": {
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "ec2.us-east-1.amazonaws.com",
    "tlsVersion": "TLSv1.2"
  },
  "userAgent": "aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/ec2.stop-instances",
  "userIdentity": {
    "accessKeyId": "AKIAI44QH8DHBEXAMPLE",
    "accountId": "777788889999",
    "arn": "arn:aws:iam::777788889999:user/Nikki",
    "principalId": "EXAMPLE6E4XEGITWATV6R",
    "sessionContext": {
      "attributes": {
        "creationDate": "2023-07-19T21:11:57Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {},
      "webIdFederationData": {}
    },
    "type": "IAMUser",
    "userName": "Nikki"
  }
}

CloudTrail Event Selectors Disabled

#
Severity
medium
Group by
requestParameters.trailName
Compliance
CIS 3.5; Stratus Red Team aws.defense-evasion.cloudtrail-event-selectors
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A CloudTrail Trail was modified to exclude management events for 1 or more resource types.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of CloudTrail changes
CLOUDTRAIL_EDIT_SELECTORS = {"PutEventSelectors"}


def rule(event):
    if not (aws_cloudtrail_success(event) and event.get("eventName") in CLOUDTRAIL_EDIT_SELECTORS):
        return False

    # Check if management events are included for each selector.
    #    deep_walk only returns a list if there's more than 1 entry in the nested array, so we must
    #    enforce it to be a list.
    includes = event.deep_walk("requestParameters", "eventSelectors", "includeManagementEvents")

    if includes is None:
        includes = []

    if not isinstance(includes, list):
        includes = [includes]

    # Return False all the management events are included, else return True and raise alert
    return not all(includes)


def dedup(event):
    # Merge on the CloudTrail ARN
    return event.deep_get("requestParameters", "trailName", default="<UNKNOWN_NAME>")


def title(event):
    return (
        f"Management events have been exluded from CloudTrail [{dedup(event)}] in account "
        f"[{event.get('recipientAccountId')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_event_selectors_disabled.py
RuleID: "AWS.CloudTrail.EventSelectorsDisabled"
DisplayName: "CloudTrail Event Selectors Disabled"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.5
  MITRE ATT&CK:
    - TA0005:T1562
  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-event-selectors
Severity: Medium
Description: >
  A CloudTrail Trail was modified to exclude management events for 1 or more resource types.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified
Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-update-a-trail-console.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of PutEventSelectors

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "4ca1cb25-7633-496b-8f92-6de876228c3f",
  "eventName": "PutEventSelectors",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2024-11-25 17:51:21.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.11",
  "managementEvent": true,
  "p_event_time": "2024-11-25 17:51:21.000000000",
  "p_log_type": "AWS.CloudTrail",
  "p_parse_time": "2024-11-25 17:55:54.253083422",
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "a8c6184a-89b1-4fc1-a6fa-324748d48b64",
  "requestParameters": {
    "eventSelectors": [
      {
        "dataResources": [
          {
            "type": "AWS::S3::Object",
            "values": []
          },
          {
            "type": "AWS::Lambda::Function",
            "values": []
          }
        ],
        "excludeManagementEventSources": [],
        "includeManagementEvents": false,
        "readWriteType": "ReadOnly"
      }
    ],
    "trailName": "sample-cloudtrail-name"
  },
  "responseElements": {
    "eventSelectors": [
      {
        "dataResources": [
          {
            "type": "AWS::S3::Object",
            "values": []
          },
          {
            "type": "AWS::Lambda::Function",
            "values": []
          }
        ],
        "excludeManagementEventSources": [],
        "includeManagementEvents": false,
        "readWriteType": "ReadOnly"
      }
    ],
    "trailARN": "arn:aws:cloudtrail:us-west-2:111122223333:trail/sample-cloudtrail-name"
  },
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "cloudtrail.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "sample-user-agent",
  "userIdentity": {
    "accessKeyId": "SAMPLE_ACCESS_KEY_ID",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/leroy.jenkins",
    "principalId": "EXAMPLEPRINCIPLEID:leroy.jenkins",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-11-25T16:53:42Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111122223333",
        "arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
        "principalId": "EXAMPLEPRINCIPLEID",
        "type": "Role",
        "userName": "SampleRole"
      }
    },
    "type": "AssumedRole"
  }
}

CloudTrail Stopped

#
Severity
medium
Group by
requestParameters.name
Compliance
CIS 3.5; Stratus Red Team aws.defense-evasion.cloudtrail-delete, aws.defense-evasion.cloudtrail-stop
Log types
AWS.CloudTrail
Tags
AWS, Security Control, DemoThreatHunting, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A CloudTrail Trail was modified.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of CloudTrail changes
CLOUDTRAIL_STOP_DELETE = {
    "DeleteTrail",
    "StopLogging",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in CLOUDTRAIL_STOP_DELETE


def dedup(event):
    # Merge on the CloudTrail ARN
    return event.deep_get("requestParameters", "name", default="<UNKNOWN_NAME>")


def title(event):
    return (
        f"CloudTrail [{dedup(event)}] in account "
        f"[{event.get('recipientAccountId')}] was stopped/deleted"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_cloudtrail_stopped.py
RuleID: "AWS.CloudTrail.Stopped"
DisplayName: "CloudTrail Stopped"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - DemoThreatHunting
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.5
  MITRE ATT&CK:
    - TA0005:T1562
  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-delete
    - aws.defense-evasion.cloudtrail-stop
Severity: Medium
Description: >
  A CloudTrail Trail was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified
Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-delete-trails-console.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of DeleteTrail, StopLogging

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DeleteTrail
  • StopLogging
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-cloudtrail-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "StopLogging",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "name": "arn:aws:cloudtrail:us-west-2:123456789012:trail/example-trail"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

CodeBuild Project made Public

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Exfiltration:Exfiltration Over Web Service
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An AWS CodeBuild Project was made publicly accessible

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return (
        event["eventName"] == "UpdateProjectVisibility"
        and event.deep_get("requestParameters", "projectVisibility") == "PUBLIC_READ"
    )


def title(event):
    return (
        f"AWS CodeBuild Project made Public by {event.deep_get('userIdentity', 'arn')} "
        f"in account {event.deep_get('recipientAccountId')}"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_codebuild_made_public.py
RuleID: "AWS.CloudTrail.CodebuildProjectMadePublic"
DisplayName: "CodeBuild Project made Public"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0010:T1567
Tags:
  - AWS
  - Security Control
  - Exfiltration:Exfiltration Over Web Service
Severity: High
Description: >
  An AWS CodeBuild Project was made publicly accessible
Runbook: TBD
Reference: https://docs.aws.amazon.com/codebuild/latest/userguide/public-builds.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is UpdateProjectVisibility
  • requestParameters.projectVisibility is PUBLIC_READ

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

TBD

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "982f8066-640d-40fb-b433-ba15e14fee40",
  "eventName": "UpdateProjectVisibility",
  "eventSource": "codebuild.amazonaws.com",
  "eventTime": "2021-08-18T14:54:53Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111122223333",
  "requestID": "4397365f-c790-4c23-9fe6-97e13a16ea84",
  "requestParameters": {
    "projectArn": "arn:aws:codebuild:us-east-1:111122223333:project/testproject1234",
    "projectVisibility": "PUBLIC_READ",
    "resourceAccessRole": "arn:aws:iam::111122223333:role/service-role/test"
  },
  "responseElements": null,
  "sourceIPAddress": "1.1.1.1",
  "userAgent": "aws-internal/3 aws-sdk-java/1.11.1030 Linux/5.4.116-64.217.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.302-b08 java/1.8.0_302 vendor/Oracle_Corporation cfg/retry-mode/legacy",
  "userIdentity": {
    "accessKeyId": "ASIAXXXXXXXXXXXX",
    "accountId": "111122223333",
    "arn": "arn:aws:sts::111122223333:assumed-role/MakeStuffPublic",
    "principalId": "111111111111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2021-08-18T14:54:10Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {},
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

DEPRECATED - AWS User Login Profile Modified

#
Severity
high
Compliance
Stratus Red Team aws.privilege-escalation.iam-update-user-login-profile
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An attacker with iam:UpdateLoginProfile permission on other users can change the password used to login to the AWS console. May be legitimate account administration.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    return (
        event.get("eventSource", "") == "iam.amazonaws.com"
        and event.get("eventName", "") == "UpdateLoginProfile"
        and not event.deep_get("requestParameters", "passwordResetRequired", default=False)
        and not event.deep_get("userIdentity", "arn", default="").endswith(
            f"/{event.deep_get('requestParameters', 'userName', default='')}"
        )
    )


def title(event):
    return (
        f"User [{event.deep_get('userIdentity', 'arn').split('/')[-1]}] "
        f"changed the password for "
        f"[{event.deep_get('requestParameters','userName')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Description: "An attacker with iam:UpdateLoginProfile permission on other users can change the password used to login to the AWS console. May be legitimate account administration."
DisplayName: "DEPRECATED - AWS User Login Profile Modified"
Enabled: true
Filename: aws_user_login_profile_modified.py
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
    - TA0005:T1108
    - TA0005:T1550
    - TA0008:T1550

  Stratus Red Team:
    - aws.privilege-escalation.iam-update-user-login-profile
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-pass-accesskeys-ssh.html
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - AWS.CloudTrail
RuleID: "AWS.User.Login.Profile.Modified"
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is iam.amazonaws.com
  • eventName is UpdateLoginProfile
  • requestParameters.passwordResetRequired is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn
userNamerequestParameters.userName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "1234",
  "eventName": "UpdateLoginProfile",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2022-09-15 13:45:24",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "987654321",
  "requestParameters": {
    "passwordResetRequired": false,
    "userName": "bob"
  },
  "sessionCredentialFromConsole": true,
  "sourceIPAddress": "AWS Internal",
  "userAgent": "AWS Internal",
  "userIdentity": {
    "accessKeyId": "ABC1234",
    "accountId": "987654321",
    "arn": "arn:aws:sts::98765432:assumed-role/IAM/alice",
    "principalId": "ABCDE:alice",
    "sessionContext": {
      "attributes": {
        "creationDate": "2022-09-15T13:36:47Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "987654321",
        "arn": "arn:aws:iam::9876432:role/IAM",
        "principalId": "1234ABC",
        "type": "Role",
        "userName": "IAM"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

Detect Reconnaissance from IAM Users

#
Severity
informational
Group by
userIdentity.arn
Log types
AWS.CloudTrail
Tags
AWS, Discovery:Cloud Service Discovery
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM user has a high volume of access denied API calls.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

from ipaddress import ip_address

from panther_aws_helpers import aws_rule_context

# service/event patterns to monitor
RECON_ACTIONS = {
    "dynamodb": ["List", "Describe", "Get"],
    "ec2": ["Describe", "Get"],
    "iam": ["List", "Get"],
    "s3": ["List", "Get"],
    "rds": ["Describe", "List"],
}


def rule(event):
    # Filter events
    if event.get("errorCode") != "AccessDenied":
        return False
    if event.deep_get("userIdentity", "type") != "IAMUser":
        return False

    # Console Activity can easily result in false positives as some pages contain a mix of
    # items that a user may or may not have access to.
    if event.get("userAgent").startswith("aws-internal/3"):
        return False

    # Validate the request came from outside of AWS
    try:
        ip_address(event.get("sourceIPAddress"))
    except ValueError:
        return False

    # Pattern match this event to the recon actions
    for event_source, event_patterns in RECON_ACTIONS.items():
        if event.get("eventSource", "").startswith(event_source) and any(
            event.get("eventName", "").startswith(event_pattern) for event_pattern in event_patterns
        ):
            return True
    return False


def dedup(event):
    return event.deep_get("userIdentity", "arn")


def title(event):
    user_type = event.deep_get("userIdentity", "type")
    if user_type == "IAMUser":
        user = event.deep_get("userIdentity", "userName")
    # root user
    elif user_type == "Root":
        user = user_type
    else:
        user = "<UNKNOWN_USER>"
    return (
        "Reconnaissance activity denied to user "
        f"[{user}] "
        "in account "
        f"[{event.get('recipientAccountId')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_user_recon_denied.py
RuleID: "AWS.IAMUser.ReconAccessDenied"
DisplayName: "Detect Reconnaissance from IAM Users"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Discovery:Cloud Service Discovery
Reports:
  MITRE ATT&CK:
    - TA0007:T1526
Severity: Info
Threshold: 15
DedupPeriodMinutes: 10
Description: An IAM user has a high volume of access denied API calls.
Runbook: Analyze the IP they came from, and other actions taken before/after.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_access-denied.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - errorMessage
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is AccessDenied
  • userIdentity.type is IAMUser
  • userAgent does not start with aws-internal/3

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 15 matches within 10m

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userAgentstarts_withaws-internal/3excludes:userAgent field:"userAgent" value:"aws-internal/3"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Analyze the IP they came from, and other actions taken before/after.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:iam::123456789012:user/tester is not authorized to perform: iam:GetRole on resource: arn:aws:iam::123456789012:role/FooBar",
  "eventID": "1",
  "eventName": "GetRole",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": null,
  "responseElements": null,
  "sourceIPAddress": "40.185.186.6",
  "userAgent": "aws-sdk-go/1.32.7 (go1.14.6; linux; amd64) exec-env/AWS_Lambda_go1.x",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/tester",
    "invokedBy": "signin.amazonaws.com",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      }
    },
    "type": "IAMUser",
    "userName": "tester"
  }
}

EC2 Network ACL Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.11
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 Network ACL was modified.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an EC2 Network ACL modification
EC2_NACL_MODIFIED_EVENTS = {
    "CreateNetworkAcl",
    "CreateNetworkAclEntry",
    "DeleteNetworkAcl",
    "DeleteNetworkAclEntry",
    "ReplaceNetworkAclEntry",
    "ReplaceNetworkAclAssociation",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in EC2_NACL_MODIFIED_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_network_acl_modified.py
RuleID: "AWS.EC2.NetworkACLModified"
DisplayName: "EC2 Network ACL Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.11
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Info
Description: An EC2 Network ACL was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-network-acl-modified
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html#nacl-tasks
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAcl, DeleteNetworkAclEntry, ReplaceNetworkAclEntry (+1 more values, see Indicators below)

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CreateNetworkAcl
  • CreateNetworkAclEntry
  • DeleteNetworkAcl
  • DeleteNetworkAclEntry
  • ReplaceNetworkAclAssociation
  • ReplaceNetworkAclEntry
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-network-acl-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateNetworkAclEntry",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "aclProtocol": "-1",
    "cidrBlock": "0.0.0.0/0",
    "egress": true,
    "icmpTypeCode": {},
    "networkAclId": "acl-1",
    "portRange": {},
    "ruleAction": "allow",
    "ruleNumber": 500
  },
  "responseElements": {
    "_return": true,
    "requestID": "1"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

EC2 Network Gateway Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.12
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 Network Gateway was modified.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an EC2 Network Gateway modification
EC2_GATEWAY_MODIFIED_EVENTS = {
    "CreateCustomerGateway",
    "DeleteCustomerGateway",
    "AttachInternetGateway",
    "CreateInternetGateway",
    "DeleteInternetGateway",
    "DetachInternetGateway",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in EC2_GATEWAY_MODIFIED_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_gateway_modified.py
RuleID: "AWS.EC2.GatewayModified"
DisplayName: "EC2 Network Gateway Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.12
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Info
Description: An EC2 Network Gateway was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-gateway-modified
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateCustomerGateway, DeleteCustomerGateway, AttachInternetGateway, CreateInternetGateway, DeleteInternetGateway (+1 more values, see Indicators below)

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AttachInternetGateway
  • CreateCustomerGateway
  • CreateInternetGateway
  • DeleteCustomerGateway
  • DeleteInternetGateway
  • DetachInternetGateway
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-gateway-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "AttachInternetGateway",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "internetGatewayId": "igw-1",
    "vpcId": "vpc-1"
  },
  "responseElements": {
    "_return": true,
    "requestID": "1"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

EC2 Route Table Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.13
Log types
AWS.CloudTrail
Tags
AWS, Exfiltration:Exfiltration Over Alternative Protocol
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 Route Table was modified.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an EC2 Route Table modification
EC2_RT_MODIFIED_EVENTS = {
    "CreateRoute",
    "CreateRouteTable",
    "ReplaceRoute",
    "ReplaceRouteTableAssociation",
    "DeleteRouteTable",
    "DeleteRoute",
    "DisassociateRouteTable",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in EC2_RT_MODIFIED_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_route_table_modified.py
RuleID: "AWS.EC2.RouteTableModified"
DisplayName: "EC2 Route Table Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Exfiltration:Exfiltration Over Alternative Protocol
Reports:
  CIS:
    - 3.13
  MITRE ATT&CK:
    - TA0010:T1048
Severity: Info
Description: An EC2 Route Table was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-route-table-modified
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/WorkWithRouteTables.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateRoute, CreateRouteTable, ReplaceRoute, ReplaceRouteTableAssociation, DeleteRouteTable (+2 more values, see Indicators below)

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CreateRoute
  • CreateRouteTable
  • DeleteRoute
  • DeleteRouteTable
  • DisassociateRouteTable
  • ReplaceRoute
  • ReplaceRouteTableAssociation
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-route-table-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateRoute",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "destinationCidrBlock": "0.0.0.0/0",
    "gatewayId": "igw-1",
    "routeTableId": "rtb-1"
  },
  "responseElements": {
    "_return": true,
    "requestID": "1"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

EC2 Secrets Manager Retrieve Secrets

#
Status
Experimental
Severity
informational
Log types
AWS.CloudTrail
Tags
AWS, Credential Access, Stratus Red Team
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

An attacker attempted to retrieve a high number of Secrets Manager secrets, through secretsmanager:GetSecretValue.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if (
        event.get("eventName") == "GetSecretValue"
        and not aws_cloudtrail_success(event)
        and event.get("errorCode") == "AccessDenied"
    ):
        return True
    return False


def title(event):
    user = event.udm("actor_user")
    return f"[{user}] is not authorized to retrieve secrets from AWS Secrets Manager"


def alert_context(event):
    return aws_rule_context(event) | {
        "errorCode": event.get("errorCode"),
        "errorMessage": event.get("errorMessage"),
    }

Rule specification

AnalysisType: rule
Filename: aws_secretsmanager_retrieve_secrets.py
RuleID: "AWS.SecretsManager.RetrieveSecrets"
DisplayName: "EC2 Secrets Manager Retrieve Secrets"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Credential Access
  - Stratus Red Team
Status: Experimental
Reports:
  MITRE ATT&CK:
    - TA0006:T1552 # Credentials from Password Stores 
Severity: Info
Description: An attacker attempted to retrieve a high number of Secrets Manager secrets, through secretsmanager:GetSecretValue.
Runbook: https://permiso.io/blog/lucr-3-scattered-spider-getting-saas-y-in-the-cloud
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.credential-access.secretsmanager-retrieve-secrets/
Threshold: 20
DedupPeriodMinutes: 1440
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is GetSecretValue
  • any of:
    • errorCode is present
    • errorMessage is present
  • errorCode is AccessDenied
Alert cadence
alerts after 20 matches within 1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
actor_user

Response runbook

https://permiso.io/blog/lucr-3-scattered-spider-getting-saas-y-in-the-cloud

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "errorCode": "AccessDenied",
  "eventCategory": "Management",
  "eventID": "dfd6d93a-2ce6-4dbe-8939-86b8ba67c868",
  "eventName": "GetSecretValue",
  "eventSource": "secretsmanager.amazonaws.com",
  "eventTime": "2024-10-17 21:48:45.000000000",
  "eventType": "AwsApiCall",
  "eventVersion": "1.09",
  "managementEvent": true,
  "readOnly": true,
  "recipientAccountId": "123123123123",
  "requestID": "5128b35f-0daf-4c5e-948a-21a1c507968c",
  "requestParameters": {
    "secretId": "arn:aws:secretsmanager:us-west-2:123123123123:secret:stratus-red-team-retrieve-secret-3-gscOm8",
    "versionId": "7DC59E8B-63AE-454D-B7A4-8A7D64AB05E7"
  },
  "sourceIPAddress": "123.123.123.123",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "secretsmanager.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "APN/1.0 HashiCorp/1.0 Terraform/1.1.2 (+https://www.terraform.io) terraform-provider-aws/3.76.1 (+https://registry.terraform.io/providers/hashicorp/aws) aws-sdk-go/1.44.157 (go1.19.3; darwin; arm64) HashiCorp-terraform-exec/0.17.3",
  "userIdentity": {
    "accessKeyId": "ASIASXP6SDP2LKLKYYC4",
    "accountId": "123123123123",
    "arn": "arn:aws:sts::123123123123:assumed-role/AWSReservedSSO_DevAdmin_635426549a280cc6/evil.genius",
    "principalId": "AROASXP6SDP2F4WLQVARB:evil.genius",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-10-17T21:48:13Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123123123123",
        "arn": "arn:aws:iam::123123123123:role/aws-reserved/sso.amazonaws.com/us-west-2/AWSReservedSSO_DevAdmin_635426549a280cc6",
        "principalId": "AROASXP6SDP2F4WLQVARB",
        "type": "Role",
        "userName": "AWSReservedSSO_DevAdmin_635426549a280cc6"
      }
    },
    "type": "AssumedRole"
  }
}

EC2 Security Group Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.1; Stratus Red Team aws.exfiltration.ec2-security-group-open-port-22-ingress
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 Security Group was modified.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an EC2 SecurityGroup modification
EC2_SG_MODIFIED_EVENTS = {
    "AuthorizeSecurityGroupIngress",
    "AuthorizeSecurityGroupEgress",
    "RevokeSecurityGroupIngress",
    "RevokeSecurityGroupEgress",
    "CreateSecurityGroup",
    "DeleteSecurityGroup",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in EC2_SG_MODIFIED_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_security_group_modified.py
RuleID: "AWS.EC2.SecurityGroupModified"
DisplayName: "EC2 Security Group Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.10
  MITRE ATT&CK:
    - TA0005:T1562

  Stratus Red Team:
    - aws.exfiltration.ec2-security-group-open-port-22-ingress
Severity: Info
DedupPeriodMinutes: 720 # 12 hours
Description: >
  An EC2 Security Group was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-securitygroup-modified
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/working-with-security-groups.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of AuthorizeSecurityGroupIngress, AuthorizeSecurityGroupEgress, RevokeSecurityGroupIngress, RevokeSecurityGroupEgress, CreateSecurityGroup (+1 more values, see Indicators below)
Alert deduplication
repeat matches within 12h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AuthorizeSecurityGroupEgress
  • AuthorizeSecurityGroupIngress
  • CreateSecurityGroup
  • DeleteSecurityGroup
  • RevokeSecurityGroupEgress
  • RevokeSecurityGroupIngress
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-securitygroup-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "AuthorizeSecurityGroupIngress",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "groupId": "sg-1",
    "ipPermissions": {
      "items": [
        {
          "fromPort": 22,
          "groups": {},
          "ipProtocol": "tcp",
          "ipRanges": {
            "items": [
              {
                "cidrIp": "127.0.0.1/32",
                "description": "SSH for me"
              }
            ]
          },
          "ipv6Ranges": {},
          "prefixListIds": {},
          "toPort": 22
        }
      ]
    }
  },
  "responseElements": {
    "_return": true,
    "requestID": "1"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

EC2 VPC Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.14
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Defense Evasion:Impair Defenses
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An EC2 VPC was modified.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of an EC2 VPC modification
EC2_VPC_MODIFIED_EVENTS = {
    "CreateVpc",
    "DeleteVpc",
    "ModifyVpcAttribute",
    "AcceptVpcPeeringConnection",
    "CreateVpcPeeringConnection",
    "DeleteVpcPeeringConnection",
    "RejectVpcPeeringConnection",
    "AttachClassicLinkVpc",
    "DetachClassicLinkVpc",
    "DisableVpcClassicLink",
    "EnableVpcClassicLink",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in EC2_VPC_MODIFIED_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ec2_vpc_modified.py
RuleID: "AWS.EC2.VPCModified"
DisplayName: "EC2 VPC Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Defense Evasion:Impair Defenses
Reports:
  CIS:
    - 3.14
  MITRE ATT&CK:
    - TA0005:T1562
Severity: Info
DedupPeriodMinutes: 720 # 12 hours
Description: An EC2 VPC was modified.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-vpc-modified
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/configure-your-vpc.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of CreateVpc, DeleteVpc, ModifyVpcAttribute, AcceptVpcPeeringConnection, CreateVpcPeeringConnection (+6 more values, see Indicators below)
Alert deduplication
repeat matches within 12h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AcceptVpcPeeringConnection
  • AttachClassicLinkVpc
  • CreateVpc
  • CreateVpcPeeringConnection
  • DeleteVpc
  • DeleteVpcPeeringConnection
  • DetachClassicLinkVpc
  • DisableVpcClassicLink
  • EnableVpcClassicLink
  • ModifyVpcAttribute
  • RejectVpcPeeringConnection
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-ec2-vpc-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateVpc",
  "eventSource": "ec2.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "amazonProvidedIpv6CidrBlock": false,
    "cidrBlock": "0.0.0.0/26",
    "instanceTenancy": "default"
  },
  "responseElements": {
    "requestID": "1",
    "vpc": {
      "cidrBlock": "0.0.0.0/26",
      "cidrBlockAssociationSet": {
        "items": [
          {
            "associationId": "vpc-cidr-assoc-1",
            "cidrBlock": "0.0.0.0/26",
            "cidrBlockState": {
              "state": "associated"
            }
          }
        ]
      },
      "dhcpOptionsId": "dopt-1",
      "instanceTenancy": "default",
      "ipv6CidrBlockAssociationSet": {},
      "isDefault": false,
      "ownerId": "123456789012",
      "state": "pending",
      "tagSet": {},
      "vpcId": "vpc-1"
    }
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.ec2.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

ECR CRUD Actions

#
Severity
informational
Group by
userIdentity.arn
Compliance
CIS 3.12
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Unauthorized ECR Create, Read, Update, or Delete event occurred.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from fnmatch import fnmatch

from panther_aws_helpers import aws_rule_context

ECR_CRUD_EVENTS = {
    "BatchCheckLayerAvailability",
    "BatchDeleteImage",
    "BatchGetImage",
    "CompleteLayerUpload",
    "CreateRepository",
    "DeleteRepository",
    "DeleteRepositoryPolicy",
    "GetAuthorizationToken",
    "GetDownloadUrlForLayer",
    "GetRepositoryPolicy",
    "InitiateLayerUpload",
    "PutImage",
    "SetRepositoryPolicy",
    "UploadLayerPart",
}

ALLOWED_ROLES = [
    "*DeployRole",
]


def rule(event):
    if (
        event.get("eventSource") == "ecr.amazonaws.com"
        and event.get("eventName") in ECR_CRUD_EVENTS
    ):
        for role in ALLOWED_ROLES:
            if fnmatch(event.deep_get("userIdentity", "arn", default="unknown-arn"), role):
                return False

        return True
    return False


def title(event):
    return (
        f"[{event.deep_get('userIdentity','arn', default = 'unknown-arn')}] "
        f"performed ECR {event.get('eventName')} in "
        f"[{event.get('recipientAccountId')} {event.get('awsRegion')}]."
    )


def dedup(event):
    return f"{event.deep_get('userIdentity','arn', default = 'unknown-arn')}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_ecr_crud.py
RuleID: "AWS.ECR.CRUD"
DisplayName: "ECR CRUD Actions"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Configuration Required
Reports:
  CIS:
    - 3.12
  MITRE ATT&CK:
    - TA0005:T1525
Severity: Info
CreateAlert: false
Description: Unauthorized ECR Create, Read, Update, or Delete event occurred.
Runbook: https://docs.aws.amazon.com/AmazonECR/latest/userguide/logging-using-cloudtrail.html
Reference: https://docs.aws.amazon.com/AmazonECR/latest/userguide/security-iam.html#security_iam_authentication
SummaryAttributes:
  - eventSource
  - eventName
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is ecr.amazonaws.com
  • eventName is one of BatchCheckLayerAvailability, BatchDeleteImage, BatchGetImage, CompleteLayerUpload, CreateRepository (+9 more values, see Indicators below)
  • userIdentity.arn does not match the pattern *DeployRole

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.arnends_withDeployRoleexcludes:userIdentity.arn field:"userIdentity.arn" value:"DeployRole"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • BatchCheckLayerAvailability
  • BatchDeleteImage
  • BatchGetImage
  • CompleteLayerUpload
  • CreateRepository
  • DeleteRepository
  • DeleteRepositoryPolicy
  • GetAuthorizationToken
  • GetDownloadUrlForLayer
  • GetRepositoryPolicy
  • InitiateLayerUpload
  • PutImage
  • SetRepositoryPolicy
  • UploadLayerPart
field:"aws::eventName" kind:in
eventSourceeq
  • ecr.amazonaws.com
field:"aws::eventSource" kind:eq value:"ecr.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

https://docs.aws.amazon.com/AmazonECR/latest/userguide/logging-using-cloudtrail.html

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-2",
  "eventID": "2bfd4ee2-2178-4a82-a27d-b12939923f0f",
  "eventName": "PutImage",
  "eventSource": "ecr.amazonaws.com",
  "eventTime": "2019-04-15T16:45:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.04",
  "recipientAccountId": "123456789012",
  "requestID": "cf044b7d-5f9d-11e9-9b2a-95983139cc57",
  "requestParameters": {
    "imageManifest": "{\n   \"schemaVersion\": 2,\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"config\": {\n      \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n      \"size\": 5543,\n      \"digest\": \"sha256:000b9b805af1cdb60628898c9f411996301a1c13afd3dbef1d8a16ac6dbf503a\"\n   },\n   \"layers\": [\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 43252507,\n         \"digest\": \"sha256:3b37166ec61459e76e33282dda08f2a9cd698ca7e3d6bc44e6a6e7580cdeff8e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 846,\n         \"digest\": \"sha256:504facff238fde83f1ca8f9f54520b4219c5b8f80be9616ddc52d31448a044bd\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 615,\n         \"digest\": \"sha256:ebbcacd28e101968415b0c812b2d2dc60f969e36b0b08c073bf796e12b1bb449\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 850,\n         \"digest\": \"sha256:c7fb3351ecad291a88b92b600037e2435c84a347683d540042086fe72c902b8a\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 168,\n         \"digest\": \"sha256:2e3debadcbf7e542e2aefbce1b64a358b1931fb403b3e4aeca27cb4d809d56c2\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 37720774,\n         \"digest\": \"sha256:f8c9f51ad524d8ae9bf4db69cd3e720ba92373ec265f5c390ffb21bb0c277941\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 30432107,\n         \"digest\": \"sha256:813a50b13f61cf1f8d25f19fa96ad3aa5b552896c83e86ce413b48b091d7f01b\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 197,\n         \"digest\": \"sha256:7ab043301a6187ea3293d80b30ba06c7bf1a0c3cd4c43d10353b31bc0cecfe7d\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 154,\n         \"digest\": \"sha256:67012cca8f31dc3b8ee2305e7762fee20c250513effdedb38a1c37784a5a2e71\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 176,\n         \"digest\": \"sha256:3bc892145603fffc9b1c97c94e2985b4cb19ca508750b15845a5d97becbd1a0e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 183,\n         \"digest\": \"sha256:6f1c79518f18251d35977e7e46bfa6c6b9cf50df2a79d4194941d95c54258d18\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:b7bcfbc2e2888afebede4dd1cd5eebf029bb6315feeaf0b56e425e11a50afe42\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:2b220f8b0f32b7c2ed8eaafe1c802633bbd94849b9ab73926f0ba46cdae91629\"\n      }\n   ]\n}",
    "imageTag": "latest",
    "registryId": "123456789012",
    "repositoryName": "testrepo"
  },
  "resources": [
    {
      "ARN": "arn:aws:ecr:us-east-2:123456789012:repository/testrepo",
      "accountId": "123456789012"
    }
  ],
  "responseElements": {
    "image": {
      "imageId": {
        "imageDigest": "sha256:98c8b060c21d9adbb6b8c41b916e95e6307102786973ab93a41e8b86d1fc6d3e",
        "imageTag": "latest"
      },
      "imageManifest": "{\n   \"schemaVersion\": 2,\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"config\": {\n      \"mediaType\": \"application/vnd.docker.container.image.v1+json\",\n      \"size\": 5543,\n      \"digest\": \"sha256:000b9b805af1cdb60628898c9f411996301a1c13afd3dbef1d8a16ac6dbf503a\"\n   },\n   \"layers\": [\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 43252507,\n         \"digest\": \"sha256:3b37166ec61459e76e33282dda08f2a9cd698ca7e3d6bc44e6a6e7580cdeff8e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 846,\n         \"digest\": \"sha256:504facff238fde83f1ca8f9f54520b4219c5b8f80be9616ddc52d31448a044bd\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 615,\n         \"digest\": \"sha256:ebbcacd28e101968415b0c812b2d2dc60f969e36b0b08c073bf796e12b1bb449\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 850,\n         \"digest\": \"sha256:c7fb3351ecad291a88b92b600037e2435c84a347683d540042086fe72c902b8a\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 168,\n         \"digest\": \"sha256:2e3debadcbf7e542e2aefbce1b64a358b1931fb403b3e4aeca27cb4d809d56c2\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 37720774,\n         \"digest\": \"sha256:f8c9f51ad524d8ae9bf4db69cd3e720ba92373ec265f5c390ffb21bb0c277941\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 30432107,\n         \"digest\": \"sha256:813a50b13f61cf1f8d25f19fa96ad3aa5b552896c83e86ce413b48b091d7f01b\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 197,\n         \"digest\": \"sha256:7ab043301a6187ea3293d80b30ba06c7bf1a0c3cd4c43d10353b31bc0cecfe7d\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 154,\n         \"digest\": \"sha256:67012cca8f31dc3b8ee2305e7762fee20c250513effdedb38a1c37784a5a2e71\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 176,\n         \"digest\": \"sha256:3bc892145603fffc9b1c97c94e2985b4cb19ca508750b15845a5d97becbd1a0e\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 183,\n         \"digest\": \"sha256:6f1c79518f18251d35977e7e46bfa6c6b9cf50df2a79d4194941d95c54258d18\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:b7bcfbc2e2888afebede4dd1cd5eebf029bb6315feeaf0b56e425e11a50afe42\"\n      },\n      {\n         \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n         \"size\": 212,\n         \"digest\": \"sha256:2b220f8b0f32b7c2ed8eaafe1c802633bbd94849b9ab73926f0ba46cdae91629\"\n      }\n   ]\n}",
      "registryId": "123456789012",
      "repositoryName": "testrepo"
    }
  },
  "sourceIPAddress": "203.0.113.12",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:user/Mary_Major",
    "principalId": "AIDACKCEVSQ6C2EXAMPLE:account_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-04-15T16:42:14Z",
        "mfaAuthenticated": "false"
      }
    },
    "type": "IAMUser",
    "userName": "Mary_Major"
  }
}

External Principal Accessing AWS Resources Via VPC Endpoint

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, CloudTrail, VPCEndpoint, Network Boundary Bridging, Cloud Service Discovery, Exfiltration Over Alternative Protocol
Reference
www.wiz.io
Source
github.com/panther-labs/panther-analysis

This rule detects when a principal from one AWS account accesses resources in a different AWS account using a VPC Endpoint. While cross-account access may be expected in some cases, it could also indicate unauthorized lateral movement between AWS accounts.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Check if this is a VPC Endpoint network activity event
    if event.get("eventType") != "AwsVpceEvent" or event.get("eventCategory") != "NetworkActivity":
        return False

    # Look for external principal pattern (limited userIdentity field)
    user_identity = event.get("userIdentity", {})

    # If it's an AWS account type without full identity details, it could be an external principal
    if (
        user_identity.get("type") == "AWSAccount"
        and "arn" not in user_identity
        and "principalId" in user_identity
    ):
        # Get the account ID from the event and compare with the principal's account
        event_account = event.get("recipientAccountId")
        principal_account = user_identity.get("accountId")

        # If the accounts don't match, it's an external principal
        if event_account and principal_account and event_account != principal_account:
            return True

    return False


def title(event):
    # Use UDM actor_user which leverages the get_actor_user helper function
    # This properly handles various identity types including AssumedRole, Root, etc.
    actor_user = event.udm("actor_user")
    principal_account = event.deep_get("userIdentity", "accountId", default="unknown")
    event_account = event.get("recipientAccountId", "unknown")

    return (
        f"External Principal [{actor_user}] from account [{principal_account}] "
        f"accessing resources in account [{event_account}]"
    )


def alert_context(event):
    principal_account = event.deep_get("userIdentity", "accountId", default="")
    event_account = event.get("recipientAccountId", "")

    context = aws_rule_context(event)
    context.update(
        {
            "event_account": event_account,
            "principal_account": principal_account,
            "principal_id": event.deep_get("userIdentity", "principalId", default="unknown"),
            "source_ip": event.get("sourceIPAddress", "unknown"),
            "event_source": event.get("eventSource", "unknown"),
            "api_call": event.get("eventName", "unknown"),
            "resources": event.get("resources", []),
            "actor_user": event.udm("actor_user"),
        }
    )

    return context

Rule specification

AnalysisType: rule
Filename: aws_vpce_external_principal.py
RuleID: AWS.CloudTrail.VPCE.ExternalPrincipal
DisplayName: External Principal Accessing AWS Resources Via VPC Endpoint
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - CloudTrail
  - VPCEndpoint
  - Network Boundary Bridging
  - Cloud Service Discovery
  - Exfiltration Over Alternative Protocol
Reports:
  MITRE ATT&CK:
    - TA0005:T1599 # Network Boundary Bridging
    - TA0007:T1526 # Cloud Service Discovery
    - TA0010:T1048 # Exfiltration Over Alternative Protocol
Severity: Medium
Description: >
  This rule detects when a principal from one AWS account accesses resources in a different AWS account using a VPC Endpoint.
  While cross-account access may be expected in some cases, it could also indicate unauthorized lateral movement between AWS accounts.
Runbook: |
  1. Identify the principal account and the accessed account from the alert context.
  2. Verify if the cross-account access is expected and authorized:
     - Check if the principal account is part of your organization
     - Review IAM policies for the VPC Endpoint to confirm if cross-account access is intentional
     - Check resource policies for the accessed service to confirm if the principal should have access
  3. If the access is unexpected:
     - Review the API calls made by the principal
     - Check the VPC Endpoint configuration for potential misconfiguration
     - Consider restricting VPC Endpoint access to prevent unauthorized cross-account access
     - Investigate for additional signs of unauthorized access
  4. Document findings and take appropriate remediation steps based on investigation.
Reference: https://www.wiz.io/blog/aws-vpc-endpoint-cloudtrail

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventType is AwsVpceEvent
  • eventCategory is NetworkActivity
  • userIdentity.type is AWSAccount
  • userIdentity does not contain arn
  • userIdentity contains principalId
  • recipientAccountId is present
  • userIdentity.accountId is present
  • recipientAccountId differs from field userIdentity.accountId

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
accountIduserIdentity.accountId

Response runbook

1. Identify the principal account and the accessed account from the alert context.

2. Verify if the cross-account access is expected and authorized:

- Check if the principal account is part of your organization

- Review IAM policies for the VPC Endpoint to confirm if cross-account access is intentional

- Check resource policies for the accessed service to confirm if the principal should have access

3. If the access is unexpected:

- Review the API calls made by the principal

- Check the VPC Endpoint configuration for potential misconfiguration

- Consider restricting VPC Endpoint access to prevent unauthorized cross-account access

- Investigate for additional signs of unauthorized access

4. Document findings and take appropriate remediation steps based on investigation.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "NetworkActivity",
  "eventName": "GetObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-03-01T00:00:00Z",
  "eventType": "AwsVpceEvent",
  "eventVersion": "1.08",
  "recipientAccountId": "222222222222",
  "requestParameters": {
    "bucketName": "example-bucket",
    "key": "sensitive-file.txt"
  },
  "responseElements": null,
  "sourceIPAddress": "10.0.0.1",
  "userIdentity": {
    "accountId": "111111111111",
    "principalId": "AROAEXAMPLE:session-name",
    "type": "AWSAccount"
  },
  "vpcEndpointAccountId": "222222222222",
  "vpcEndpointId": "vpce-EXAMPLE08c1b6b9b7"
}

Failed Root Console Login

#
Severity
high
Compliance
CIS 3.6
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Authentication, DemoThreatHunting, Credential Access:Brute Force
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

A Root console login failed.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


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


def title(event):
    return (
        f"AWS root login failed from [{event.get('sourceIPAddress')}] in account "
        f"[{event.get('recipientAccountId')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_console_root_login_failed.py
RuleID: "AWS.Console.RootLoginFailed"
DisplayName: "Failed Root Console Login"
Enabled: true
DedupPeriodMinutes: 15
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Authentication
  - DemoThreatHunting
  - Credential Access:Brute Force
Threshold: 5
Reports:
  CIS:
    - 3.6
  MITRE ATT&CK:
    - TA0006:T1110
Severity: High
Description: A Root console login failed.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-console-login-failed
Reference: https://amzn.to/3aMSmTd
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ConsoleLogin
  • userIdentity.type is Root
  • responseElements.ConsoleLogin is Failure
Alert cadence
alerts after 5 matches within 15m

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-console-login-failed

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/",
    "MFAUsed": "No",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestParameters": null,
  "responseElements": {
    "ConsoleLogin": "Failure"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:root",
    "principalId": "1111",
    "type": "Root",
    "userName": "root"
  }
}

IAM Administrator Role Policy Attached

#
Severity
informational
Compliance
CIS 1.1
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM role policy was attached with Administrator Access, which could indicate a potential security risk.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not aws_cloudtrail_success(event) or event.get("eventName") != "AttachRolePolicy":
        return False

    policy = event.deep_get("requestParameters", "policyArn", default="POLICY_NOT_FOUND")

    return policy.endswith("AdministratorAccess")


def alert_context(event):
    context = aws_rule_context(event)
    context["request_rolename"] = event.deep_get(
        "requestParameters", "roleName", default="ROLENAME_NOT_FOUND"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_iam_attach_admin_role_policy.py
RuleID: "AWS.IAM.AttachAdminRolePolicy"
DisplayName: "IAM Administrator Role Policy Attached"
Enabled: true
LogTypes:
  - AWS.CloudTrail
CreateAlert: false
Reports:
  CIS:
    - 1.1
  MITRE ATT&CK:
    - TA0007:T1078
Severity: Info
Description: >
  An IAM role policy was attached with Administrator Access, which could indicate a potential security risk.
Runbook: Check if the action was expected. If not, remove the policy from the role.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is AttachRolePolicy
  • requestParameters.policyArn ends with AdministratorAccess

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneAttachRolePolicyexcludes:eventName field:"eventName" value:"AttachRolePolicy"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Check if the action was expected. If not, remove the policy from the role.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "AttachRolePolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "policyArn": "arn:aws:iam::aws:policy/AdministratorAccess",
    "roleName": "new-role"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

IAM Assume Role Blocklist Ignored

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Configuration Required, Identity and Access Management, Privilege Escalation:Abuse Elevation Control Mechanism
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A user assumed a role that was explicitly blocklisted for manual user assumption.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# This is a list of role ARNs that should not be assumed by users in normal operations
ASSUME_ROLE_BLOCKLIST = [
    "arn:aws:iam::123456789012:role/FullAdminRole",
]


def rule(event):
    # Only considering successful AssumeRole action
    if not aws_cloudtrail_success(event) or event.get("eventName") != "AssumeRole":
        return False

    # Only considering user actions
    if event.deep_get("userIdentity", "type") not in ["IAMUser", "FederatedUser"]:
        return False

    return event.deep_get("requestParameters", "roleArn") in ASSUME_ROLE_BLOCKLIST


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_assume_role_blocklist_ignored.py
RuleID: "AWS.CloudTrail.IAMAssumeRoleBlacklistIgnored"
DisplayName: "IAM Assume Role Blocklist Ignored"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Configuration Required
  - Identity and Access Management
  - Privilege Escalation:Abuse Elevation Control Mechanism
Reports:
  MITRE ATT&CK:
    - TA0004:T1548
Severity: High
Description: >
  A user assumed a role that was explicitly blocklisted for manual user assumption.
Runbook: >
  Verify that this was an approved assume role action. If not, consider revoking the access immediately and updating the AssumeRolePolicyDocument to prevent this from happening again.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is AssumeRole
  • userIdentity.type is one of IAMUser, FederatedUser
  • requestParameters.roleArn is one of arn:aws:iam::123456789012:role/FullAdminRole

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneAssumeRoleexcludes:eventName field:"eventName" value:"AssumeRole"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify that this was an approved assume role action. If not, consider revoking the access immediately and updating the AssumeRolePolicyDocument to prevent this from happening again.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1111",
  "eventName": "AssumeRole",
  "eventSource": "sts.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "durationSeconds": 900,
    "roleArn": "arn:aws:iam::123456789012:role/FullAdminRole",
    "roleSessionName": "1111"
  },
  "resources": [
    {
      "ARN": "arn:aws:iam::123456789012:role/FullAdminRole",
      "accountId": "123456789012",
      "type": "AWS::IAM::Role"
    }
  ],
  "responseElements": {
    "assumedRoleUser": {
      "arn": "arn:aws:sts::123456789012:assumed-role/FullAdminRole/1111",
      "assumedRoleId": "ABCD:1111"
    },
    "credentials": {
      "accessKeyId": "1111",
      "expiration": "Jan 01, 2019 0:00:00 PM",
      "sessionToken": "1111"
    }
  },
  "sharedEventID": "1111",
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "aws-sdk-go/1.4.14 (go1.11.4; darwin; amd64)",
  "userIdentity": {
    "accesKeyId": "1111",
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/example-user",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      }
    },
    "type": "IAMUser",
    "userName": "example-user"
  }
}

IAM Change

#
Severity
informational
Compliance
Stratus Red Team aws.persistence.iam-backdoor-role, aws.persistence.iam-backdoor-user, aws.persistence.iam-create-admin-user, aws.persistence.iam-create-backdoor-role, aws.persistence.iam-create-user-login-profile, aws.privilege-escalation.iam-update-user-login-profile
Log types
AWS.CloudTrail
Tags
AWS, Identity and Access Management
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A change occurred in the IAM configuration. This could be a resource being created, deleted, or modified. This is a high level view of changes, helfpul to indicate how dynamic a certain IAM environment is.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

IAM_CHANGE_ACTIONS = [
    "Add",
    "Attach",
    "Change",
    "Create",
    "Deactivate",
    "Delete",
    "Detach",
    "Enable",
    "Put",
    "Remove",
    "Set",
    "Update",
    "Upload",
]


def rule(event):
    # Only check IAM events, as the next check is relatively computationally
    # expensive and can often be skipped
    if not aws_cloudtrail_success(event) or event.get("eventSource") != "iam.amazonaws.com":
        return False

    return any((event.get("eventName", "").startswith(action) for action in IAM_CHANGE_ACTIONS))


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_anything_changed.py
RuleID: "AWS.CloudTrail.IAMAnythingChanged"
DisplayName: "IAM Change"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity and Access Management
Reports:
  Stratus Red Team:
    - aws.persistence.iam-backdoor-role
    - aws.persistence.iam-backdoor-user
    - aws.persistence.iam-create-admin-user
    - aws.persistence.iam-create-backdoor-role
    - aws.persistence.iam-create-user-login-profile
    - aws.privilege-escalation.iam-update-user-login-profile
Severity: Info
DedupPeriodMinutes: 720 # 12 hours
Description: >
  A change occurred in the IAM configuration. This could be a resource being created, deleted, or modified. This is a high level view of changes, helfpul to indicate how dynamic a certain IAM environment is.
Runbook: >
  Ensure this was an approved IAM configuration change.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/cloudtrail-integration.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is iam.amazonaws.com
  • any of:
    • eventName starts with Add
    • eventName starts with Attach
    • eventName starts with Change
    • eventName starts with Create
    • eventName starts with Deactivate
    • eventName starts with Delete
    • eventName starts with Detach
    • eventName starts with Enable
    • eventName starts with Put
    • eventName starts with Remove
    • eventName starts with Set
    • eventName starts with Update
    • eventName starts with Upload
Alert deduplication
repeat matches within 12h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventSourceneiam.amazonaws.comexcludes:eventSource field:"eventSource" value:"iam.amazonaws.com"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamestarts_with
  • Add
  • Attach
  • Change
  • Create
  • Deactivate
  • Delete
  • Detach
  • Enable
  • Put
  • Remove
  • Set
  • Update
  • Upload
field:"aws::eventName" kind:starts_with

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Ensure this was an approved IAM configuration change.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1111",
  "eventName": "AttachRolePolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "policyArn": "arn:aws:iam::aws:policy/example-policy",
    "roleName": "LambdaFunctionRole-1111"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "cloudformation.amazonaws.com",
  "userIdentity": {
    "accesKeyId": "1111",
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "invokedBy": "cloudformation.amazonaws.com",
    "principalId": "1111:example-user",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-user"
      }
    },
    "type": "AssumedRole"
  }
}

IAM Entity Created Without CloudFormation

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Configuration Required, Identity and Access Management, Persistence:Create Account
Reference
blog.awsfundamentals.com
Source
github.com/panther-labs/panther-analysis

An IAM Entity (Group, Policy, Role, or User) was created manually. IAM entities should be created in code to ensure that permissions are tracked and managed correctly.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import re

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# The role dedicated for IAM administration
IAM_ADMIN_ROLES = {
    "arn:aws:iam::123456789012:role/IdentityCFNServiceRole",
}

# The role patterns dedicated for IAM Service Roles
IAM_ADMIN_ROLE_PATTERNS = {"arn:aws:iam::[0-9]+:role/IdentityCFNServiceRole"}

# API calls that are indicative of IAM entity creation
IAM_ENTITY_CREATION_EVENTS = {
    "BatchCreateUser",
    "CreateGroup",
    "CreateInstanceProfile",
    "CreatePolicy",
    "CreatePolicyVersion",
    "CreateRole",
    "CreateServiceLinkedRole",
    "CreateUser",
}


def rule(event):
    # Check if this event is in scope
    if (
        not aws_cloudtrail_success(event)
        or event.get("eventName") not in IAM_ENTITY_CREATION_EVENTS
    ):
        return False

    # All IAM changes MUST go through CloudFormation
    if event.deep_get("userIdentity", "invokedBy") != "cloudformation.amazonaws.com":
        return True

    # Only approved IAM Roles can make IAM Changes
    for admin_role_pattern in IAM_ADMIN_ROLE_PATTERNS:
        # Check if the arn matches any role patterns, return False if there is a match
        if (
            len(
                re.findall(
                    admin_role_pattern,
                    event.deep_get("userIdentity", "sessionContext", "sessionIssuer", "arn"),
                )
            )
            > 0
        ):
            return False

    return (
        event.deep_get("userIdentity", "sessionContext", "sessionIssuer", "arn")
        not in IAM_ADMIN_ROLES
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_entity_created_without_cloudformation.py
RuleID: "AWS.CloudTrail.IAMEntityCreatedWithoutCloudFormation"
DisplayName: "IAM Entity Created Without CloudFormation"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0003:T1136
Tags:
  - AWS
  - Configuration Required
  - Identity and Access Management
  - Persistence:Create Account
Severity: Medium
Description: >
  An IAM Entity (Group, Policy, Role, or User) was created manually. IAM entities should be created in code to ensure that permissions are tracked and managed correctly.
Runbook: >
  Verify whether IAM entity needs to exist. If so, re-create it in an appropriate CloudFormation, Terraform, or other template. Delete the original manually created entity.
Reference: https://blog.awsfundamentals.com/aws-iam-roles-with-aws-cloudformation
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of BatchCreateUser, CreateGroup, CreateInstanceProfile, CreatePolicy, CreatePolicyVersion (+3 more values, see Indicators below)
  • any of:
    • userIdentity.invokedBy is not cloudformation.amazonaws.com
    • userIdentity.sessionContext.sessionIssuer.arn is not one of arn:aws:iam::123456789012:role/IdentityCFNServiceRole

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
eventNameinBatchCreateUser, CreateGroup, CreateInstanceProfile, CreatePolicy, CreatePolicyVersion, CreateRole, CreateServiceLinkedRole, CreateUserexcludes:eventName
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • BatchCreateUser
  • CreateGroup
  • CreateInstanceProfile
  • CreatePolicy
  • CreatePolicyVersion
  • CreateRole
  • CreateServiceLinkedRole
  • CreateUser
field:"aws::eventName" kind:in
userIdentity.invokedByne
  • cloudformation.amazonaws.com
field:"userIdentity.invokedBy" kind:ne value:"cloudformation.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify whether IAM entity needs to exist. If so, re-create it in an appropriate CloudFormation, Terraform, or other template. Delete the original manually created entity.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "CreateUser",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "path": "/",
    "userName": "user"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/IdentityCFNServiceRole",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

IAM Inline Policy Network Admin

#
Severity
medium
Compliance
PCI 1.1.5, 2.2.4, 7.1.2
Resource types
AWS.IAM.User, AWS.IAM.Role, AWS.IAM.Group
Tags
AWS, PCI, Persistence:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

This policy validates that IAM entities (Groups, Roles, and Users) do not have inline policies attached that grant network admin privileges. Inline policies are more difficult to track and audit than managed policies, and can lead to persistent unexpected access.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

import json

from policyuniverse.action_categories import categories_for_actions
from policyuniverse.expander_minimizer import expand_policy
from policyuniverse.policy import Policy

# White listed policies (e.g. the approved network admin policy) can be specified here, or as an
# exception to this policy.

ADMIN_ACTIONS = {
    "Tagging",
    "Write",
}
NETWORK_RESOURCES = {
    "dhcpoptions",
    "internetgateway",
    "networkacl",
    "networkinterface",
    "routetable",
    "securitygroup",
    "subnet",
    "transitgateway",
    "vpc",
    "vpn",
}


def is_ec2_admin_policy(iam_policy):
    #
    # These first two checks can technically be skipped and this policy will still return correct
    # results, but they prevent the more computationally expensive check the majority of the time.
    #
    action_summary = iam_policy.action_summary()

    # Check if the policy applies to EC2 resources
    if "ec2" not in action_summary:
        return False

    # Check if the policy grants administrative privileges
    if not ADMIN_ACTIONS.intersection(action_summary["ec2"]):
        return False

    # Get the EC2 actions pertaining specifically to network resources
    network_actions = set()
    for statement in iam_policy.statements:
        # Only check statements granting access
        if statement.effect != "Allow":
            continue
        # Only check actions that are granted on network resources
        for action in statement.actions:
            if any(resource in action for resource in NETWORK_RESOURCES):
                network_actions.add(action)

    # For all actions that have been granted on network resources, ensure none grant admin access
    network_actions_summary = categories_for_actions(network_actions)
    return any(action in ADMIN_ACTIONS for action in network_actions_summary["ec2"])


def policy(resource):
    # This policy only applies to resources with an inline policy document
    if resource["InlinePolicies"] is None:
        return True

    for inline_policy in resource["InlinePolicies"].values():
        iam_policy = Policy(expand_policy(json.loads(inline_policy)))
        if is_ec2_admin_policy(iam_policy):
            return False

    return True

Rule specification

AnalysisType: policy
Filename: aws_iam_inline_policy_does_not_grant_network_admin_access.py
PolicyID: "AWS.IAM.Entity.InlinePolicyDoesNotGrantNetworkAdminAccess"
DisplayName: "IAM Inline Policy Network Admin"
Enabled: true
ResourceTypes:
  - AWS.IAM.User
  - AWS.IAM.Role
  - AWS.IAM.Group
Tags:
  - AWS
  - PCI
  - Persistence:Valid Accounts
Reports:
  PCI:
    - 1.1.5
    - 2.2.4
    - 7.1.2
  MITRE ATT&CK:
    - TA0003:T1078
Severity: Medium
Description: >
  This policy validates that IAM entities (Groups, Roles, and Users) do not have inline policies attached that grant network admin privileges. Inline policies are more difficult to track and audit than managed policies, and can lead to persistent unexpected access.
Runbook: >
  Remove the inline policy, and if the IAM entity needs the provided permissions create an IAM managed policy with those permissions and apply it to the entity.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html

Stages and Predicates

Flags AWS.IAM.User, AWS.IAM.Role, AWS.IAM.Group resources when the condition below holds.

Condition

  • InlinePolicies is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InlinePoliciesis_null(no value, null check)excludes:InlinePolicies

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InlinePoliciesis_not_null
  • (no value, null check)
field:"InlinePolicies" kind:is_not_null

Response runbook

Remove the inline policy, and if the IAM entity needs the provided permissions create an IAM managed policy with those permissions and apply it to the entity.

IAM Policy Modified

#
Severity
informational
Group by
recipientAccountId
Compliance
CIS 3.4; Stratus Red Team aws.persistence.iam-create-admin-user, aws.persistence.iam-create-backdoor-role
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Privilege Escalation:Abuse Elevation Control Mechanism
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM Policy was changed.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of IAM Policy changes
POLICY_CHANGE_EVENTS = {
    "DeleteGroupPolicy",
    "DeleteRolePolicy",
    "DeleteUserPolicy",
    # Put<Entity>Policy is for inline policies.
    # these can be moved into their own rule if inline policies are of a greater concern.
    "PutGroupPolicy",
    "PutRolePolicy",
    "PutUserPolicy",
    "CreatePolicy",
    "DeletePolicy",
    "CreatePolicyVersion",
    "DeletePolicyVersion",
    "AttachRolePolicy",
    "DetachRolePolicy",
    "AttachUserPolicy",
    "DetachUserPolicy",
    "AttachGroupPolicy",
    "DetachGroupPolicy",
}


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in POLICY_CHANGE_EVENTS


def dedup(event):
    return event.get("recipientAccountId")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_policy_modified.py
RuleID: "AWS.IAM.PolicyModified"
DisplayName: "IAM Policy Modified"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Privilege Escalation:Abuse Elevation Control Mechanism
Reports:
  CIS:
    - 3.4
  MITRE ATT&CK:
    - TA0004:T1548

  Stratus Red Team:
    - aws.persistence.iam-create-admin-user
    - aws.persistence.iam-create-backdoor-role
Severity: Info
DedupPeriodMinutes: 720 # 12 hours
Description: >
  An IAM Policy was changed.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-iam-policy-modified
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of DeleteGroupPolicy, DeleteRolePolicy, DeleteUserPolicy, PutGroupPolicy, PutRolePolicy (+11 more values, see Indicators below)
Alert deduplication
repeat matches within 12h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AttachGroupPolicy
  • AttachRolePolicy
  • AttachUserPolicy
  • CreatePolicy
  • CreatePolicyVersion
  • DeleteGroupPolicy
  • DeletePolicy
  • DeletePolicyVersion
  • DeleteRolePolicy
  • DeleteUserPolicy
  • DetachGroupPolicy
  • DetachRolePolicy
  • DetachUserPolicy
  • PutGroupPolicy
  • PutRolePolicy
  • PutUserPolicy
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-iam-policy-modified

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "DeleteGroupPolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "groupName": "group",
    "policyName": "policy"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

IAM Role Added to RDS Instance or Cluster

#
Severity
medium
Group by
awsRegion, recipientAccountId, requestParameters.dBInstanceIdentifier
Log types
AWS.CloudTrail
Tags
AWS, Persistence, Privilege Escalation, Additional Cloud Credentials, RDS
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when IAM roles are added to RDS instances or clusters. While legitimate for features like S3 import/export, attackers may add overly permissive roles to maintain access or escalate privileges for data exfiltration.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_rds_context


def rule(event):
    if event.get("eventSource") != "rds.amazonaws.com":
        return False
    role_events = ["AddRoleToDBInstance", "AddRoleToDBCluster"]
    if event.get("eventName") not in role_events:
        return False
    return event.deep_get("errorCode") is None


def title(event):
    event_name = event.get("eventName", "Unknown")
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="<UNKNOWN>"
    )
    role_arn = event.deep_get("requestParameters", "roleArn", default="<UNKNOWN_ROLE>")
    user = event.deep_get("userIdentity", "userName") or event.deep_get(
        "userIdentity", "principalId", default="<UNKNOWN_USER>"
    )
    resource_type = "Instance" if event_name == "AddRoleToDBInstance" else "Cluster"
    return f"IAM Role Added to RDS {resource_type}: [{db_identifier}] role [{role_arn}] by [{user}]"


def dedup(event):
    db_identifier = event.deep_get("requestParameters", "dBInstanceIdentifier") or event.deep_get(
        "requestParameters", "dBClusterIdentifier", default="unknown"
    )
    account_id = event.deep_get("recipientAccountId", default="unknown")
    region = event.get("awsRegion", "unknown")
    return f"{account_id}:{region}:{db_identifier}"


def alert_context(event):
    context = aws_rds_context(event)
    context["role_arn"] = event.deep_get("requestParameters", "roleArn", default="N/A")
    context["feature_name"] = event.deep_get("requestParameters", "featureName", default="N/A")
    return context

Rule specification

AnalysisType: rule
Filename: aws_rds_iam_role_added.py
RuleID: "AWS.RDS.IAMRoleAdded"
DisplayName: "IAM Role Added to RDS Instance or Cluster"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Persistence
  - Privilege Escalation
  - Additional Cloud Credentials
  - RDS
Severity: Medium
Description: >
  Detects when IAM roles are added to RDS instances or clusters. While legitimate for features
  like S3 import/export, attackers may add overly permissive roles to maintain access or
  escalate privileges for data exfiltration.
Runbook: |
  1. Find all IAM role additions by the user ARN in the past 24 hours
  2. Check if this user has added IAM roles to databases in the past 90 days to determine if this is normal behavior
  3. Look for data export or modification operations from this database in the 48 hours after the role was added
Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PostgreSQL.S3Import.html
Reports:
  MITRE ATT&CK:
    - TA0003:T1098.001  # Additional Cloud Credentials
    - TA0004:T1078.004  # Cloud Accounts
DedupPeriodMinutes: 60
SummaryAttributes:
  - eventName
  - userIdentity:principalId
  - requestParameters:dBInstanceIdentifier
  - requestParameters:dBClusterIdentifier
  - requestParameters:roleArn
  - p_any_aws_account_ids
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is rds.amazonaws.com
  • eventName is one of AddRoleToDBInstance, AddRoleToDBCluster
  • errorCode is empty

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
errorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
eventNamein
  • AddRoleToDBCluster
  • AddRoleToDBInstance
field:"aws::eventName" kind:in
eventSourceeq
  • rds.amazonaws.com
field:"aws::eventSource" kind:eq value:"rds.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
dBInstanceIdentifierrequestParameters.dBInstanceIdentifier
roleArnrequestParameters.roleArn
userNameuserIdentity.userName

Response runbook

1. Find all IAM role additions by the user ARN in the past 24 hours

2. Check if this user has added IAM roles to databases in the past 90 days to determine if this is normal behavior

3. Look for data export or modification operations from this database in the 48 hours after the role was added

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "eventName": "AddRoleToDBInstance",
  "eventSource": "rds.amazonaws.com",
  "eventTime": "2024-01-17T13:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestParameters": {
    "dBInstanceIdentifier": "production-postgres",
    "featureName": "s3Import",
    "roleArn": "arn:aws:iam::123456789012:role/RDS-S3-Import-Role"
  },
  "responseElements": {
    "associatedRoles": [
      {
        "featureName": "s3Import",
        "roleArn": "arn:aws:iam::123456789012:role/RDS-S3-Import-Role",
        "status": "PENDING"
      }
    ],
    "dBInstanceIdentifier": "production-postgres"
  },
  "sourceIPAddress": "10.0.1.100",
  "userAgent": "aws-cli/2.13.0",
  "userIdentity": {
    "accessKeyId": "ASIAIOSFODNN7EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/DBARole/user",
    "principalId": "AIDAI23HXS3EXAMPLE:user",
    "type": "AssumedRole"
  }
}

IAM Role Created

#
Severity
informational
Compliance
CIS 1.1
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM role was created.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") == "CreateRole"


def alert_context(event):
    context = aws_rule_context(event)
    context["request_rolename"] = event.deep_get(
        "requestParameters", "roleName", default="ROLENAME_NOT_FOUND"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_iam_create_role.py
RuleID: "AWS.IAM.CreateRole"
DisplayName: "IAM Role Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
CreateAlert: false
Reports:
  CIS:
    - 1.1
  MITRE ATT&CK:
    - TA0007:T1078
Severity: Info
Description: >
  An IAM role was created.
Runbook: Check if the action was expected.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is CreateRole

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Check if the action was expected.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateRole",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "roleName": "new-role"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

IAM Role Policy Updated to Allow Internet Access

#
Severity
medium
Compliance
CIS 1.1
Log types
AWS.CloudTrail
Tags
AWS, Security Control, IAM
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM role policy was updated to allow internet access, which could indicate a backdoor.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Detection logic

import json

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from policyuniverse.policy import Policy


def rule(event):
    if not aws_cloudtrail_success(event) or event.get("eventName") != "UpdateAssumeRolePolicy":
        return False

    policy = event.deep_get("requestParameters", "policyDocument", default="{}")

    return Policy(json.loads(policy)).is_internet_accessible()


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_iam_backdoor_role.py
RuleID: "AWS.IAM.BackdoorRole"
DisplayName: "IAM Role Policy Updated to Allow Internet Access"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - IAM
Reports:
  CIS:
    - 1.1
  MITRE ATT&CK:
    - TA0007:T1078
Severity: Medium
Description: >
  An IAM role policy was updated to allow internet access, which could indicate a backdoor.
Runbook: Check if the action was authorized and if the policy was updated by a trusted user. If not, revert the policy and investigate the user
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is UpdateAssumeRolePolicy

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneUpdateAssumeRolePolicyexcludes:eventName field:"eventName" value:"UpdateAssumeRolePolicy"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Check if the action was authorized and if the policy was updated by a trusted user. If not, revert the policy and investigate the user

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "UpdateAssumeRolePolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"sts:AssumeRole\",\"Condition\":{\"StringEquals\":{\"sts:ExternalId\":\"12345\"}}}]}"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

IAM User Created

#
Severity
informational
Compliance
CIS 1.1
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM user was created, which could indicate a new user creation or policy update.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") == "CreateUser"


def alert_context(event):
    context = aws_rule_context(event)
    context["request_username"] = event.deep_get(
        "requestParameters", "userName", default="USERNAME_NOT_FOUND"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_iam_create_user.py
RuleID: "AWS.IAM.CreateUser"
DisplayName: "IAM User Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  CIS:
    - 1.1
  MITRE ATT&CK:
    - TA0007:T1078
Severity: Info
CreateAlert: false
Description: >
  An IAM user was created, which could indicate a new user creation or policy update.
Runbook: Check if the user was created by an authorized user. If not, investigate the user creation.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is CreateUser

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Check if the user was created by an authorized user. If not, investigate the user creation.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "CreateUser",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "userName": "new-user"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

IAM User Policy Attached with Administrator Access

#
Severity
informational
Compliance
CIS 1.1
Log types
AWS.CloudTrail
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An IAM user policy was attached with Administrator Access, which could indicate a potential security risk.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not aws_cloudtrail_success(event) or event.get("eventName") != "AttachUserPolicy":
        return False

    policy = event.deep_get("requestParameters", "policyArn", default="POLICY_NOT_FOUND")

    return policy.endswith("AdministratorAccess")


def alert_context(event):
    context = aws_rule_context(event)
    context["request_username"] = event.deep_get(
        "requestParameters", "userName", default="USERNAME_NOT_FOUND"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_iam_attach_admin_user_policy.py
RuleID: "AWS.IAM.AttachAdminUserPolicy"
DisplayName: "IAM User Policy Attached with Administrator Access"
Enabled: true
CreateAlert: false
LogTypes:
  - AWS.CloudTrail
Reports:
  CIS:
    - 1.1
  MITRE ATT&CK:
    - TA0007:T1078
Severity: Info
Description: >
  An IAM user policy was attached with Administrator Access, which could indicate a potential security risk.
Runbook: Check if the user policy was attached by an authorized user. If not, investigate the user policy attachment.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is AttachUserPolicy
  • requestParameters.policyArn ends with AdministratorAccess

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneAttachUserPolicyexcludes:eventName field:"eventName" value:"AttachUserPolicy"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Check if the user policy was attached by an authorized user. If not, investigate the user policy attachment.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "AttachUserPolicy",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "policyArn": "arn:aws:iam::aws:policy/AdministratorAccess",
    "userName": "new-user"
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "console.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "Tester"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

KMS CMK Disabled or Deleted

#
Severity
informational
Group by
eventName
Compliance
CIS 3.7
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A KMS Customer Managed Key was disabled or scheduled for deletion. This could potentially lead to permanent loss of encrypted data.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

# API calls that are indicative of KMS CMK Deletion
KMS_LOSS_EVENTS = {"DisableKey", "ScheduleKeyDeletion"}
KMS_KEY_TYPE = "AWS::KMS::Key"


def rule(event):
    return aws_cloudtrail_success(event) and event.get("eventName") in KMS_LOSS_EVENTS


def dedup(event):
    for resource in event.get("resources") or []:
        if resource.get("type", "") == KMS_KEY_TYPE:
            return resource.get("ARN")
    return event.get("eventName")


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_kms_cmk_loss.py
RuleID: "AWS.KMS.CustomerManagedKeyLoss"
DisplayName: "KMS CMK Disabled or Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Impact:Data Destruction
Reports:
  CIS:
    - 3.7
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Info
Description: >
  A KMS Customer Managed Key was disabled or scheduled for deletion. This could potentially lead to permanent loss of encrypted data.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-kms-cmk-loss
Reference: https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventName is one of DisableKey, ScheduleKeyDeletion

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • DisableKey
  • ScheduleKeyDeletion
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-kms-cmk-loss

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "DisableKey",
  "eventSource": "kms.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "keyId": "1"
  },
  "resources": [
    {
      "ARN": "arn:aws:kms:us-west-2:123456789012:key/1",
      "accountId": "123456789012",
      "type": "AWS::KMS::Key"
    }
  ],
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/tester",
    "principalId": "1111:tester",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/tester",
        "principalId": "1111",
        "type": "Role",
        "userName": "tester"
      }
    },
    "type": "AssumedRole"
  }
}

Lambda Code Updated by User

#
Status
Experimental
Severity
high
Log types
AWS.CloudTrail
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when Lambda function code is updated by an IAM user, federated user, or AWS SSO user. This may indicate compromised credentials, a developer bypassing CI/CD guardrails, or insider threat activity.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "lambda.amazonaws.com"
        and event.get("eventName").startswith("UpdateFunctionCode")
    ):
        return False

    identity_type = event.deep_get("userIdentity", "type", default="")
    if identity_type in ("IAMUser", "FederatedUser"):
        return True
    if identity_type == "AssumedRole":
        role_name = event.deep_get(
            "userIdentity", "sessionContext", "sessionIssuer", "userName", default=""
        )
        return role_name.startswith("AWSReservedSSO_")
    return False


def title(event):
    lambda_name = event.deep_get(
        "responseElements", "functionName", default="LAMBDA_NAME_NOT_FOUND"
    )
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"updated Lambda function code for [{lambda_name}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["identity_type"] = event.deep_get("userIdentity", "type")
    context["user_arn"] = event.deep_get("userIdentity", "arn")

    return context

Rule specification

AnalysisType: rule
Filename: aws_overwrite_lambda_code.py
RuleID: "AWS.Lambda.UpdateFunctionCode"
DisplayName: "Lambda Code Updated by User"
Enabled: true
CreateAlert: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0007:T1078
Severity: High
Status: Experimental
Description: >
  Detects when Lambda function code is updated by an IAM user, federated user, or AWS SSO user.
  This may indicate compromised credentials, a developer bypassing CI/CD guardrails,
  or insider threat activity.
Runbook: |
  Verify the user identity and whether this is authorized.
  Review the code changes made to the Lambda function.
  If unauthorized:
  - Disable the user's access key or revoke the SSO session
  - Revert Lambda function code to previous version
  - Investigate credential compromise
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.persistence.lambda-overwrite-code/

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is lambda.amazonaws.com
  • eventName starts with UpdateFunctionCode
  • any of:
    • userIdentity.type is one of IAMUser, FederatedUser
    • all of:
      • userIdentity.type is AssumedRole
      • userIdentity.sessionContext.sessionIssuer.userName starts with AWSReservedSSO_

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
functionNameresponseElements.functionName

Response runbook

Verify the user identity and whether this is authorized.

Review the code changes made to the Lambda function.

If unauthorized:

- Disable the user's access key or revoke the SSO session

- Revert Lambda function code to previous version

- Investigate credential compromise

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "UpdateFunctionCode20150331v2",
  "eventSource": "lambda.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "p_log_type": "AWS.CloudTrail",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "functionName": "production-api"
  },
  "responseElements": {
    "functionName": "production-api"
  },
  "sourceIPAddress": "203.0.113.50",
  "userAgent": "aws-cli/2.13.5 Python/3.11.4",
  "userIdentity": {
    "accessKeyId": "AKIAI123456789EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/john.developer",
    "principalId": "AIDAI123456789EXAMPLE",
    "type": "IAMUser",
    "userName": "john.developer"
  }
}

Lambda Configuration Updated with Layers by User

#
Severity
high
Log types
AWS.CloudTrail
Reference
stratus-red-team.cloud
Source
github.com/panther-labs/panther-analysis

Detects when Lambda function configuration is updated with layers by an IAM user, federated user, or AWS SSO user. Lambda layers can execute code before the main function, making them a stealthy persistence mechanism. This may indicate compromised credentials, a developer bypassing CI/CD guardrails, or insider threat activity.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    if not (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "lambda.amazonaws.com"
        and event.get("eventName") == "UpdateFunctionConfiguration20150331v2"
        and event.deep_get("responseElements", "layers")
    ):
        return False

    identity_type = event.deep_get("userIdentity", "type", default="")
    if identity_type in ("IAMUser", "FederatedUser"):
        return True
    if identity_type == "AssumedRole":
        role_name = event.deep_get(
            "userIdentity", "sessionContext", "sessionIssuer", "userName", default=""
        )
        return role_name.startswith("AWSReservedSSO_")
    return False


def title(event):
    lambda_name = event.deep_get(
        "responseElements", "functionName", default="LAMBDA_NAME_NOT_FOUND"
    )
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"updated Lambda function configuration with layers for [{lambda_name}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["identity_type"] = event.deep_get("userIdentity", "type")
    context["user_arn"] = event.deep_get("userIdentity", "arn")

    layers = event.deep_get("responseElements", "layers", default=[])
    context["layer_arns"] = [layer.get("arn") for layer in layers]

    return context

Rule specification

AnalysisType: rule
Filename: aws_add_malicious_lambda_extension.py
RuleID: "AWS.Lambda.UpdateFunctionConfiguration"
DisplayName: "Lambda Configuration Updated with Layers by User"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0007:T1078
Severity: High
CreateAlert: true
Description: >
  Detects when Lambda function configuration is updated with layers by an IAM user,
  federated user, or AWS SSO user. Lambda layers can execute code before the main function,
  making them a stealthy persistence mechanism. This may indicate compromised credentials,
  a developer bypassing CI/CD guardrails, or insider threat activity.
Runbook: |
  Verify the user identity and layer ARNs are authorized.
  Check if layers are from trusted sources (same account, known publishers).
  If unauthorized:
  - Disable the user's access key or revoke the SSO session
  - Revert Lambda configuration to previous version
  - Investigate credential compromise
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.persistence.lambda-layer-extension/

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is lambda.amazonaws.com
  • eventName is UpdateFunctionConfiguration20150331v2
  • responseElements.layers is present
  • any of:
    • userIdentity.type is one of IAMUser, FederatedUser
    • all of:
      • userIdentity.type is AssumedRole
      • userIdentity.sessionContext.sessionIssuer.userName starts with AWSReservedSSO_

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
functionNameresponseElements.functionName

Response runbook

Verify the user identity and layer ARNs are authorized.

Check if layers are from trusted sources (same account, known publishers).

If unauthorized:

- Disable the user's access key or revoke the SSO session

- Revert Lambda configuration to previous version

- Investigate credential compromise

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "UpdateFunctionConfiguration20150331v2",
  "eventSource": "lambda.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "p_log_type": "AWS.CloudTrail",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "functionName": "production-api"
  },
  "responseElements": {
    "functionName": "production-api",
    "layers": [
      {
        "arn": "arn:aws:lambda:us-west-2:123456789012:layer:malicious-layer:1"
      }
    ]
  },
  "sourceIPAddress": "203.0.113.50",
  "userAgent": "aws-cli/2.13.5 Python/3.11.4",
  "userIdentity": {
    "accessKeyId": "AKIAI123456789EXAMPLE",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/john.developer",
    "principalId": "AIDAI123456789EXAMPLE",
    "type": "IAMUser",
    "userName": "john.developer"
  }
}

Lambda CRUD Actions

#
Severity
high
Group by
userIdentity.arn
Compliance
CIS 3.12
Log types
AWS.CloudTrail
Tags
AWS, Security Control, Configuration Required
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Unauthorized lambda Create, Read, Update, or Delete event occurred.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from fnmatch import fnmatch

from panther_aws_helpers import aws_rule_context

LAMBDA_CRUD_EVENTS = {
    "AddPermission",
    "CreateAlias",
    "CreateEventSourceMapping",
    "CreateFunction",
    "DeleteAlias",
    "DeleteEventSourceMapping",
    "DeleteFunction",
    "PublishVersion",
    "RemovePermission",
    "UpdateAlias",
    "UpdateEventSourceMapping",
    "UpdateFunctionCode",
    "UpdateFunctionConfiguration",
}

ALLOWED_ROLES = [
    "*DeployRole",
]


def rule(event):
    if (
        event.get("eventSource") == "lambda.amazonaws.com"
        and event.get("eventName") in LAMBDA_CRUD_EVENTS
    ):
        for role in ALLOWED_ROLES:
            if fnmatch(event.deep_get("userIdentity", "arn", default="unknown-arn"), role):
                return False
        return True
    return False


def title(event):
    return (
        f"[{event.deep_get('userIdentity','arn', default = 'unknown-arn')}] "
        f"performed Lambda "
        f"[{event.get('eventName')}] in "
        f"[{event.get('recipientAccountId')} {event.get('awsRegion')}]."
    )


def dedup(event):
    return f"{event.deep_get('userIdentity','arn', default = 'unknown-arn')}"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_lambda_crud.py
RuleID: "AWS.LAMBDA.CRUD"
DisplayName: "Lambda CRUD Actions"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Security Control
  - Configuration Required
Reports:
  CIS:
    - 3.12
  MITRE ATT&CK:
    - TA0005:T1525
Severity: High
Description: Unauthorized lambda Create, Read, Update, or Delete event occurred.
Runbook: https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html
Reference: https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html
SummaryAttributes:
  - eventSource
  - eventName
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is lambda.amazonaws.com
  • eventName is one of AddPermission, CreateAlias, CreateEventSourceMapping, CreateFunction, DeleteAlias (+8 more values, see Indicators below)
  • userIdentity.arn does not match the pattern *DeployRole

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
userIdentity.arnends_withDeployRoleexcludes:userIdentity.arn field:"userIdentity.arn" value:"DeployRole"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • AddPermission
  • CreateAlias
  • CreateEventSourceMapping
  • CreateFunction
  • DeleteAlias
  • DeleteEventSourceMapping
  • DeleteFunction
  • PublishVersion
  • RemovePermission
  • UpdateAlias
  • UpdateEventSourceMapping
  • UpdateFunctionCode
  • UpdateFunctionConfiguration
field:"aws::eventName" kind:in
eventSourceeq
  • lambda.amazonaws.com
field:"aws::eventSource" kind:eq value:"lambda.amazonaws.com"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
arnuserIdentity.arn

Response runbook

https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "20b84ce5-730f-482e-b2b2-e8fcc87ceb22",
  "eventName": "DeleteFunction",
  "eventSource": "lambda.amazonaws.com",
  "eventTime": "2015-03-18T19:04:42Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.03",
  "recipientAccountId": "999999999999",
  "requestID": "a2198ecc-cda1-11e4-aaa2-e356da31e4ff",
  "requestParameters": {
    "functionName": "basic-node-task"
  },
  "responseElements": null,
  "sourceIPAddress": "127.0.0.1",
  "userAgent": "Python-httplib2/0.8 (gzip)",
  "userIdentity": {
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "accountId": "999999999999",
    "arn": "arn:aws:iam::999999999999:user/myUserName",
    "principalId": "A1B2C3D4E5F6G7EXAMPLE",
    "type": "IAMUser",
    "userName": "myUserName"
  }
}

Logins Without MFA

#
Severity
high
Compliance
CIS 3.2; Stratus Red Team aws.initial-access.console-login-without-mfa
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Authentication, Initial Access:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A console login was made without multi-factor authentication.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import logging

from panther_aws_helpers import aws_rule_context
from panther_detection_helpers.caching import check_account_age

# Set to True for environments that permit direct role assumption via external IDP
ROLES_VIA_EXTERNAL_IDP = False


# pylint: disable=R0911,R0912,R1260
def rule(event):
    if event.get("eventName") != "ConsoleLogin":
        return False

    # Extract some nested JSON structure
    additional_event_data = event.get("additionalEventData", {})
    response_elements = event.get("responseElements", {})
    user_identity_type = event.deep_get("userIdentity", "type", default="")

    # When there is an external IdP setup and users directly assume roles
    # the additionalData.MFAUsed attribute will be set to "no"
    #  AND the userIdentity.sessionContext.mfaAuthenticated attribute will be "false"
    #
    # This will create a lack of visibility into the condition where
    #  users are allowed to directly AssumeRole outside of the IdP and without MFA
    #
    # To date we have not identified data inside the log events that clearly
    #  delinates AssumeRole backed by an external IdP vs not backed by external IdP
    if ROLES_VIA_EXTERNAL_IDP and user_identity_type == "AssumedRole":
        return False

    # If using AWS SSOv2 or other SAML provider return False
    if (
        "AWSReservedSSO" in event.deep_get("userIdentity", "arn", default=" ")
        or additional_event_data.get("SamlProviderArn") is not None
    ):
        return False

    # If Account is less than 3 days old do not alert
    # This functionality is not enabled by default, in order to start logging new user creations
    # Enable indicator_creation_rules/new_account_logging to start logging new users
    new_user_string = (
        event.deep_get("userIdentity", "userName", default="<MISSING_USER_NAME>")
        + "-"
        + event.udm("actor_user")
    )
    is_new_user = check_account_age(new_user_string)
    if isinstance(is_new_user, str):
        logging.debug("check_account_age is a mocked string for unit testing")
        if is_new_user == "False":
            is_new_user = False
        if is_new_user == "True":
            is_new_user = True
    if is_new_user:
        return False

    new_account_string = "new_account - " + str(event.get("recipientAccountId"))
    is_new_account = check_account_age(new_account_string)
    if isinstance(is_new_account, str):
        logging.debug("check_account_age is a mocked string for unit testing")
        if is_new_account == "False":
            is_new_account = False
        if is_new_account == "True":
            is_new_account = True
    if is_new_account:
        return False

    if response_elements.get("ConsoleLogin") == "Success":
        # This logic is inverted because at times the second condition is None.
        # It is not recommended to remove this 'double negative"
        if (
            additional_event_data.get("MFAUsed") != "Yes"
            and event.deep_get("userIdentity", "sessionContext", "attributes", "mfaAuthenticated")
            != "true"
        ):
            return True
    return False


def title(event):
    if event.deep_get("userIdentity", "type") == "Root":
        user_string = "the root user"
    else:
        user = event.deep_get("userIdentity", "userName") or event.deep_get(
            "userIdentity", "sessionContext", "sessionIssuer", "userName"
        )
        type_ = event.deep_get(
            "userIdentity", "sessionContext", "sessionIssuer", "type", default="user"
        ).lower()
        user_string = f"{type_} {user}"
    account_id = event.get("recipientAccountId")

    return f"AWS login detected without MFA for [{user_string}] in [{account_id}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_console_login_without_mfa.py
RuleID: "AWS.Console.LoginWithoutMFA"
DisplayName: "Logins Without MFA"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Authentication
  - Initial Access:Valid Accounts
Reports:
  CIS:
    - 3.2
  MITRE ATT&CK:
    - TA0001:T1078

  Stratus Red Team:
    - aws.initial-access.console-login-without-mfa
Severity: High
Description: A console login was made without multi-factor authentication.
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-console-login-without-mfa
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ConsoleLogin
  • userIdentity.type is not AssumedRole
  • userIdentity.arn does not contain AWSReservedSSO
  • additionalEventData.SamlProviderArn is empty
  • responseElements.ConsoleLogin is Success
  • additionalEventData.MFAUsed is not Yes
  • userIdentity.sessionContext.attributes.mfaAuthenticated is not true

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
additionalEventData.SamlProviderArnis_not_null(no value, null check)excludes:additionalEventData.SamlProviderArn
userIdentity.arncontainsAWSReservedSSOexcludes:userIdentity.arn field:"userIdentity.arn" value:"AWSReservedSSO"
userIdentity.typeeqAssumedRoleexcludes:userIdentity.type field:"userIdentity.type" value:"AssumedRole"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-console-login-without-mfa

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/",
    "MFAUsed": "No",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "p_log_type": "AWS.CloudTrail",
  "recipientAccountId": "123456789012",
  "requestParameters": null,
  "responseElements": {
    "ConsoleLogin": "Success"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/tester",
    "principalId": "1111",
    "type": "IAMUser",
    "userName": "tester"
  }
}

Logins Without SAML

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Configuration Required, Identity & Access Management, Authentication, Initial Access:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

An AWS console login was made without SAML/SSO.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    additional_event_data = event.get("additionalEventData", {})
    return (
        event.get("eventName") == "ConsoleLogin"
        and event.deep_get("userIdentity", "type") != "AssumedRole"
        and not additional_event_data.get("SamlProviderArn")
    )


def title(event):
    return f"AWS logins without SAML in account " f"[{event.get('recipientAccountId')}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_console_login_without_saml.py
RuleID: "AWS.Console.LoginWithoutSAML"
DisplayName: "Logins Without SAML"
DedupPeriodMinutes: 60
Enabled: false
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
Tags:
  - AWS
  - Configuration Required
  - Identity & Access Management
  - Authentication
  - Initial Access:Valid Accounts
Severity: High
Description: An AWS console login was made without SAML/SSO.
Runbook: Modify the AWS account configuration.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-saml.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ConsoleLogin
  • userIdentity.type is not AssumedRole
  • additionalEventData.SamlProviderArn is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Modify the AWS account configuration.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/",
    "MFAUsed": "Yes",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestParameters": null,
  "responseElements": {
    "ConsoleLogin": "Success"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/tester",
    "principalId": "1111",
    "type": "IAMUser",
    "userName": "tester"
  }
}

Monitor Unauthorized API Calls

#
Severity
informational
Group by
actor_user
Compliance
CIS 3.1
Log types
AWS.CloudTrail
Tags
AWS, Discovery:Cloud Service Discovery
Reference
amzn.to
Source
github.com/panther-labs/panther-analysis

An unauthorized AWS API call was made

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

from ipaddress import ip_address

from panther_aws_helpers import aws_rule_context
from panther_misp_helpers import get_misp_warning_lists

# Do not alert on these access denied errors for these events.
# Events could be exceptions because they are particularly noisy and provide little to no value,
# or because they are expected as part of the normal operating procedure for certain tools.
EVENT_EXCEPTIONS = {
    "DescribeEventAggregates",  # Noisy, doesn't really provide any actionable info
    "ListResourceTags",  # The audit role hits this when scanning locked down resources
}


def rule(event):
    source_ip = event.get("sourceIPAddress")

    try:
        ip_address(source_ip)
    except ValueError:
        return False

    # Filter out known Amazon IP ranges
    misp_data = get_misp_warning_lists(event)
    if misp_data and misp_data.has_warning_list_id(source_ip, "amazon-aws"):
        return False

    return (
        event.get("errorCode") == "AccessDenied" and event.get("eventName") not in EVENT_EXCEPTIONS
    )


def dedup(event):
    return event.udm("actor_user")


def title(event):
    return f"Access denied to {event.deep_get('userIdentity', 'type')} [{dedup(event)}]"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_unauthorized_api_call.py
RuleID: "AWS.CloudTrail.UnauthorizedAPICall"
DisplayName: "Monitor Unauthorized API Calls"
Enabled: true
DedupPeriodMinutes: 1440 # 24 hours
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Discovery:Cloud Service Discovery
Reports:
  CIS:
    - 3.1
  MITRE ATT&CK:
    - TA0007:T1526
Severity: Info
CreateAlert: false
Description: An unauthorized AWS API call was made
Runbook: https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-unauthorized-api-call
Reference: https://amzn.to/3aOukaA
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns
Threshold: 20

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is AccessDenied
  • eventName is not one of DescribeEventAggregates, ListResourceTags

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 20 matches within 1d

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
eventNameinDescribeEventAggregates, ListResourceTagsexcludes:eventName field:"eventName" value:"DescribeEventAggregates" field:"eventName" value:"ListResourceTags"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
typeuserIdentity.type

Response runbook

https://docs.runpanther.io/alert-runbooks/built-in-rules/aws-unauthorized-api-call

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:iam::123456789012:user/tester is not authorized to perform: iam:Action on resource: arn:aws:iam::123456789012:resource",
  "eventID": "1",
  "eventName": "CreateServiceLinkedRole",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "p_log_type": "AWS.CloudTrail",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": null,
  "responseElements": null,
  "sourceIPAddress": "3.10.107.144",
  "userAgent": "sqs.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/tester",
    "invokedBy": "signin.amazonaws.com",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      }
    },
    "type": "IAMUser",
    "userName": "tester"
  }
}

New IAM Credentials Updated

#
Severity
informational
Group by
userIdentity.userName
Compliance
Stratus Red Team aws.persistence.iam-backdoor-user, aws.persistence.iam-create-admin-user, aws.persistence.iam-create-user-login-profile
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Persistence:Account Manipulation
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A console password, access key, or user has been created.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context

UPDATE_EVENTS = {"ChangePassword", "CreateAccessKey", "CreateLoginProfile", "CreateUser"}


def rule(event):
    return event.get("eventName") in UPDATE_EVENTS and aws_cloudtrail_success(event)


def dedup(event):
    return event.deep_get("userIdentity", "userName", default="<UNKNOWN_USER>")


def title(event):
    return (
        f"{event.deep_get('userIdentity', 'type')} [{event.deep_get('userIdentity', 'arn')}]"
        f" has updated their IAM credentials"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_update_credentials.py
RuleID: "AWS.IAM.CredentialsUpdated"
DisplayName: "New IAM Credentials Updated"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Reports:
  MITRE ATT&CK:
    - TA0003:T1098

  Stratus Red Team:
    - aws.persistence.iam-backdoor-user
    - aws.persistence.iam-create-admin-user
    - aws.persistence.iam-create-user-login-profile
Tags:
  - AWS
  - Identity & Access Management
  - Persistence:Account Manipulation
Severity: Info
Description: A console password, access key, or user has been created.
Runbook: This rule is purely informational, there is no action needed.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/list_identityandaccessmanagement.html
SummaryAttributes:
  - eventName
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is one of ChangePassword, CreateAccessKey, CreateLoginProfile, CreateUser
  • errorCode is empty
  • errorMessage is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • ChangePassword
  • CreateAccessKey
  • CreateLoginProfile
  • CreateUser
field:"aws::eventName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
typeuserIdentity.type
arnuserIdentity.arn

Response runbook

This rule is purely informational, there is no action needed.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "a431f05e-67e1-11ea-bc55-0242ac130003",
  "eventName": "ChangePassword",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-12-31T01:50:46Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "a431f05e-67e1-11ea-bc55-0242ac130003",
  "requestParameters": null,
  "responseElements": null,
  "sourceIPAddress": "64.25.27.224",
  "userAgent": "signin.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "AAAAIIIIIIU74NPJW5K76",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:user/test_user",
    "invokedBy": "signin.amazonaws.com",
    "principalId": "AAAAIIIIIIU74NPJW5K76",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-12-31T01:50:17Z",
        "mfaAuthenticated": "true"
      }
    },
    "type": "IAMUser",
    "userName": "test_user"
  }
}

Root Account Access Key Created

#
Severity
critical
Log types
AWS.CloudTrail
Tags
AWS, Identity and Access Management, Persistence:Account Manipulation
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects creation of programmatic access keys for the AWS root account, which violates critical security best practices. Root account credentials provide unrestricted access to all AWS resources and cannot be scoped with granular permissions. If compromised, these keys grant attackers complete control over the AWS environment including billing and account closure capabilities.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Only check access key creation events
    if event.get("eventName") != "CreateAccessKey":
        return False

    # Only root can create root access keys
    if event.deep_get("userIdentity", "type") != "Root":
        return False

    # Only alert if the root user is creating an access key for itself
    return event.get("requestParameters") is None


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_root_access_key_created.py
RuleID: "AWS.CloudTrail.RootAccessKeyCreated"
DisplayName: "Root Account Access Key Created"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity and Access Management
  - Persistence:Account Manipulation
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
Severity: Critical
Description: >
  Detects creation of programmatic access keys for the AWS root account, which violates critical security best practices. Root account credentials provide unrestricted access to all AWS resources and cannot be scoped with granular permissions. If compromised, these keys grant attackers complete control over the AWS environment including billing and account closure capabilities.
Runbook: |
  1. Query CloudTrail for all API calls where userIdentity.accessKeyId matches responseElements.accessKey.accessKeyId in the 24 hours after the key creation to identify all actions taken using the root access key
  2. Check if the root account credentials may be compromised by reviewing sourceIPAddress and userAgent against known legitimate access patterns, then verify with the account owner if this creation was authorized
  3. Search CloudTrail for IAM policy modifications, user creations, role changes, and resource deletions by userIdentity.type="Root" in the 6 hours around this event to identify unauthorized changes made using root credentials
Reference: https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is CreateAccessKey
  • userIdentity.type is Root
  • requestParameters is empty

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

1. Query CloudTrail for all API calls where userIdentity.accessKeyId matches responseElements.accessKey.accessKeyId in the 24 hours after the key creation to identify all actions taken using the root access key

2. Check if the root account credentials may be compromised by reviewing sourceIPAddress and userAgent against known legitimate access patterns, then verify with the account owner if this creation was authorized

3. Search CloudTrail for IAM policy modifications, user creations, role changes, and resource deletions by userIdentity.type="Root" in the 6 hours around this event to identify unauthorized changes made using root credentials

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1111",
  "eventName": "CreateAccessKey",
  "eventSource": "iam.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": null,
  "responseElements": {
    "accessKey": {
      "accessKeyId": "1111",
      "createDate": "Jan 01, 2019 0:00:00 PM",
      "status": "Active"
    }
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "signin.amazonaws.com",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:root",
    "invokedBy": "signin.amazonaws.com",
    "principalId": "123456789012",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "true"
      }
    },
    "type": "Root"
  }
}

Root Account Activity

#
Severity
high
Group by
sourceIPAddress
Entities
aws_account_ids, aws_arns, ip_addresses
Compliance
CIS 3.3
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, DemoThreatHunting, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Root account activity that modifies AWS resources or configuration was detected. Read-only root events (enumeration, console reads) are excluded — only impactful root actions trigger this rule.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Detection logic

from panther_aws_helpers import aws_cloudtrail_success

EVENT_ALLOW_LIST = {"CreateServiceLinkedRole"}


def rule(event):
    return (
        event.deep_get("userIdentity", "type") == "Root"
        and aws_cloudtrail_success(event)
        and event.deep_get("userIdentity", "invokedBy") is None
        and event.get("eventType") != "AwsServiceEvent"
        and event.get("eventName") not in EVENT_ALLOW_LIST
        and not event.get("readOnly")
    )


def dedup(event):
    return event.get("sourceIPAddress", "<UNKNOWN_IP>")


def title(event):
    return (
        "AWS root user activity "
        f"[{event.get('eventName')}] "
        "in account "
        f"[{event.get('recipientAccountId')}]"
    )


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

Rule specification

AnalysisType: rule
Filename: aws_root_activity.py
RuleID: "AWS.Root.Activity"
DisplayName: "Root Account Activity"
DedupPeriodMinutes: 60
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - DemoThreatHunting
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 3.3
  MITRE ATT&CK:
    - TA0004:T1078
Severity: High
Description: >
  Root account activity that modifies AWS resources or configuration was detected. Read-only root events (enumeration, console reads) are excluded — only impactful root actions trigger this rule.
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.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html
SummaryAttributes:
  - awsRegion
  - eventName
  - eventSource
  - userAgent
  - p_any_aws_account_ids
  - p_any_aws_arns
  - p_any_ip_addresses

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • userIdentity.type is Root
  • errorCode is empty
  • errorMessage is empty
  • userIdentity.invokedBy is empty
  • eventType is not AwsServiceEvent
  • eventName is not one of CreateServiceLinkedRole
  • readOnly is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameeqCreateServiceLinkedRoleexcludes:eventName field:"eventName" value:"CreateServiceLinkedRole"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
sourceIPAddress
userIdentityAccountIduserIdentity.accountId
userIdentityArnuserIdentity.arn
eventTime
mfaUsedadditionalEventData.MFAUsed
eventName
recipientAccountId

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

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "AuthenticationMethod": "AuthHeader",
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "SignatureVersion": "SigV4"
  },
  "awsRegion": "us-west-2",
  "eventID": "1",
  "eventName": "PutBucketVersioning",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1",
  "requestParameters": {
    "VersioningConfiguration": {
      "MfaDelete": "Enabled",
      "Status": "Enabled",
      "xmlns": "http://s3.amazonaws.com/doc/2006-03-01/"
    },
    "bucketName": "bucket",
    "host": [
      "bucket.s3.us-west-2.amazonaws.com"
    ],
    "versioning": [
      ""
    ]
  },
  "responseElements": null,
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accessKeyId": "1",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:root",
    "principalId": "123456789012",
    "sessionContext": {
      "attributes": {
        "creationDate": "2019-01-01T00:00:00Z",
        "mfaAuthenticated": "false"
      }
    },
    "type": "Root"
  }
}

Root Console Login

#
Severity
high
Group by
eventName, eventTime, recipientAccountId
Compliance
CIS 3.6
Log types
AWS.CloudTrail
Tags
AWS, Identity & Access Management, Authentication, DemoThreatHunting, Privilege Escalation:Valid Accounts
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

The root account has been logged into.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_ipinfo_helpers import geoinfo_from_ip_formatted


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


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


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


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

Rule specification

AnalysisType: rule
Filename: aws_console_root_login.py
RuleID: "AWS.Console.RootLogin"
DisplayName: "Root Console Login"
Enabled: true
DedupPeriodMinutes: 15
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity & Access Management
  - Authentication
  - DemoThreatHunting
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 3.6
  MITRE ATT&CK:
    - TA0004:T1078
Severity: High
Description: The root account has been logged into.
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.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is ConsoleLogin
  • userIdentity.type is Root
  • responseElements.ConsoleLogin is Success
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
sourceIPAddress
userIdentityAccountIduserIdentity.accountId
userIdentityArnuserIdentity.arn
eventTime
mfaUsedadditionalEventData.MFAUsed
recipientAccountId

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

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "LoginTo": "https://console.aws.amazon.com/console/",
    "MFAUsed": "No",
    "MobileVersion": "No"
  },
  "awsRegion": "us-east-1",
  "eventID": "1",
  "eventName": "ConsoleLogin",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestParameters": null,
  "responseElements": {
    "ConsoleLogin": "Success"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla",
  "userIdentity": {
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:root",
    "principalId": "1111",
    "type": "Root",
    "userName": "root"
  }
}

Root Password Changed

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Identity and Access Management, Persistence:Account Manipulation
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Someone manually changed the Root console login password.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Only check password update changes
    if event.get("eventName") != "PasswordUpdated":
        return False

    # Only check root activity
    if event.deep_get("userIdentity", "type") != "Root":
        return False

    # Only alert if the login was a success
    return event.deep_get("responseElements", "PasswordUpdated") == "Success"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_root_password_changed.py
RuleID: "AWS.CloudTrail.RootPasswordChanged"
DisplayName: "Root Password Changed"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Identity and Access Management
  - Persistence:Account Manipulation
Severity: High
Reports:
  MITRE ATT&CK:
    - TA0003:T1098
Description: >
  Someone manually changed the Root console login password.
Runbook: >
  Verify that the root password change was authorized. If not, AWS support should be contacted immediately as the root account cannot be recovered through normal means and grants complete access to the account.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_change-root.html
SummaryAttributes:
  - userAgent
  - sourceIpAddress
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is PasswordUpdated
  • userIdentity.type is Root
  • responseElements.PasswordUpdated is Success

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Verify that the root password change was authorized. If not, AWS support should be contacted immediately as the root account cannot be recovered through normal means and grants complete access to the account.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "1111",
  "eventName": "PasswordUpdated",
  "eventSource": "signin.amazonaws.com",
  "eventTime": "2019-01-01T00:00:00Z",
  "eventType": "AwsConsoleSignIn",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": null,
  "responseElements": {
    "PasswordUpdated": "Success"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36",
  "userIdentity": {
    "accesKeyId": "1111",
    "accessKeyId": "",
    "accountId": "123456789012",
    "arn": "arn:aws:iam::123456789012:root",
    "principalId": "123456789012",
    "type": "Root"
  }
}

S3 Access Via VPC Endpoint From External IP

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, VPC, S3, Data Exfiltration, CloudTrail, Network Boundary Bridging, Exfiltration Over Alternative Protocol
Reference
www.wiz.io
Source
github.com/panther-labs/panther-analysis

Detects S3 data access through VPC endpoints from external/public IP addresses, which could indicate data exfiltration attempts. This rule can be customized with the following overrides: - S3_DATA_ACCESS_OPERATIONS: List of S3 operations to monitor

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import ipaddress

from panther_aws_helpers import aws_rule_context

# Define S3 data access operations
S3_DATA_ACCESS_OPERATIONS = [
    "GetObject",
    "GetObjectVersion",
    "GetObjectAcl",
    "GetObjectVersionAcl",
    "PutObject",
    "PutObjectAcl",
    "PutObjectVersionAcl",
    "CopyObject",
    "DeleteObject",
    "DeleteObjects",
    "DeleteObjectVersion",
]


def rule(event):
    # Check if this is a VPC Endpoint network activity event for S3
    if (
        event.get("eventType") != "AwsVpceEvent"
        or event.get("eventCategory") != "NetworkActivity"
        or event.get("eventSource") != "s3.amazonaws.com"
    ):
        return False

    # Focus on data access operations
    if event.get("eventName") not in S3_DATA_ACCESS_OPERATIONS:
        return False

    # Check for external IP
    source_ip = event.get("sourceIPAddress", "")
    if not source_ip:
        return False

    try:
        ip_obj = ipaddress.ip_address(source_ip)
        if ip_obj.is_global:
            return True
    except ValueError:
        # If source_ip is not a valid IP address
        pass

    return False


def title(event):
    # Use UDM actor_user which leverages the get_actor_user helper function
    actor_user = event.udm("actor_user")
    source_ip = event.get("sourceIPAddress", "unknown")
    bucket_name = event.deep_get("requestParameters", "bucketName", default="unknown")

    return (
        f"S3 Access via VPC Endpoint from External IP: [{actor_user}] from "
        f"[{source_ip}] to bucket [{bucket_name}]"
    )


def alert_context(event):
    account_id = event.deep_get("userIdentity", "accountId", default="")

    context = aws_rule_context(event)
    context.update(
        {
            "account_id": account_id,
            "principal_id": event.deep_get("userIdentity", "principalId", default="unknown"),
            "actor_user": event.udm("actor_user"),
            "source_ip": event.get("sourceIPAddress", "unknown"),
            "event_source": event.get("eventSource", "unknown"),
            "api_call": event.get("eventName", "unknown"),
            "resources": event.get("resources", []),
            "request_parameters": event.get("requestParameters", {}),
            "config": {
                "operations_monitored": S3_DATA_ACCESS_OPERATIONS,
            },
        }
    )

    return context

Rule specification

AnalysisType: rule
Filename: aws_vpce_s3_external_ip.py
RuleID: "AWS.CloudTrail.VPCE.S3ExternalIP"
DisplayName: "S3 Access Via VPC Endpoint From External IP"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Tags:
  - AWS
  - VPC
  - S3
  - Data Exfiltration
  - CloudTrail
  - Network Boundary Bridging
  - Exfiltration Over Alternative Protocol
Description: |
  Detects S3 data access through VPC endpoints from external/public IP addresses, which could indicate data exfiltration attempts.
  
  This rule can be customized with the following overrides:
  - S3_DATA_ACCESS_OPERATIONS: List of S3 operations to monitor
Runbook: |
  1. Identify the principal and the specific S3 objects being accessed
  2. Verify if the external IP address belongs to a legitimate service or entity
  3. Check if the access pattern is expected for this user/role
  4. Review the contents of the S3 objects to determine sensitivity
  5. If unauthorized, determine how the principal obtained access credentials
  6. Revoke access immediately if determined to be malicious
  7. Consider implementing stricter bucket policies and VPC endpoint policies
Reference: https://www.wiz.io/blog/aws-vpc-endpoint-cloudtrail
SummaryAttributes:
  - userIdentity.principalId
  - sourceIPAddress
  - eventSource
  - eventName
  - requestParameters
  - resources
DedupPeriodMinutes: 60

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventType is AwsVpceEvent
  • eventCategory is NetworkActivity
  • eventSource is s3.amazonaws.com
  • eventName is one of GetObject, GetObjectVersion, GetObjectAcl, GetObjectVersionAcl, PutObject (+6 more values, see Indicators below)
  • sourceIPAddress is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventNamein
  • CopyObject
  • DeleteObject
  • DeleteObjectVersion
  • DeleteObjects
  • GetObject
  • GetObjectAcl
  • GetObjectVersion
  • GetObjectVersionAcl
  • PutObject
  • PutObjectAcl
  • PutObjectVersionAcl
field:"aws::eventName" kind:in
sourceIPAddressis_not_null
  • (no value, null check)
field:"aws::sourceIPAddress" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Identify the principal and the specific S3 objects being accessed

2. Verify if the external IP address belongs to a legitimate service or entity

3. Check if the access pattern is expected for this user/role

4. Review the contents of the S3 objects to determine sensitivity

5. If unauthorized, determine how the principal obtained access credentials

6. Revoke access immediately if determined to be malicious

7. Consider implementing stricter bucket policies and VPC endpoint policies

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "eventCategory": "NetworkActivity",
  "eventName": "GetObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-01-01T12:00:00Z",
  "eventType": "AwsVpceEvent",
  "recipientAccountId": "012345678901",
  "requestParameters": {
    "bucketName": "sensitive-data-bucket",
    "key": "confidential/file.pdf"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::sensitive-data-bucket/confidential/file.pdf",
      "type": "AWS::S3::Object"
    }
  ],
  "sourceIPAddress": "8.8.8.8",
  "userIdentity": {
    "accountId": "012345678901",
    "principalId": "AIDAEXAMPLE",
    "type": "IAMUser"
  },
  "vpcEndpointAccountId": "012345678901",
  "vpcEndpointId": "vpce-EXAMPLE08c1b6b9b7"
}

S3 Bucket Deleted

#
Severity
informational
Group by
userIdentity.arn
Compliance
Stratus Red Team aws.defense-evasion.cloudtrail-lifecycle-rule, aws.exfiltration.s3-backdoor-bucket-policy
Log types
AWS.CloudTrail
Tags
AWS, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

A S3 Bucket, Policy, or Website was deleted

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    # Capture DeleteBucket, DeleteBucketPolicy, DeleteBucketWebsite
    return event.get("eventName").startswith("DeleteBucket") and aws_cloudtrail_success(event)


def helper_strip_role_session_id(user_identity_arn):
    # The Arn structure is arn:aws:sts::123456789012:assumed-role/RoleName/<sessionId>
    arn_parts = user_identity_arn.split("/")
    if arn_parts:
        return "/".join(arn_parts[:2])
    return user_identity_arn


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", "<NO_ARN_FOUND>")


def title(event):
    return f"{event.deep_get('userIdentity', 'type')} [{dedup(event)}] destroyed a bucket"


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_bucket_deleted.py
RuleID: "AWS.S3.BucketDeleted"
DisplayName: "S3 Bucket Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0040:T1485

  Stratus Red Team:
    - aws.defense-evasion.cloudtrail-lifecycle-rule
    - aws.exfiltration.s3-backdoor-bucket-policy
Severity: Info
Description: A S3 Bucket, Policy, or Website was deleted
Runbook: Explore if this bucket deletion was potentially destructive
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/DeletingObjects.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - vpcEndpointId

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName starts with DeleteBucket
  • errorCode is empty
  • errorMessage is empty

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
typeuserIdentity.type

Response runbook

Explore if this bucket deletion was potentially destructive

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "AuthenticationMethod": "AuthHeader",
    "CipherSuite": "ECDHE-RSA-AES128-SHA",
    "SignatureVersion": "SigV4",
    "vpcEndpointId": "vpce-aaa333c9"
  },
  "awsRegion": "us-east-2",
  "errorCode": "",
  "eventID": "6795ef5c-7777-4444-8888-cabb7f252bd3",
  "eventName": "DeleteBucket",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2020-02-14T00:43:54Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.05",
  "recipientAccountId": "123456789012",
  "requestID": "EEEE5AAAAAA44444",
  "requestParameters": {
    "bucketName": "secrets",
    "host": [
      "s3-us-east-2.amazonaws.com"
    ]
  },
  "responseElements": null,
  "sourceIPAddress": "157.130.196.214",
  "userAgent": "[S3Console/0.4, aws-internal/3 aws-sdk-java/1.11.666 Linux/4.9.184-0.1.ac.235.83.329.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.232-b09 java/1.8.0_232 vendor/Oracle_Corporation]",
  "userIdentity": {
    "accessKeyId": "AAAAAAAAAAAAAAAAAAAAA",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/BucketAdministrator/user_name",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2020-02-14T00:11:28Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/BucketAdministrator",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "BucketAdministrator"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-aaa333c9"
}

S3 Bucket Encryption Deleted

#
Status
Experimental
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, S3, Defense Evasion, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when S3 bucket encryption configuration is deleted, which could expose data to unauthorized access or indicate ransomware preparation activity.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "DeleteBucketEncryption"
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"deleted bucket encryption for bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_delete_bucket_encryption.py
RuleID: "AWS.S3.DeleteBucketEncryption"
DisplayName: "S3 Bucket Encryption Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - Defense Evasion
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1485
Severity: Medium
Status: Experimental
Description: Detects when S3 bucket encryption configuration is deleted, which could expose data to unauthorized access or indicate ransomware preparation activity.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the encryption deletion to identify if this is part of a larger attack pattern
  2. Check if this user has performed DeleteBucketEncryption on this bucket in the past 90 days to determine if this is normal administrative activity
  3. Find other alerts with rule IDs AWS.S3.DisableBucketLogging, AWS.S3.SuspendVersioning, or AWS.S3.DisableMfaDelete for the same bucket in the past 7 days to detect coordinated security control disabling
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is DeleteBucketEncryption

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the encryption deletion to identify if this is part of a larger attack pattern

2. Check if this user has performed DeleteBucketEncryption on this bucket in the past 90 days to determine if this is normal administrative activity

3. Find other alerts with rule IDs AWS.S3.DisableBucketLogging, AWS.S3.SuspendVersioning, or AWS.S3.DisableMfaDelete for the same bucket in the past 7 days to detect coordinated security control disabling

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "DeleteBucketEncryption",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "bucketName": "sensitive-data-bucket",
    "encryption": "",
    "host": "sample-bucket-pensive-jones.s3.amazonaws.com"
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

S3 Bucket Logging Disabled

#
Severity
low
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when server access logging is disabled on an S3 bucket, removing audit trail capabilities that could indicate ransomware preparation activity or an attempt to evade detection.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "PutBucketLogging"
        and event.deep_get("requestParameters", "logging") == ""
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"disabled bucket logging for bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}]"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_disable_bucket_logging.py
RuleID: "AWS.S3.DisableBucketLogging"
DisplayName: "S3 Bucket Logging Disabled"
Enabled: true
CreateAlert: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1485
Severity: Low
Description: Detects when server access logging is disabled on an S3 bucket, removing audit trail capabilities that could indicate ransomware preparation activity or an attempt to evade detection.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern
  2. Check if this user has modified bucket logging configurations in the past 90 days to determine if this is normal administrative activity
  3. Find other alerts with rule IDs AWS.S3.SuspendVersioning, AWS.S3.DisableMfaDelete, or AWS.S3.DeleteBucketEncryption for the same bucket in the past 7 days to detect coordinated security control disabling
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is PutBucketLogging
  • requestParameters.logging is ""

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the bucket logging was disabled to identify if this is part of a larger attack pattern

2. Check if this user has modified bucket logging configurations in the past 90 days to determine if this is normal administrative activity

3. Find other alerts with rule IDs AWS.S3.SuspendVersioning, AWS.S3.DisableMfaDelete, or AWS.S3.DeleteBucketEncryption for the same bucket in the past 7 days to detect coordinated security control disabling

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "additionalEventData": {
    "AuthenticationMethod": "AuthHeader",
    "CipherSuite": "TLS_AES_128_GCM_SHA256",
    "SignatureVersion": "SigV4",
    "bytesTransferredIn": 108,
    "bytesTransferredOut": 0,
    "x-amz-id-2": "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234567890=="
  },
  "awsRegion": "us-west-2",
  "eventCategory": "Management",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "PutBucketLogging",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.11",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "BucketLoggingStatus": {
      "xmlns": "http://s3.amazonaws.com/doc/2006-03-01/"
    },
    "Host": "sample-bucket-great-proskuriakova.s3.us-west-2.amazonaws.com",
    "bucketName": "critical-data-bucket",
    "logging": ""
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::sample-bucket-great-proskuriakova",
      "accountId": "111111111111",
      "type": "AWS::S3::Bucket"
    }
  ],
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "tlsDetails": {
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "sample-bucket-great-proskuriakova.s3.us-west-2.amazonaws.com",
    "tlsVersion": "TLSv1.3"
  },
  "userAgent": "[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4 Safari/537.36]",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-quirky-greider/sso.amazonaws.com/us-west-2/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  }
}

S3 Bucket Policy Confused Deputy Protection for Service Principals

#
Severity
high
Resource types
AWS.S3.Bucket
Tags
AWS, Security Control, Best Practices
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Ensures that S3 bucket policies with service principals include conditions to prevent the confused deputy problem.

Detection logic

import json

REQUIRED_CONDITIONS = {
    "aws:SourceArn",
    "aws:SourceAccount",
    "aws:SourceOrgID",
    "aws:SourceOrgPaths",
}


def policy(resource):
    bucket_policy = resource.get("Policy")
    if bucket_policy is None:
        return True  # Pass if there is no bucket policy

    policy_statements = json.loads(bucket_policy).get("Statement", [])
    for statement in policy_statements:
        # Check if the statement includes a service principal and allows access
        principal = statement.get("Principal", {})
        if "Service" in principal and statement["Effect"] == "Allow":
            conditions = statement.get("Condition", {})
            # Flatten nested condition keys (e.g., inside "StringEquals")
            flat_condition_keys = set()
            for condition in conditions.values():
                if isinstance(condition, dict):
                    flat_condition_keys.update(condition.keys())
            # Check if any required condition key is present
            if not {str.casefold(x) for x in REQUIRED_CONDITIONS} & {
                str.casefold(x) for x in flat_condition_keys
            }:
                return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_s3_bucket_policy_confused_deputy.py
PolicyID: "AWS.S3.Bucket.PolicyConfusedDeputyProtection"
DisplayName: "S3 Bucket Policy Confused Deputy Protection for Service Principals"
Enabled: true
ResourceTypes:
  - AWS.S3.Bucket
Tags:
  - AWS
  - Security Control
  - Best Practices
Severity: High
Description: >
  Ensures that S3 bucket policies with service principals include conditions to prevent the confused deputy problem.
Runbook: |
  Update the bucket policy to include conditions such as aws:SourceArn, aws:SourceAccount,
  aws:SourceOrgID, or aws:SourceOrgPaths when a service principal is specified.
Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html

Stages and Predicates

Flags AWS.S3.Bucket resources when the condition below holds.

Condition

  • Policy is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Policyis_null(no value, null check)excludes:Policy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policyis_not_null
  • (no value, null check)
field:"Policy" kind:is_not_null

Response runbook

Update the bucket policy to include conditions such as aws:SourceArn, aws:SourceAccount,

aws:SourceOrgID, or aws:SourceOrgPaths when a service principal is specified.

S3 Bucket Replication Deleted

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when S3 bucket replication configuration is deleted, which could prevent data backup and indicate ransomware preparation activity.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "DeleteBucketReplication"
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"deleted bucket replication for bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_delete_bucket_replication.py
RuleID: "AWS.S3.DeleteBucketReplication"
DisplayName: "S3 Bucket Replication Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1485
Severity: Medium
Description: Detects when S3 bucket replication configuration is deleted, which could prevent data backup and indicate ransomware preparation activity.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the replication deletion to establish full activity context
  2. Check if this user has historically managed replication settings for this bucket in the past 90 days to verify if this is authorized administrative work
  3. Find other alerts indicating security control disabling (DeleteBucketEncryption, SuspendVersioning, DisableBucketLogging) for the same bucket in the past 7 days to identify ransomware preparation patterns
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is DeleteBucketReplication

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the replication deletion to establish full activity context

2. Check if this user has historically managed replication settings for this bucket in the past 90 days to verify if this is authorized administrative work

3. Find other alerts indicating security control disabling (DeleteBucketEncryption, SuspendVersioning, DisableBucketLogging) for the same bucket in the past 7 days to identify ransomware preparation patterns

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "DeleteBucketReplication",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "bucketName": "production-backup-bucket",
    "host": "sample-bucket-serene-pike.s3.amazonaws.com",
    "replication": ""
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

S3 Bucket Versioning Suspended

#
Severity
low
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when S3 bucket versioning is suspended or disabled, which removes the ability to recover previous versions of objects and is a common precursor to ransomware attacks or data destruction.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "PutBucketVersioning"
        and event.deep_get("requestParameters", "VersioningConfiguration", "Status")
        in ("Suspended", "Disabled")
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"suspended object versioning in bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}]"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="UNKNOWN_BUCKET"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_s3_suspend_versioning.py
RuleID: "AWS.S3.SuspendVersioning"
DisplayName: "S3 Bucket Versioning Suspended"
Enabled: true
CreateAlert: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1485
Severity: Low
Description: Detects when S3 bucket versioning is suspended or disabled, which removes the ability to recover previous versions of objects and is a common precursor to ransomware attacks or data destruction.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after versioning was suspended
  2. Find any DeleteObject or DeleteObjects events on this bucket in the 6 hours after versioning was suspended to detect potential data destruction attempts
  3. Search for other security control changes (DisableBucketLogging, DisableMfaDelete, DeleteBucketEncryption) on the same bucket in the past 7 days to identify coordinated attack patterns
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is PutBucketVersioning
  • requestParameters.VersioningConfiguration.Status is one of Suspended, Disabled

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after versioning was suspended

2. Find any DeleteObject or DeleteObjects events on this bucket in the 6 hours after versioning was suspended to detect potential data destruction attempts

3. Search for other security control changes (DisableBucketLogging, DisableMfaDelete, DeleteBucketEncryption) on the same bucket in the past 7 days to identify coordinated attack patterns

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "PutBucketVersioning",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "VersioningConfiguration": {
      "Status": "Suspended"
    },
    "bucketName": "important-files-bucket",
    "host": "sample-bucket-lucid-jang.s3.amazonaws.com"
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

S3 MFA Delete Disabled

#
Severity
low
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Impact:Data Destruction
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when MFA Delete is disabled on an S3 bucket, removing an important security control that prevents accidental or malicious deletion of versioned objects and could indicate ransomware preparation activity.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "PutBucketVersioning"
        and event.deep_get("requestParameters", "VersioningConfiguration", "MfaDelete")
        == "Disabled"
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"disabled MFA Delete feature for bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}] "
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="UNKNOWN_BUCKET"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_s3_disable_mfa_delete.py
RuleID: "AWS.S3.DisableMfaDelete"
DisplayName: "S3 MFA Delete Disabled"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1485
Severity: Low
Description: Detects when MFA Delete is disabled on an S3 bucket, removing an important security control that prevents accidental or malicious deletion of versioned objects and could indicate ransomware preparation activity.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after MFA Delete was disabled
  2. Find any DeleteObject or DeleteObjects events on this bucket in the 6 hours after MFA Delete was disabled to check if bulk deletion occurred
  3. Search for other security control changes (DisableBucketLogging, SuspendVersioning, DeleteBucketEncryption) on the same bucket in the past 7 days to identify ransomware preparation patterns
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is PutBucketVersioning
  • requestParameters.VersioningConfiguration.MfaDelete is Disabled

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after MFA Delete was disabled

2. Find any DeleteObject or DeleteObjects events on this bucket in the 6 hours after MFA Delete was disabled to check if bulk deletion occurred

3. Search for other security control changes (DisableBucketLogging, SuspendVersioning, DeleteBucketEncryption) on the same bucket in the past 7 days to identify ransomware preparation patterns

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "PutBucketVersioning",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "VersioningConfiguration": {
      "MfaDelete": "Disabled",
      "Status": "Enabled"
    },
    "bucketName": "critical-data-bucket",
    "host": "sample-bucket-lucid-kowalevski.s3.amazonaws.com"
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "true"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

S3 Object Encrypted with External KMS Key

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, S3, Ransomware, Impact:Data Destruction
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects when an S3 object is copied with a KMS key belonging to an account ID different than the bucket owner's account ID. This technique is used in S3 ransomware attacks where attackers encrypt objects with their own KMS key from an attacker-controlled AWS account, making the data inaccessible to the original owner. This is often a precursor to ransom demands or permanent data loss.

MITRE ATT&CK coverage

TacticTechniques
Impact

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):

    if event.get("eventName") != "CopyObject" or not aws_cloudtrail_success(event):
        return False

    kms_key_arn = event.deep_get(
        "requestParameters",
        "x-amz-server-side-encryption-aws-kms-key-id",
        default="<UNKNOWN_KEY_ID>",
    )

    if kms_key_arn.startswith("arn:aws:kms:"):
        # Extract account ID from KMS key ARN (format: arn:aws:kms:region:account:key/key-id)
        kms_parts = kms_key_arn.split(":")
        if len(kms_parts) >= 5:
            kms_account_id = kms_parts[4]
            bucket_account_id = event.get("recipientAccountId", "")

            # Alert on cross-account KMS key usage
            if kms_account_id != bucket_account_id:
                return True

    return False


def title(event):

    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"encrypted an object in bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}] "
        f"with a KMS key belonging to a different account ID "
        f"than the account owner ID"
    )


def alert_context(event):
    context = aws_rule_context(event)
    context["bucketName"] = event.deep_get(
        "requestParameters", "bucketName", default="<UNKNOWN_BUCKET>"
    )
    context["objectKey"] = event.deep_get("requestParameters", "key", default="<UNKNOWN_KEY>")

    kms_key_arn = event.deep_get(
        "requestParameters",
        "x-amz-server-side-encryption-aws-kms-key-id",
        default="<UNKNOWN_KEY_ARN>",
    )
    context["kmsKeyId"] = kms_key_arn

    # Add cross-account indicator
    if kms_key_arn:
        kms_parts = kms_key_arn.split(":")
        if len(kms_parts) >= 5:
            kms_account_id = kms_parts[4]
            bucket_account_id = event.get("recipientAccountId", "")
            context["isCrossAccountKms"] = kms_account_id != bucket_account_id
            context["kmsAccountId"] = kms_account_id
            context["bucketAccountId"] = bucket_account_id

    context["encryption"] = event.deep_get(
        "requestParameters", "x-amz-server-side-encryption", default="<UNKNOWN_ENCRYPTION>"
    )
    return context

Rule specification

AnalysisType: rule
Filename: aws_s3_copy_object_cross_account_kms.py
RuleID: "AWS.S3.CopyObject.CrossAccount.Encryption.KMS"
DisplayName: "S3 Object Encrypted with External KMS Key"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - S3
  - Ransomware
  - Impact:Data Destruction
Reports:
  MITRE ATT&CK:
    - TA0040:T1486 # Impact: Data Encrypted for Impact
Severity: High
Description: >
  Detects when an S3 object is copied with a KMS key belonging to an account ID different than
  the bucket owner's account ID. This technique is used in S3
  ransomware attacks where attackers encrypt objects with their own KMS key from
  an attacker-controlled AWS account, making the data inaccessible to the original
  owner. This is often a precursor to ransom demands or permanent data loss.
Runbook: |
  1. Query CloudTrail for all CopyObject events by the userIdentity:arn in the 24 hours before and after the alert to identify all affected objects in the requestParameters:bucketName
  2. Check if the KMS key ARN from resources field belongs to an external account ID that appears in any legitimate cross-account operations in the past 90 days
  3. Find all S3 GetObject and ListBucket events by this user on the source bucket in the 1 hour before the first CopyObject to check if the attacker performed reconnaissance
Reference: https://rhinosecuritylabs.com/aws/s3-ransomware-part-1-attack-vector/

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventName is CopyObject
  • errorCode is empty
  • errorMessage is empty
  • requestParameters.x-amz-server-side-encryption-aws-kms-key-id starts with arn:aws:kms:

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage
eventNameneCopyObjectexcludes:eventName field:"eventName" value:"CopyObject"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all CopyObject events by the userIdentity:arn in the 24 hours before and after the alert to identify all affected objects in the requestParameters:bucketName

2. Check if the KMS key ARN from resources field belongs to an external account ID that appears in any legitimate cross-account operations in the past 90 days

3. Find all S3 GetObject and ListBucket events by this user on the source bucket in the 1 hour before the first CopyObject to check if the attacker performed reconnaissance

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "CopyObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": false,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "bucketName": "victim-data-bucket",
    "key": "important-file.txt",
    "x-amz-copy-source": "victim-data-bucket/important-file.txt",
    "x-amz-server-side-encryption": "aws:kms",
    "x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:us-east-1:999999999999:key/attacker-key-id"
  },
  "resources": [
    {
      "ARN": "arn:aws:s3:::sample-bucket-quizzical-yalow/important-file.txt",
      "type": "AWS::S3::Object"
    },
    {
      "ARN": "arn:aws:kms:us-east-1:999999999999:key/attacker-key-id",
      "accountId": "999999999999",
      "type": "AWS::KMS::Key"
    }
  ],
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-elastic-rhodes/sample-role-brave-yalow-role-jolly-banzai-role-intelligent-brahmagupta-role-admiring-cori-role-beautiful-keldysh-role-hopeful-chaplygin",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:attacker",
    "type": "AssumedRole"
  }
}

S3 Public Access Block Deleted

#
Status
Experimental
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion, Initial Access
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Detects when S3 bucket public access block configuration is deleted, which could allow unauthorized public access to sensitive data or indicate preparation for data exfiltration.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Defense Impairment

Telemetry coverage

Detection logic

from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context


def rule(event):
    return (
        aws_cloudtrail_success(event)
        and event.get("eventSource") == "s3.amazonaws.com"
        and event.get("eventName") == "DeleteBucketPublicAccessBlock"
    )


def title(event):
    return (
        f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
        f"deleted public access block for bucket "
        f"[{event.deep_get('requestParameters', 'bucketName')}] bucket"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_s3_delete_public_access_block.py
RuleID: "AWS.S3.DeletePublicAccessBlock"
DisplayName: "S3 Public Access Block Deleted"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion
  - Initial Access
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0001:T1190
Severity: Medium
Status: Experimental
Description: Detects when S3 bucket public access block configuration is deleted, which could allow unauthorized public access to sensitive data or indicate preparation for data exfiltration.
Runbook: |
  1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the public access block deletion
  2. Find any PutBucketPolicy or PutBucketAcl events on this bucket in the 1 hour after the deletion to check if the bucket or objects were made public
  3. Search for GetObject events from public IP addresses on this bucket in the 6 hours after the deletion to detect unauthorized data access
Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html
SummaryAttributes:
  - sourceIpAddress
  - userAgent
  - recipientAccountId
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • errorCode is empty
  • errorMessage is empty
  • eventSource is s3.amazonaws.com
  • eventName is DeleteBucketPublicAccessBlock

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
errorCodeis_not_null(no value, null check)excludes:errorCode
errorMessageis_not_null(no value, null check)excludes:errorMessage

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user
bucketNamerequestParameters.bucketName

Response runbook

1. Query CloudTrail for all S3 API calls by the userIdentity:arn on the requestParameters:bucketName in the 24 hours before and after the public access block deletion

2. Find any PutBucketPolicy or PutBucketAcl events on this bucket in the 1 hour after the deletion to check if the bucket or objects were made public

3. Search for GetObject events from public IP addresses on this bucket in the 6 hours after the deletion to detect unauthorized data access

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventID": "12345678-1234-1234-1234-111111111111",
  "eventName": "DeleteBucketPublicAccessBlock",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2024-01-15T10:45:23Z",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "111111111111",
  "requestID": "ABC123DEF456",
  "requestParameters": {
    "bucketName": "corporate-documents",
    "host": "sample-bucket-sweet-kepler.s3.amazonaws.com",
    "publicAccessBlock": ""
  },
  "responseElements": null,
  "sourceIPAddress": "1.2.3.4",
  "userAgent": "aws-cli/2.13.0 Python/3.11.4 Linux/5.10.0-1234-aws exe/x86_64.ubuntu.22",
  "userIdentity": {
    "accessKeyId": "ASIA-MOCKACCESSKEYID-1",
    "accountId": "111111111111",
    "arn": "arn:aws:sts::111111111111:assumed-role/sample-role-dreamy-yonath/sample-role-brave-yalow-role-intelligent-brahmagupta-role-beautiful-keldysh-role-happy-easley-role-bold-buck",
    "principalId": "AAAAAAAAAAAAAAAAAAAAA:user_name",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-01-15T10:30:00Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "111111111111",
        "arn": "arn:aws:iam::111111111111:role/sample-role-dreamy-yonath",
        "principalId": "AAAAAAAAAAAAAAAAAAAAA",
        "type": "Role",
        "userName": "AdminRole"
      }
    },
    "type": "AssumedRole"
  },
  "vpcEndpointId": "vpce-1a2b3c4d"
}

Sensitive API Calls Via VPC Endpoint

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, VPC, CloudTrail, Network Boundary Bridging, Cloud Service Discovery, Account Manipulation, Impair Defenses
Reference
www.wiz.io
Source
github.com/panther-labs/panther-analysis

Detects sensitive or unusual API calls that might indicate lateral movement, reconnaissance, or other malicious activities through VPC Endpoints. Only available for CloudTrail, EC2, KMS, S3, and Secrets Manager services.

MITRE ATT&CK coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_aws_helpers import aws_rule_context

# Define sensitive API calls to monitor as a constant
SENSITIVE_APIS = {
    "ec2.amazonaws.com": [
        "DescribeInstances",
        "DescribeNetworkInterfaces",
        "CreateKeyPair",
        "ImportKeyPair",
        "RunInstances",
    ],
    "kms.amazonaws.com": ["Decrypt", "GenerateDataKey", "CreateKey", "ScheduleKeyDeletion"],
    "secretsmanager.amazonaws.com": [
        "GetSecretValue",
        "CreateSecret",
        "PutSecretValue",
        "DeleteSecret",
    ],
    "s3.amazonaws.com": ["ListAllMyBuckets", "DeleteBucketPolicy", "PutBucketPolicy"],
    "cloudtrail.amazonaws.com": ["StopLogging", "DeleteTrail", "UpdateTrail"],
}


def rule(event):
    # Check if this is a VPC Endpoint network activity event
    if event.get("eventType") != "AwsVpceEvent" or event.get("eventCategory") != "NetworkActivity":
        return False

    event_source = event.get("eventSource")
    event_name = event.get("eventName")

    if event_source in SENSITIVE_APIS and event_name in SENSITIVE_APIS[event_source]:
        return True

    return False


def title(event):
    # Use UDM actor_user which leverages the get_actor_user helper function
    # This properly handles various identity types including AssumedRole, Root, etc.
    actor_user = event.udm("actor_user")
    api_name = event.get("eventName", "unknown")
    service = event.get("eventSource", "unknown").split(".")[0]

    return (
        f"Sensitive AWS API [{api_name}] called via VPC Endpoint by [{actor_user}] "
        f"to service [{service}]"
    )


def alert_context(event):
    account_id = event.deep_get("userIdentity", "accountId", default="")

    context = aws_rule_context(event)
    context.update(
        {
            "account_id": account_id,
            "principal_id": event.deep_get("userIdentity", "principalId", default="unknown"),
            "principal_type": event.deep_get("userIdentity", "type", default="unknown"),
            "actor_user": event.udm("actor_user"),
            "source_ip": event.get("sourceIPAddress", "unknown"),
            "event_source": event.get("eventSource", "unknown"),
            "api_call": event.get("eventName", "unknown"),
            "resources": event.get("resources", []),
            "request_parameters": event.get("requestParameters", {}),
        }
    )

    return context

Rule specification

AnalysisType: rule
Filename: aws_vpce_sensitive_api_calls.py
RuleID: "AWS.CloudTrail.VPCE.SensitiveAPICalls"
DisplayName: "Sensitive API Calls Via VPC Endpoint"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Tags:
  - AWS
  - VPC
  - CloudTrail
  - Network Boundary Bridging
  - Cloud Service Discovery
  - Account Manipulation
  - Impair Defenses
Reports:
  MITRE ATT&CK:
    - TA0007:T1526 # Cloud Service Discovery
    - TA0003:T1098 # Account Manipulation
    - TA0005:T1562 # Impair Defenses
    - TA0005:T1599 # Network Boundary Bridging
Description: Detects sensitive or unusual API calls that might indicate lateral movement, reconnaissance, or other malicious activities through VPC Endpoints. Only available for CloudTrail, EC2, KMS, S3, and Secrets Manager services.
Runbook: |
  1. Identify the principal making the sensitive API call and the specific service affected
  2. Determine if this action is expected from this principal
  3. Check if the API call is one that typically requires additional scrutiny (e.g., logging configuration changes)
  4. Investigate whether the VPC Endpoint is configured to properly restrict access
  5. Review additional API calls from the same principal for suspicious patterns
  6. If unexpected activity is confirmed, consider temporarily restricting the principal's access
  7. Document findings and take appropriate remediation steps based on investigation
Reference: https://www.wiz.io/blog/aws-vpc-endpoint-cloudtrail
SummaryAttributes:
  - userIdentity.principalId
  - userIdentity.accountId
  - sourceIPAddress
  - eventSource
  - eventName

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventType is AwsVpceEvent
  • eventCategory is NetworkActivity
  • eventSource is one of ec2.amazonaws.com, kms.amazonaws.com, secretsmanager.amazonaws.com, s3.amazonaws.com, cloudtrail.amazonaws.com

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
eventCategoryneNetworkActivityexcludes:eventCategory field:"eventCategory" value:"NetworkActivity"
eventTypeneAwsVpceEventexcludes:eventType field:"eventType" value:"AwsVpceEvent"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventSourcein
  • cloudtrail.amazonaws.com
  • ec2.amazonaws.com
  • kms.amazonaws.com
  • s3.amazonaws.com
  • secretsmanager.amazonaws.com
field:"aws::eventSource" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

1. Identify the principal making the sensitive API call and the specific service affected

2. Determine if this action is expected from this principal

3. Check if the API call is one that typically requires additional scrutiny (e.g., logging configuration changes)

4. Investigate whether the VPC Endpoint is configured to properly restrict access

5. Review additional API calls from the same principal for suspicious patterns

6. If unexpected activity is confirmed, consider temporarily restricting the principal's access

7. Document findings and take appropriate remediation steps based on investigation

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "NetworkActivity",
  "eventName": "UpdateTrail",
  "eventSource": "cloudtrail.amazonaws.com",
  "eventTime": "2023-03-01T00:00:00Z",
  "eventType": "AwsVpceEvent",
  "eventVersion": "1.08",
  "recipientAccountId": "111111111111",
  "requestParameters": {
    "isMultiRegionTrail": false,
    "name": "management-events"
  },
  "responseElements": null,
  "sourceIPAddress": "10.0.0.1",
  "userIdentity": {
    "accountId": "111111111111",
    "principalId": "AROAEXAMPLE:session-name",
    "type": "IAMUser"
  },
  "vpcEndpointAccountId": "111111111111",
  "vpcEndpointId": "vpce-1234abcd"
}

Sensitive AWS CloudWatch Log Encryption

#
Severity
high
Resource types
AWS.CloudWatch.LogGroup
Tags
AWS, Configuration Required, Panther
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

AWS automatically performs server-side encryption of logs, but you can encrypt with your own CMK to protect extra sensitive log data.

Detection logic

from fnmatch import fnmatch

# replace the log groups ARN regex with the
# log groups that should be encrypted with your CMK
SENSITIVE_LOG_GROUP_ARN_REGEXS = {"*LogGroup-2*"}


def policy(resource):
    if resource["KmsKeyId"] is None:
        if SENSITIVE_LOG_GROUP_ARN_REGEXS and any(
            fnmatch(resource.get("Arn"), group_arn) for group_arn in SENSITIVE_LOG_GROUP_ARN_REGEXS
        ):
            return False
    return True

Rule specification

AnalysisType: policy
Filename: aws_cloudwatch_loggroup_sensitive_encrypted.py
PolicyID: "AWS.CloudWatchLogs.SensitiveLogGroup.Encryption"
DisplayName: "Sensitive AWS CloudWatch Log Encryption"
Enabled: false
ResourceTypes:
  - AWS.CloudWatch.LogGroup
Tags:
  - AWS
  - Configuration Required
  - Panther
Severity: High
Description: >
  AWS automatically performs server-side encryption of logs, but you can encrypt with your own CMK
  to protect extra sensitive log data.
Runbook: >
  Encrypt the CloudWatch log group with a KMS key, or add this log group to the ignore list (SENSITIVE_LOG_GROUP_ARN_REGEXS).
Reference: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Response runbook

Encrypt the CloudWatch log group with a KMS key, or add this log group to the ignore list (SENSITIVE_LOG_GROUP_ARN_REGEXS).

SIGNAL - AWS Console SSO Sign-In

#
Severity
informational
Log types
AWS.CloudTrail
Source
github.com/panther-labs/panther-analysis

Telemetry coverage

Detection logic

def rule(event):
    return (
        event.get("eventSource") == "sso.amazonaws.com" and event.get("eventName") == "Authenticate"
    )

Rule specification

AnalysisType: rule
Filename: aws_console_signin.py
RuleID: "AWS.Console.Sign-In"
DisplayName: "SIGNAL - AWS Console SSO Sign-In"
Enabled: true
CreateAlert: false
LogTypes:
    - AWS.CloudTrail
Severity: Info
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventSource is sso.amazonaws.com
  • eventName is Authenticate

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "eventCategory": "Management",
  "eventID": "8cb05708-9764-4774-a048-59a4c8e1684d",
  "eventName": "Authenticate",
  "eventSource": "sso.amazonaws.com",
  "eventTime": "2024-06-03 15:23:22.000000000",
  "eventType": "AwsServiceEvent",
  "eventVersion": "1.08",
  "managementEvent": true
}

Signal - VPC Flow Logs Allowed SSH

#
Severity
informational
Log types
AWS.VPCFlow
Tags
AWS, Signal
Source
github.com/panther-labs/panther-analysis

VPC Flow Logs observed inbound traffic on SSH port. This rule is a signal to be used in correlation rules.

MITRE ATT&CK coverage

TacticTechniques
Lateral Movement

Detection logic

from ipaddress import ip_network

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Defaults to True (no alert) if 'dstport' is not present
    if event.udm("destination_port") != 22 or event.get("action") != "ACCEPT":
        return False

    # Only monitor for traffic coming from non-private IP space
    #
    # Defaults to True (no alert) if 'srcaddr' key is not present
    source_ip = event.udm("source_ip") or "0.0.0.0/32"
    if not ip_network(source_ip).is_global:
        return False

    # Alert if the traffic is destined for internal IP addresses
    #
    # Defaults to False(no alert) if 'dstaddr' key is not present
    destination_ip = event.udm("destination_ip") or "1.0.0.0/32"
    return not ip_network(destination_ip).is_global


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_ssh_allowed_signal.py
RuleID: "AWS.VPC.SSHAllowedSignal"
DisplayName: "Signal - VPC Flow Logs Allowed SSH"
Enabled: true
CreateAlert: false
LogTypes:
  - AWS.VPCFlow
Tags:
  - AWS
  - Signal
Reports:
  MITRE ATT&CK:
    - TA0008:T1021.004 # Lateral Movement: Remote Services: SSH
Severity: Info
Description: >
  VPC Flow Logs observed inbound traffic on SSH port.
  This rule is a signal to be used in correlation rules.

Stages and Predicates

Fires on AWS.VPCFlow events when all of the conditions below hold.

Condition

  • destination_port is 22
  • action is ACCEPT

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "action": "ACCEPT",
  "dstAddr": "10.0.0.1",
  "dstPort": 22,
  "instanceId": "i-0d4c7318592c6a2c7",
  "p_log_type": "AWS.VPCFlow",
  "srcAddr": "1.1.1.1"
}

StopInstance WITH ModifyInstanceAttributes

#
Severity
high
Time window
30h
Match by
p_alert_context.instance_ids
Reference
unit42.paloaltonetworks.com
Source
github.com/panther-labs/panther-analysis

Identifies when StopInstance and ModifyInstanceAttributes CloudTrail events occur in a short period of time. Since EC2 startup scripts cannot be modified without first stopping the instance, StopInstances should be a signal.

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "AWS.EC2.StopInstance.WITH.ModifyInstanceAttributes"
DisplayName: "StopInstance WITH ModifyInstanceAttributes"
Enabled: false
Severity: High
Description: Identifies when StopInstance and ModifyInstanceAttributes CloudTrail events occur in a short period of time. Since EC2 startup scripts cannot be modified without first stopping the instance, StopInstances should be a signal.
Reference: https://unit42.paloaltonetworks.com/malicious-operations-of-exposed-iam-keys-cryptojacking/
Reports:
  MITRE ATT&CK:
    - TA0002:T1059
Detection:
    - Group:
        - ID: StopInstance
          RuleID: AWS.EC2.StopInstances
        - ID: StartupScriptChange
          RuleID: AWS.EC2.Startup.Script.Change
      MatchCriteria:
        field_name:
          - GroupID: StopInstance
            Match: p_alert_context.instance_ids
          - GroupID: StartupScriptChange
            Match: p_alert_context.instance_ids
      LookbackWindowMinutes: 1800
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.instance_ids. Each step needs one match unless a higher minimum is shown.

Stage 1: step StopInstance

References detection CloudTrail EC2 StopInstances.

Stage 2: step StartupScriptChange

References detection AWS EC2 Startup Script Change.

Unused AWS Region

#
Severity
high
Log types
AWS.CloudTrail
Tags
AWS, Defense Evasion:Unused/Unsupported Cloud Regions, Configuration Required
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

CloudTrail logged non-read activity from a verboten AWS region.

MITRE ATT&CK coverage

Detection logic

from panther_aws_helpers import aws_rule_context

# Define a list of verboten or unused regions
# Could modify to include expected user mappings: { "123456789012": { "us-west-1", "us-east-2" } }
UNUSED_REGIONS = {"ap-east-1", "eu-west-3", "eu-central-1"}


def rule(event):
    if (
        event.get("awsRegion", "<UNKNOWN_AWS_REGION>") in UNUSED_REGIONS
        and event.get("readOnly") is False
    ):
        return True
    return False


def title(event):
    aws_username = event.deep_get("userIdentity", "sessionContext", "sessionIssuer", "userName")
    return (
        "Non-read-only API call in unused region"
        f" {event.get('awsRegion', '<UNKNOWN_AWS_REGION>')} by user {aws_username}"
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_unused_region.py
RuleID: "AWS.UnusedRegion"
DisplayName: "Unused AWS Region"
Enabled: false
LogTypes:
  - AWS.CloudTrail
Tags:
  - AWS
  - Defense Evasion:Unused/Unsupported Cloud Regions
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0005:T1535
Severity: High
Description: CloudTrail logged non-read activity from a verboten AWS region.
Runbook: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws-enable-disable-regions.html
Reference: https://attack.mitre.org/techniques/T1535/
SummaryAttributes:
  - eventSource
  - eventName
  - recipientAccountId
  - awsRegion
  - p_any_aws_arns

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • awsRegion is one of ap-east-1, eu-west-3, eu-central-1
  • readOnly is false

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
awsRegionin
  • ap-east-1
  • eu-central-1
  • eu-west-3
field:"awsRegion" kind:in
readOnlyeq
  • false transforms: boolean
field:"readOnly" kind:eq value:"false"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
userNameuserIdentity.sessionContext.sessionIssuer.userName

Response runbook

https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws-enable-disable-regions.html

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "apiVersion": "20140328",
  "awsRegion": "eu-central-1",
  "eventID": "1111",
  "eventName": "CreateLogStream",
  "eventSource": "logs.amazonaws.com",
  "eventTime": "2021-10-21 22:29:06",
  "eventType": "AwsApiCall",
  "eventVersion": "1.08",
  "managementEvent": true,
  "readOnly": false,
  "recipientAccountId": "123456789012",
  "requestID": "1111",
  "requestParameters": {
    "logGroupName": "/aws/lambda/panther-analysis-api",
    "logStreamName": "2021/10/21/[$LATEST]1111"
  },
  "sourceIPAddress": "111.111.111.111",
  "userAgent": "awslambda-worker/1.0 rusoto/0.47.0 rust/1.55.0 linux",
  "userIdentity": {
    "accessKeyId": "1111",
    "accountId": "123456789012",
    "arn": "arn:aws:sts::123456789012:assumed-role/example-role/example-user",
    "principalId": "1111",
    "sessionContext": {
      "attributes": {
        "creationDate": "2021-10-21T22:29:02Z",
        "mfaAuthenticated": "false"
      },
      "sessionIssuer": {
        "accountId": "123456789012",
        "arn": "arn:aws:iam::123456789012:role/example-role",
        "principalId": "1111",
        "type": "Role",
        "userName": "example-role"
      },
      "webIdFederationData": {}
    },
    "type": "AssumedRole"
  }
}

VPC Endpoint Access Denied

#
Severity
medium
Log types
AWS.CloudTrail
Tags
AWS, VPC, CloudTrail, Network Boundary Bridging, Defense Evasion, Lateral Movement, Impair Defenses
Reference
www.wiz.io
Source
github.com/panther-labs/panther-analysis

Detects when access is denied due to VPC Endpoint policies, which could indicate attempted unauthorized access to AWS resources.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Discovery

Detection logic

from panther_aws_helpers import aws_rule_context


def rule(event):
    # Check if this is a VPC Endpoint network activity event
    if event.get("eventType") != "AwsVpceEvent" or event.get("eventCategory") != "NetworkActivity":
        return False

    # Look for access denied errors
    if event.get("errorCode") == "VpceAccessDenied":
        return True

    return False


def title(event):
    actor_user = event.udm("actor_user")
    source_ip = event.get("sourceIPAddress", "unknown")
    service = event.get("eventSource", "unknown").split(".")[0]
    return f"VPC Endpoint Access Denied for [{actor_user}] from [{source_ip}] to [{service}]"


def alert_context(event):
    account_id = event.deep_get("userIdentity", "accountId", default="unknown")

    context = aws_rule_context(event)
    context.update(
        {
            "account_id": account_id,
            "principal_id": event.deep_get("userIdentity", "principalId", default="unknown"),
            "source_ip": event.get("sourceIPAddress", "unknown"),
            "event_source": event.get("eventSource", "unknown"),
            "api_call": event.get("eventName", "unknown"),
            "error_message": event.get("errorMessage", ""),
            "resources": event.get("resources", []),
            "actor_user": event.udm("actor_user"),
        }
    )

    return context

Rule specification

AnalysisType: rule
Filename: aws_vpce_access_denied.py
RuleID: "AWS.CloudTrail.VPCE.AccessDenied"
DisplayName: "VPC Endpoint Access Denied"
Enabled: true
LogTypes:
  - AWS.CloudTrail
Severity: Medium
Tags:
  - AWS
  - VPC
  - CloudTrail
  - Network Boundary Bridging
  - Defense Evasion
  - Lateral Movement
  - Impair Defenses
Reports:
  MITRE ATT&CK:
    - TA0005:T1599 # Network Boundary Bridging
    - TA0007:T1526 # Cloud Service Discovery
Description: Detects when access is denied due to VPC Endpoint policies, which could indicate attempted unauthorized access to AWS resources.
Runbook: |
  1. Identify the principal (user/role) and source IP that was denied access
  2. Determine if this is expected behavior based on your VPC endpoint policies
  3. Check if there are multiple failed attempts from the same principal/IP
  4. If unexpected, investigate why the principal is attempting to access resources through the VPC endpoint
  5. Consider updating your VPC endpoint policies if necessary
  6. Document findings and take appropriate remediation steps based on investigation
Reference: https://www.wiz.io/blog/aws-vpc-endpoint-cloudtrail
SummaryAttributes:
  - errorCode
  - errorMessage
  - sourceIPAddress
  - eventSource
  - eventName
  - userIdentity.principalId

Stages and Predicates

Fires on AWS.CloudTrail events when all of the conditions below hold.

Condition

  • eventType is AwsVpceEvent
  • eventCategory is NetworkActivity
  • errorCode is VpceAccessDenied

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
eventCategoryneNetworkActivityexcludes:eventCategory field:"eventCategory" value:"NetworkActivity"
eventTypeneAwsVpceEventexcludes:eventType field:"eventType" value:"AwsVpceEvent"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
actor_user

Response runbook

1. Identify the principal (user/role) and source IP that was denied access

2. Determine if this is expected behavior based on your VPC endpoint policies

3. Check if there are multiple failed attempts from the same principal/IP

4. If unexpected, investigate why the principal is attempting to access resources through the VPC endpoint

5. Consider updating your VPC endpoint policies if necessary

6. Document findings and take appropriate remediation steps based on investigation

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "awsRegion": "us-east-1",
  "errorCode": "VpceAccessDenied",
  "errorMessage": "The request was denied due to a VPC endpoint policy",
  "eventCategory": "NetworkActivity",
  "eventName": "GetObject",
  "eventSource": "s3.amazonaws.com",
  "eventTime": "2023-03-01T00:00:00Z",
  "eventType": "AwsVpceEvent",
  "eventVersion": "1.08",
  "recipientAccountId": "222222222222",
  "requestParameters": {
    "bucketName": "example-bucket",
    "key": "sensitive-file.txt"
  },
  "responseElements": null,
  "sourceIPAddress": "10.0.0.1",
  "userIdentity": {
    "accountId": "111111111111",
    "principalId": "AROAEXAMPLE:session-name",
    "type": "AWSAccount"
  },
  "vpcEndpointAccountId": "222222222222",
  "vpcEndpointId": "vpce-EXAMPLE08c1b6b9b7"
}

VPC Flow Logs Inbound Port Allowlist

#
Severity
high
Log types
AWS.VPCFlow, OCSF.NetworkActivity
Tags
AWS, DataModel, Configuration Required, Security Control, Command and Control:Non-Standard Port
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

VPC Flow Logs observed inbound traffic violating the port allowlist.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Detection logic

from ipaddress import ip_network

from panther_aws_helpers import aws_rule_context

APPROVED_PORTS = {
    80,
    443,
}


def rule(event):
    # Can't perform this check without a destination port
    if not event.udm("destination_port"):
        return False

    # Only monitor for non allowlisted ports
    if event.udm("destination_port") in APPROVED_PORTS:
        return False

    # Only monitor for traffic coming from non-private IP space
    #
    # Defaults to True (no alert) if 'srcaddr' key is not present
    source_ip = event.udm("source_ip") or "0.0.0.0/32"
    if not ip_network(source_ip).is_global:
        return False

    # Alert if the traffic is destined for internal IP addresses
    #
    # Defaults to False (no alert) if 'dstaddr' key is not present
    destination_ip = event.udm("destination_ip") or "1.0.0.0/32"
    return not ip_network(destination_ip).is_global


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_inbound_traffic_port_allowlist.py
RuleID: "AWS.VPC.InboundPortWhitelist"
DisplayName: "VPC Flow Logs Inbound Port Allowlist"
Enabled: false
LogTypes:
  - AWS.VPCFlow
  - OCSF.NetworkActivity
Tags:
  - AWS
  - DataModel
  - Configuration Required
  - Security Control
  - Command and Control:Non-Standard Port
Reports:
  MITRE ATT&CK:
    - TA0011:T1571
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html
Severity: High
Description: >
  VPC Flow Logs observed inbound traffic violating the port allowlist.
Runbook: >
  Block the unapproved traffic, or update the approved ports list.
SummaryAttributes:
  - srcaddr
  - dstaddr
  - dstport

Stages and Predicates

Fires on AWS.VPCFlow, OCSF.NetworkActivity events when all of the conditions below hold.

Condition

  • destination_port is present
  • destination_port is not one of 80, 443

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
destination_portis_not_null
  • (no value, null check)
field:"destination_port" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Block the unapproved traffic, or update the approved ports list.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "dstAddr": "10.0.0.1",
  "dstPort": 22,
  "p_log_type": "AWS.VPCFlow",
  "srcAddr": "1.1.1.1"
}

VPC Flow Logs Inbound Port Blocklist

#
Severity
high
Log types
AWS.VPCFlow, OCSF.NetworkActivity
Tags
AWS, DataModel, Configuration Required, Security Control, Command and Control:Non-Standard Port
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

VPC Flow Logs observed inbound traffic violating the port blocklist.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Detection logic

from ipaddress import ip_network

from panther_aws_helpers import aws_rule_context

CONTROLLED_PORTS = {
    22,
    3389,
}


def rule(event):
    # Only monitor for blocklisted ports
    #
    # Defaults to True (no alert) if 'dstport' is not present
    if event.udm("destination_port") not in CONTROLLED_PORTS:
        return False

    # Only monitor for traffic coming from non-private IP space
    #
    # Defaults to True (no alert) if 'srcaddr' key is not present
    source_ip = event.udm("source_ip") or "0.0.0.0/32"
    if not ip_network(source_ip).is_global:
        return False

    # Alert if the traffic is destined for internal IP addresses
    #
    # Defaults to False(no alert) if 'dstaddr' key is not present
    destination_ip = event.udm("destination_ip") or "1.0.0.0/32"
    return not ip_network(destination_ip).is_global


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_inbound_traffic_port_blocklist.py
RuleID: "AWS.VPC.InboundPortBlacklist"
DisplayName: "VPC Flow Logs Inbound Port Blocklist"
Enabled: false
LogTypes:
  - AWS.VPCFlow
  - OCSF.NetworkActivity
Tags:
  - AWS
  - DataModel
  - Configuration Required
  - Security Control
  - Command and Control:Non-Standard Port
Reports:
  MITRE ATT&CK:
    - TA0011:T1571
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html
Severity: High
Description: >
  VPC Flow Logs observed inbound traffic violating the port blocklist.
Runbook: >
  Block the unapproved traffic, or update the unapproved ports list.
SummaryAttributes:
  - srcaddr
  - dstaddr
  - dstport

Stages and Predicates

Fires on AWS.VPCFlow, OCSF.NetworkActivity events when the condition below holds.

Condition

  • destination_port is one of 22, 3389

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
destination_portin
  • 22 transforms: number
  • 3389 transforms: number
field:"destination_port" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Block the unapproved traffic, or update the unapproved ports list.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "dstAddr": "10.0.0.1",
  "dstPort": 22,
  "p_log_type": "AWS.VPCFlow",
  "srcAddr": "1.1.1.1"
}

VPC Flow Logs Unapproved Outbound DNS Traffic

#
Severity
medium
Log types
AWS.VPCFlow, OCSF.NetworkActivity
Tags
AWS, DataModel, Configuration Required, Security Control, Command and Control:Application Layer Protocol
Reference
docs.aws.amazon.com
Source
github.com/panther-labs/panther-analysis

Alerts if outbound DNS traffic is detected to a non-approved DNS server. DNS is often used as a means to exfiltrate data or perform command and control for compromised hosts. All DNS traffic should be routed through internal DNS servers or trusted 3rd parties.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Detection logic

from ipaddress import ip_network

from panther_aws_helpers import aws_rule_context

APPROVED_DNS_SERVERS = {
    "1.1.1.1",  # CloudFlare DNS
    "8.8.8.8",  # Google DNS
    # '10.0.0.1', # Internal DNS
}


def rule(event):
    # Common DNS ports, for better security use an application layer aware network monitor
    #
    # Defaults to True (no alert) if 'dstport' key is not present
    if event.udm("destination_port") != 53 and event.udm("destination_port") != 5353:
        return False

    # Only monitor traffic that is originating internally
    #
    # Defaults to True (no alert) if 'srcaddr' key is not present
    source_ip = event.udm("source_ip") or "0.0.0.0/32"
    if ip_network(source_ip).is_global:
        return False

    dest_ip = event.udm("destination_ip") or "192.168.0.1/32"
    if ip_network(dest_ip).is_private:
        return False

    # No clean way to default to False (no alert), so explicitly check for key
    return (
        bool(event.udm("destination_ip"))
        and event.udm("destination_ip") not in APPROVED_DNS_SERVERS
    )


def alert_context(event):
    return aws_rule_context(event)

Rule specification

AnalysisType: rule
Filename: aws_vpc_unapproved_outbound_dns.py
RuleID: "AWS.VPC.UnapprovedOutboundDNS"
DisplayName: "VPC Flow Logs Unapproved Outbound DNS Traffic"
Enabled: false
LogTypes:
  - AWS.VPCFlow
  - OCSF.NetworkActivity
Tags:
  - AWS
  - DataModel
  - Configuration Required
  - Security Control
  - Command and Control:Application Layer Protocol
Reports:
  MITRE ATT&CK:
    - TA0011:T1071
Reference: https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html
Severity: Medium
Description: >
  Alerts if outbound DNS traffic is detected to a non-approved DNS server. DNS is often used as a means to exfiltrate data or perform command and control for compromised hosts. All DNS traffic should be routed through internal DNS servers or trusted 3rd parties.
Runbook: >
  Investigate the host sending unapproved DNS activity for signs of compromise or other malicious activity. Update network configurations appropriately to ensure all DNS traffic is routed to approved DNS servers.
SummaryAttributes:
  - srcaddr
  - dstaddr
  - dstport

Stages and Predicates

Fires on AWS.VPCFlow, OCSF.NetworkActivity events when all of the conditions below hold.

Condition

  • any of:
    • destination_port is 53
    • destination_port is 5353
  • destination_ip is present
  • destination_ip is not one of 1.1.1.1, 8.8.8.8

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
destination_ipis_not_null
  • (no value, null check)
field:"destination_ip" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity

Response runbook

Investigate the host sending unapproved DNS activity for signs of compromise or other malicious activity. Update network configurations appropriately to ensure all DNS traffic is routed to approved DNS servers.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "dstAddr": "100.100.100.100",
  "dstPort": 53,
  "p_log_type": "AWS.VPCFlow",
  "srcAddr": "10.0.0.1"
}

VPC Flow Port Scanning

#
Status
Experimental
Severity
medium
Group by
dstAddr, region, srcAddr, subNetId, vpcId
Log types
AWS.VPCFlow
Tags
Discovery:Network Service Discovery
Source
github.com/panther-labs/panther-analysis

Detects potential port scanning activity by alerting when a single source address communicates with 10 or more distinct destination ports on the same target within 60 minutes. Common ports (80, 443, 53, etc.) are excluded to reduce noise.

MITRE ATT&CK coverage

TacticTechniques
Discovery

Detection logic

from ipaddress import ip_address

COMMON_PORTS = {80, 123, 443, 445, 53, 853, 2049}


def rule(event):
    if event.get("flowDirection") != "egress":
        return False
    src_addr = event.get("srcAddr", "")
    if not src_addr or src_addr == "null":
        return False
    if event.get("dstPort") in COMMON_PORTS:
        return False
    return True


def title(event):
    src = event.get("srcAddr", "Unknown")
    dst = event.get("dstAddr", "Unknown")
    return f"Port Scanning Detected from [{src}] to [{dst}]"


def dedup(event):
    src = event.get("srcAddr", "")
    dst = event.get("dstAddr", "")
    vpc = event.get("vpcId", "")
    region = event.get("region", "")
    subnet = event.get("subNetId", "")
    return f"{src}:{dst}:{vpc}:{region}:{subnet}"


def unique(event):
    port = event.get("dstPort")
    return str(port) if port is not None else None


def severity(event):
    try:
        src = ip_address(event.get("srcAddr", ""))
        if src.is_private:
            return "HIGH"
    except ValueError:
        pass
    return "DEFAULT"

Rule specification

AnalysisType: rule
Filename: aws_vpc_port_scanning.py
RuleID: "AWS.VPC.PortScanning"
DisplayName: "VPC Flow Port Scanning"
Status: Experimental
Enabled: false
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 10
LogTypes:
  - AWS.VPCFlow
Description: >
  Detects potential port scanning activity by alerting when a single source address
  communicates with 10 or more distinct destination ports on the same target within 60 minutes.
  Common ports (80, 443, 53, etc.) are excluded to reduce noise.
Reports:
  MITRE ATT&CK:
    - TA0007:T1046
Tags:
  - Discovery:Network Service Discovery
Runbook: |
  1. Query VPC Flow logs for all egress traffic from srcAddr in the 1 hour around this alert to identify the full sequence of dstPort values targeted on dstAddr within vpcId
  2. Check if srcAddr is associated with known vulnerability scanners, corporate IT tooling, or threat intelligence feeds
  3. Find other alerts involving srcAddr or vpcId in the past 7 days to determine if this is part of ongoing reconnaissance

Stages and Predicates

Fires on AWS.VPCFlow events when all of the conditions below hold.

Condition

  • flowDirection is egress
  • srcAddr is present
  • srcAddr is not null
  • dstPort is not one of 80, 123, 443, 445, 53 (+2 more values, see Indicators below)
Alert cadence
alerts after 10 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
srcAddreqnullexcludes:srcAddr field:"srcAddr" value:"null"
srcAddris_null(no value, null check)excludes:srcAddr
dstPortin123, 2049, 443, 445, 53, 80, 853excludes:dstPort

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
srcAddr
dstAddr

Response runbook

1. Query VPC Flow logs for all egress traffic from srcAddr in the 1 hour around this alert to identify the full sequence of dstPort values targeted on dstAddr within vpcId

2. Check if srcAddr is associated with known vulnerability scanners, corporate IT tooling, or threat intelligence feeds

3. Find other alerts involving srcAddr or vpcId in the past 7 days to determine if this is part of ongoing reconnaissance

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "dstAddr": "192.168.1.1",
  "dstPort": 8080,
  "flowDirection": "egress",
  "region": "us-east-1",
  "srcAddr": "10.0.0.1",
  "srcPort": 54321,
  "subNetId": "subnet-12345678",
  "vpcId": "vpc-12345678"
}