Detection rules › Kusto

AWSCloudTrail - Suspicious AWS EC2 Compute Resource Deployments

Severity
medium
Time window
1d
Group by
AccountName, AccountUPNSuffix, RecipientAccountId, SourceIpAddress, UserAgent, UserIdentityArn
Source
github.com/Azure/Azure-Sentinel

'This detection focused on Suspicious deployment of AWS EC2 resource (virtual machine) scale sets was detected. This behavior might indicate that the threat actor is deploying computing resources for cryptocurrency mining activities.This detection centers around identifying suspicious instances of AWS EC2 resource deployment, particularly scale sets. Such behavior raises concerns of potential threat actor involvement, potentially indicative of efforts to deploy computing resources for the purpose of cryptocurrency mining activities.

MITRE ATT&CK coverage

TacticTechniques
ImpactT1496 Resource Hijacking

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 9e457dc4-81f0-4d25-bc37-a5fa4a17946a
name: AWSCloudTrail - Suspicious AWS EC2 Compute Resource Deployments
description: |
  'This detection focused on Suspicious deployment of AWS EC2 resource (virtual machine) scale sets was detected. This behavior might indicate that the threat actor is deploying computing resources for cryptocurrency mining activities.This detection centers around identifying suspicious instances of AWS EC2 resource deployment, particularly scale sets. Such behavior raises concerns of potential threat actor involvement, potentially indicative of efforts to deploy computing resources for the purpose of cryptocurrency mining activities.
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  -  Impact
relevantTechniques:
  - T1496
query: |
  // Retrieve AWS CloudTrail events generated within the last day
  AWSCloudTrail
  // Filter events related to instance creation
  | where EventName =~ "RunInstances"
  // Exclude events with error messages
  | where isempty(ErrorMessage)
  // Extract the event source type
  | extend EventSourceSplit = split(EventSource, ".")
  | extend Type = tostring(EventSourceSplit[0])
  // Extract instance-related details from the event data
  | extend instance = tostring(parse_json(RequestParameters).instanceType),platform = tostring(parse_json(ResponseElements).instancesSet.items[0].platform)
  // Determine the operating system platform
  | extend OSplatform = iff(isempty(platform), tostring("Linux"), platform),CPU = tostring(parse_json(ResponseElements).instancesSet.items[0].cpuOptions),core = toint(parse_json(ResponseElements).instancesSet.items[0].cpuOptions.coreCount),corThread = toint(parse_json(ResponseElements).instancesSet.items[0].cpuOptions.threadsPerCore),InstanceId = tostring(parse_json(ResponseElements).instancesSet.items[0].instanceId)
  // Filter out instances with empty core values
  | where isnotempty(core)
  // Calculate the total compute based on core and thread counts
  | extend totalCorecompute = core * corThread
  | 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 analysis
  | summarize Start= min(TimeGenerated),
    end=   max(TimeGenerated),
    totalgpu= sum(totalCorecompute)
    by SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserAgent
  // Filter results based on total GPU compute and time duration
  | where totalgpu > 800
  | where datetime_diff('hour', end, Start) < 8
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
    UserAgent: UserAgent
    AWSUser: UserIdentityArn
    SourceIpAddress: SourceIpAddress
    TotalCoreCount: totalgpu
alertDetailsOverride:
  alertDisplayNameFormat: Suspicious EC2 deployment - {{totalgpu}} cores deployed by {{AccountName}}
  alertDescriptionFormat: User {{AccountName}} deployed {{totalgpu}} total CPU cores across EC2 instances from {{SourceIpAddress}} within an 8-hour window, suggesting potential cryptocurrency mining activity.
kind: Scheduled
version: 1.0.2

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName =~ "RunInstances"

Stage 3: where

| where isempty(ErrorMessage)

Stage 4: extend (4 consecutive steps)

| extend EventSourceSplit = split(EventSource, ".")
| extend Type = tostring(EventSourceSplit[0])
| extend instance = tostring(parse_json(RequestParameters).instanceType),platform = tostring(parse_json(ResponseElements).instancesSet.items[0].platform)
| extend OSplatform = iff(isempty(platform), tostring("Linux"), platform),CPU = tostring(parse_json(ResponseElements).instancesSet.items[0].cpuOptions),core = toint(parse_json(ResponseElements).instancesSet.items[0].cpuOptions.coreCount),corThread = toint(parse_json(ResponseElements).instancesSet.items[0].cpuOptions.threadsPerCore),InstanceId = tostring(parse_json(ResponseElements).instancesSet.items[0].instanceId)

Stage 5: where

| where isnotempty(core)

Stage 6: extend (5 consecutive steps)

| extend totalCorecompute = core * corThread
| 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 7: summarize

| summarize Start= min(TimeGenerated),
  end=   max(TimeGenerated),
  totalgpu= sum(totalCorecompute)
  by SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserAgent
Threshold
gt 800

Stage 8: where

| where totalgpu > 800

Stage 9: where where end - Start < 8h

| where datetime_diff('hour', end, Start) < 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
ErrorMessageis_null
  • (no value, null check)
EventNameeq
  • RunInstances
coreis_not_null
  • (no value, null check)
totalgpugt
  • 800 transforms: cased

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
RecipientAccountIdsummarize
SourceIpAddresssummarize
Startsummarize
UserAgentsummarize
UserIdentityArnsummarize
endsummarize
totalgpusummarize