Detection rules › Panther

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
https://aws.amazon.com/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/
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
CollectionT1530 Data from Cloud Storage

Rule body yaml

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
Tests:
  - Name: Access From Approved IP
    ExpectedResult: false
    Log: { "remoteip": "10.0.0.1", "bucket": "my-test-bucket" }
  - Name: Access From Unapproved IP
    ExpectedResult: true
    Log: { "remoteip": "11.0.0.1", "bucket": "my-test-bucket" }
  - Name: Access From IPv6
    ExpectedResult: true
    Log: { "remoteip": "2600:1ffe:8140::a47:a85a", "bucket": "my-test-bucket" }

Detection logic

Rule logic imperative Python

from ipaddress import IPv4Network, IPv6Network, ip_network
from panther_aws_helpers import aws_rule_context
BUCKETS_TO_MONITOR = {
}
ALLOWLIST_NETWORKS = {
    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'"""
    if supernet.network_address.version != subnet.network_address.version:
        return False
    return subnet.subnet_of(supernet)

The parser cannot express this rule's logic as a field filter; the imperative Python above is the detection.

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

Field
eventName
eventSource
awsRegion
recipientAccountId
sourceIPAddress
userAgent
userIdentity
bucket