Detection rules › Kusto

AWSCloudTrail - S3 bucket exposed via policy

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

Detects S3 bucket policy updates that allow public principals without restrictive conditions. Public bucket policies can expose sensitive data and should be reviewed immediately.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 44a5b65e-b0a9-4591-aabc-388fd92a28c4
name: AWSCloudTrail - S3 bucket exposed via policy
description: |
  Detects S3 bucket policy updates that allow public principals without restrictive conditions.
  Public bucket policies can expose sensitive data and should be reviewed immediately.
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 == "PutBucketPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).bucketPolicy))).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 bucket policy public exposure by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} creating public bucket policy exposure in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName == "PutBucketPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).bucketPolicy))).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