Detection rules › Kusto

AWSCloudTrail - S3 bucket access point publicly exposed

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Exfiltration

Telemetry coverage

Rule body

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

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Statementextend
Actionextend
Conditionextend
Effectextend
Principalextend
Principal_awsextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend