Detection rules › Kusto

Workspace deletion activity from an infected device

Severity
medium
Time window
14d
Group by
AadTenantId, AadUserId, AccountObjectId, AccountTenantId
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'This query will alert on any sign-ins from devices infected with malware in correlation with workspace deletion activity. Attackers may attempt to delete workspaces containing compute instances after successful compromise to cause service unavailability to regular business operation.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
ImpactT1489 Service Stop

Rule body kusto

id: a5b3429d-f1da-42b9-883c-327ecb7b91ff
name: Workspace deletion activity from an infected device
description: |
  'This query will alert on any sign-ins from devices infected with malware in correlation with workspace deletion activity. 
  Attackers may attempt to delete workspaces containing compute instances after successful compromise to cause service unavailability to regular business operation.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Impact
relevantTechniques:
  - T1078
  - T1489
query: |
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName == "Azure Active Directory Identity Protection"
    | where AlertName == "Sign-in from an infected device"
    | mv-apply EntityAccount=todynamic(Entities) on
    (
    where EntityAccount.Type == "account"
    | extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
    )
    | mv-apply EntityIp=todynamic(Entities) on
    (
    where EntityIp.Type == "ip"
    | extend IpAddress = tostring(EntityIp.Address)
    )
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserAccount = AccountUPN
    | extend UserName = AccountDisplayName
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | project  AlertName, AlertSeverity, CompromisedEntity, UserAccount, IpAddress, TimeGenerated, UserName
    | join kind=inner 
    (
    AzureActivity
    | where OperationNameValue has_any ("/workspaces/computes/delete", "workspaces/delete") 
    | where ActivityStatusValue has_any ("Succeeded", "Success")
    | project TimeGenerated, ResourceProviderValue, _ResourceId, SubscriptionId, UserAccount=Caller, IpAddress=CallerIpAddress, CorrelationId, OperationId, ResourceGroup, TenantId
    ) on IpAddress, UserAccount
    | extend AccountName = tostring(split(UserAccount, "@")[0]), AccountUPNSuffix = tostring(split(UserAccount, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserAccount
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: _ResourceId
version: 1.0.8
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Platform" ]

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where ProductName == "Azure Active Directory Identity Protection"

Stage 4: where

| where AlertName == "Sign-in from an infected device"

Stage 5: kusto:mv-apply

| mv-apply EntityAccount=todynamic(Entities) on
(
where EntityAccount.Type == "account"
| extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
)

Stage 6: kusto:mv-apply

| mv-apply EntityIp=todynamic(Entities) on
(
where EntityIp.Type == "ip"
| extend IpAddress = tostring(EntityIp.Address)
)

Stage 7: join

| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserAccount = AccountUPN
| extend UserName = AccountDisplayName
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId

Stage 8: extend

| extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
CompromisedEntity =
if(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccount
elseCompromisedEntity

Stage 9: project

| project  AlertName, AlertSeverity, CompromisedEntity, UserAccount, IpAddress, TimeGenerated, UserName

Stage 10: join

| join kind=inner 
(
AzureActivity
| where OperationNameValue has_any ("/workspaces/computes/delete", "workspaces/delete") 
| where ActivityStatusValue has_any ("Succeeded", "Success")
| project TimeGenerated, ResourceProviderValue, _ResourceId, SubscriptionId, UserAccount=Caller, IpAddress=CallerIpAddress, CorrelationId, OperationId, ResourceGroup, TenantId
) on IpAddress, UserAccount

Stage 11: extend

| extend AccountName = tostring(split(UserAccount, "@")[0]), AccountUPNSuffix = tostring(split(UserAccount, "@")[1])

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
AccountDisplayNameis_not_null
  • (no value, null check)
ActivityStatusValuematch
  • Succeeded
  • Success
AlertNameeq
  • Sign-in from an infected device transforms: cased
OperationNameValuematch
  • /workspaces/computes/delete
  • workspaces/delete
ProductNameeq
  • Azure Active Directory Identity Protection transforms: cased
Typeeq
  • account transforms: cased
  • ip transforms: cased
UserAccountis_not_null
  • (no value, null check)

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
AlertNameproject
AlertSeverityproject
CompromisedEntityproject
IpAddressproject
TimeGeneratedproject
UserAccountproject
UserNameproject
AccountNameextend
AccountUPNSuffixextend