Detection rules › Kusto
AWSCloudTrail - S3 bucket access point publicly exposed
Detects S3 access point policy changes that allow public principals. Public exposure through access point configuration can lead to unauthorized data access and potential information disclosure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | T1537 Transfer Data to Cloud Account |
Rule body kusto
id: b7a44e0d-ae4c-4fb2-be1b-aa0e45f2327b
name: AWSCloudTrail - S3 bucket access point publicly exposed
description: |
Detects S3 access point policy changes that allow public principals. Public exposure through access point
configuration can lead to unauthorized data access and potential information disclosure.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1537
query: |
AWSCloudTrail
| where EventName == "PutAccessPointPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)
| extend Statement = parse_json(tostring((parse_json(RequestParameters).PutAccessPointPolicyRequest.Policy))).Statement
| mvexpand Statement
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition), Principal_aws = tostring(parse_json(Statement).Principal.AWS), Principal = tostring(parse_json(Statement).Principal)
| extend Action = tostring(Action)
| where Effect =~ "Allow" and (Principal_aws == "*" or Principal == "*") and isempty(Condition)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
| distinct TimeGenerated, EventName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
EventName: EventName
UserIdentityArn: UserIdentityArn
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS S3 access point publicly exposed by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} creating public S3 access point exposure in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName == "PutAccessPointPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).PutAccessPointPolicyRequest.Policy))).Statement
Stage 4: mv-expand
| mvexpand Statement
Stage 5: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition), Principal_aws = tostring(parse_json(Statement).Principal.AWS), Principal = tostring(parse_json(Statement).Principal)
Stage 6: extend
| extend Action = tostring(Action)
Stage 7: where
| where Effect =~ "Allow" and (Principal_aws == "*" or Principal == "*") and isempty(Condition)
Stage 8: extend (4 consecutive steps)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
Stage 9: distinct
| distinct TimeGenerated, EventName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Condition | is_null | |
Effect | eq |
|
ErrorCode | is_null | |
ErrorMessage | is_null | |
EventName | eq |
|
Principal | eq |
|
Principal_aws | eq |
|
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 | Source |
|---|---|
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Principal | extend |
Principal_aws | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |