Detection rules › Kusto

AWSCloudTrail - Suspicious AWS CLI Command Execution

Severity
medium
Time window
1m
Group by
AccountName, AccountUPNSuffix, RecipientAccountId, SessionMfaAuthenticated, SourceIpAddress, UserIdentityUserName
Source
github.com/Azure/Azure-Sentinel

'This detection focuses on identifying potentially suspicious activities involving the execution of AWS Command Line Interface (CLI) commands, particularly focusing on reconnaissance operations.'

MITRE ATT&CK coverage

Rule body kusto

id: 8c2dc344-9352-4ca1-8863-b1b7a5e09e59
name: AWSCloudTrail - Suspicious AWS CLI Command Execution
description: |
  'This detection focuses on identifying potentially suspicious activities involving the execution of AWS Command Line Interface (CLI) commands, particularly focusing on reconnaissance operations.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
  - T1592.004
  - T1589.002
  - T1589.003
  - T1590
  - T1591
  - T1596
query: |
  let SuspiciousCommands= pack_array('iam.list-users', 'iam.list-groups', 'ec2.describe-vpcs', 'ec2.describe-subnets', 'route53.list-hosted-zones', 'kms.list-keys', 'kms.list-aliases', 'ecs.list-clusters', 'ecs.list-services', 'iam.list-roles', 'iam.get-user''iam.list-access-keys', 'ec2.describe-security-groups', 'ec2.describe-network-acls', 'ec2.describe-network-interfaces', 'ec2.describe-route-tables', 'ec2.describe-internet-gateways', 'ec2.describe-vpc-peering-connections', 'ec2.describe-network-interfaces', 'ec2.describe-network-interfaces', 'ec2.describe-transit-gateway-vpc-attachment', 'ec2.describe-vpc');
  // Retrieve AWS CloudTrail events
  AWSCloudTrail 
  // Filter events with UserAgent starting with "aws-cli"
  | where UserAgent startswith "aws-cli" 
  // Extract the command from the UserAgent using string splitting
  | extend command = tostring(split(UserAgent, "off command/", 1)[0])  
  // Filter events based on predefined suspicious command list
  | where command has_any (SuspiciousCommands)  
  | 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]), "")
  // Summarize relevant information for further analysis
  | summarize 
      CommadCount = dcount(command), 
      EventCount = dcount(EventName), 
      commands = make_list(command), 
      Events = make_list(EventName) 
      by 
      bin(TimeGenerated, 1min), 
      RecipientAccountId, AccountName, AccountUPNSuffix, 
      UserIdentityUserName, 
      SourceIpAddress, 
      SessionMfaAuthenticated 
  // Filter out results with a sufficient count of unique suspicious commands in 1 min time window
  | where CommadCount >= 8
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
customDetails:
   SuspiciousCommand: commands
   AWSUser: UserIdentityUserName
   AWSUserIp: SourceIpAddress
alertDetailsOverride:
  alertDisplayNameFormat: Suspicious AWS CLI reconnaissance by {{AccountName}} - {{CommadCount}} commands
  alertDescriptionFormat: User {{AccountName}} executed {{CommadCount}} suspicious AWS CLI reconnaissance commands from {{SourceIpAddress}} within 1 minute window.
kind: Scheduled
version: 1.0.2

Stages and Predicates

Let binding: SuspiciousCommands

let SuspiciousCommands = pack_array('iam.list-users', 'iam.list-groups', 'ec2.describe-vpcs', 'ec2.describe-subnets', 'route53.list-hosted-zones', 'kms.list-keys', 'kms.list-aliases', 'ecs.list-clusters', 'ecs.list-services', 'iam.list-roles', 'iam.get-user''iam.list-access-keys', 'ec2.describe-security-groups', 'ec2.describe-network-acls', 'ec2.describe-network-interfaces', 'ec2.describe-route-tables', 'ec2.describe-internet-gateways', 'ec2.describe-vpc-peering-connections', 'ec2.describe-network-interfaces', 'ec2.describe-network-interfaces', 'ec2.describe-transit-gateway-vpc-attachment', 'ec2.describe-vpc');

Stage 1: source

AWSCloudTrail

Stage 2: where

| where UserAgent startswith "aws-cli"

Stage 3: extend

| extend command = tostring(split(UserAgent, "off command/", 1)[0])

Stage 4: where

| where command has_any (SuspiciousCommands)

References SuspiciousCommands (defined above).

Stage 5: 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 6: summarize

| summarize 
    CommadCount = dcount(command), 
    EventCount = dcount(EventName), 
    commands = make_list(command), 
    Events = make_list(EventName) 
    by 
    bin(TimeGenerated, 1min), 
    RecipientAccountId, AccountName, AccountUPNSuffix, 
    UserIdentityUserName, 
    SourceIpAddress, 
    SessionMfaAuthenticated
Threshold
ge 8

Stage 7: where

| where CommadCount >= 8

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.

FieldKindValues
CommadCountge
  • 8 transforms: cased
UserAgentstarts_with
  • aws-cli
commandmatch
  • SuspiciousCommands

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.

FieldSource
AccountNamesummarize
AccountUPNSuffixsummarize
CommadCountsummarize
EventCountsummarize
Eventssummarize
RecipientAccountIdsummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
UserIdentityUserNamesummarize
commandssummarize