Detection rules › Kusto

Kusto rules: 1p

Affected rows stateful anomaly on database

#
Status
available
Severity
medium
Time window
14d
Group by
Database
Source
github.com/Azure/Azure-Sentinel

'Goal: To detect anomalous data change/deletion. This query detects SQL queries that changed/deleted a large number of rows, which is significantly higher than normal for this database. The detection is calculated inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher threshold will detect only more severe anomalies).'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2a632013-379d-4993-956f-615063d31e10
name: Affected rows stateful anomaly on database
description: |
  'Goal: To detect anomalous data change/deletion. This query detects SQL queries that changed/deleted a large number of rows, which is significantly higher than normal for this database.
  The detection is calculated inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher threshold will detect only more severe anomalies).'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Impact
relevantTechniques:
  - T1485 
  - T1565 
  - T1491 
tags:
  - SQL
query: |
    let volumeThresholdZ = 3.0;                     // Minimal threshold for the Zscore to trigger anomaly (number of standard deviations above mean). If set higher, only very significant alerts will fire.
    let volumeThresholdQ = volumeThresholdZ;        // Minimal threshold for the Qscore to trigger anomaly (number of Inter-Percentile Ranges above high percentile). If set higher, only very significant alerts will fire.
    let volumeThresholdHardcoded = 500;             // Minimal value for the volume metric to trigger anomaly.
    let detectionWindow = 1h;                       // The size of the recent detection window for detecting anomalies.  
    let trainingWindow = detectionWindow + 14d;     // The size of the training window before the detection window for learning the normal state.
    let monitoredColumn = 'AffectedRows';           // The name of the column for volumetric anomalies.
    let processedData = materialize (
        AzureDiagnostics
        | where TimeGenerated >= ago(trainingWindow)
        | where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
        | project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
                  ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
                  IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
                  ResponseRows = response_rows_d, Statement = statement_s
        | extend QuantityColumn = column_ifexists(monitoredColumn, 0)
        | extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',
                                               (ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')
        | where WindowType in ('detection', 'training'));
    let trainingSet =
        processedData
        | where WindowType == 'training'
        | summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
                    P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
          by Database;
    processedData
    | where WindowType == 'detection'
    | join kind = inner (trainingSet) on Database
    | extend ZScoreVal = iff(N >= 20, round(todouble(QuantityColumn - AvgVal) / todouble(StdVal + 1), 2), 0.00),
             QScoreVal = iff(N >= 20, round(todouble(QuantityColumn - P99Val) / todouble(P99Val - P50Val + 1), 2), 0.00)
    | extend IsVolumeAnomalyOnVal = iff((ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ and QuantityColumn > volumeThresholdHardcoded), true, false), AnomalyScore = round((ZScoreVal + QScoreVal)/2, 0)
    | where IsVolumeAnomalyOnVal == 'true'
    | project TimeGenerated, Database, PrincipalName, ClientIp, HostName, ApplicationName, ActionName, Statement,
              IsSuccess, ResponseRows, AffectedRows, IsVolumeAnomalyOnVal, AnomalyScore, ResourceId
    | extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ApplicationName
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
alertDetailsOverride:
  alertDisplayNameFormat: 'Affected rows anomaly on database {{Database}}'
  alertDescriptionFormat: |
    'An anomaly was detected on database {{Database}} with {{AffectedRows}} affected rows in a statement, which is significantly higher than normal for this database. Investigate the database activity for potential suspicious data access, collection, or exfiltration.'
kind: Scheduled
version: 1.1.3

Stages and Predicates

Parameters

let volumeThresholdZ = 3.0;
let volumeThresholdQ = volumeThresholdZ;
let volumeThresholdHardcoded = 500;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredColumn = 'AffectedRows';

let processedData is inlined into the numbered stages below.

Let Binding: trainingSet used in Stage 9

let trainingSet = processedData
    | where WindowType == 'training'
    | summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
                P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
      by Database;

Stages 1 to 7 define let processedData as the rule's main pipeline source. Stages 8 to 15 use it.

Stage 1: source

AzureDiagnostics

Stage 2: where

| where TimeGenerated >= ago(trainingWindow)

Stage 3: where

| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM")

Stage 4: project

| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
              ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
              IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
              ResponseRows = response_rows_d, Statement = statement_s

Stage 5: extend

| extend QuantityColumn = column_ifexists(monitoredColumn, 0)

Stage 6: extend

| extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',
                                           (ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')
WindowType =
ifTimeGenerated >= ago(3600s)'detection'
elif/* macro: (ago(trainingWindow) <= TimeGenerated) */'training'
else'other'

Stage 7: where

| where WindowType in ('detection', 'training')

Stage 8: where

processedData
| where WindowType == 'detection'

Stage 9: join

| join kind = inner (trainingSet) on Database

Stage 10: extend

extend QScoreVal, ZScoreVal
QScoreVal =
ifN >= 20round((todouble((QuantityColumn - P99Val)) / todouble(((P99Val - P50Val) + 1))), 2)
else0.0
ZScoreVal =
ifN >= 20round((todouble((QuantityColumn - AvgVal)) / todouble((StdVal + 1))), 2)
else0.0

Stage 11: extend

extend AnomalyScore, IsVolumeAnomalyOnVal
IsVolumeAnomalyOnVal =
if(ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ) and QuantityColumn > volumeThresholdHardcodedtrue
elsefalse

Stage 12: where

where IsVolumeAnomalyOnVal == true

Stage 13: project

project ActionName, AffectedRows, AnomalyScore, ApplicationName, ClientIp, Database, HostName, IsSuccess, IsVolumeAnomalyOnVal, PrincipalName, ResourceId, ResponseRows, Statement, TimeGenerated

Stage 14: extend

extend Name, UPNSuffix

Stage 15: summarize aggregation inside the join branch

summarize by Database

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Databasesummarize

Anomalous sign-in location by user account and authenticating application

#
Status
available
Severity
medium
Time window
7d
Group by
AppDisplayName, AppId, CountOfLocations, TimeGenerated, UserId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

'This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an individual application.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 7cb8f77d-c52f-4e46-b82f-3cf2e106224a
name: Anomalous sign-in location by user account and authenticating application
description: |
  'This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an individual application.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  // Adjust this figure to adjust how sensitive this detection is
  let sensitivity = 2.5;
  // Adjust this figure to set the value that defines the requested estimation accuracy. The default value is 1. Possible values are 0, 1, 2, 3, 4.
  let dcountAccuracy = 1;
  let AuthEvents = materialize(
  union isfuzzy=True SigninLogs, AADNonInteractiveUserSignInLogs
  | where TimeGenerated between (ago(7d) .. now())
  | where ResultType == 0
  | extend LocationDetails = LocationDetails_dynamic
  | extend Location = strcat(LocationDetails.countryOrRegion, "-", LocationDetails.state,"-", LocationDetails.city)
  | where Location != "--");
  AuthEvents
  | summarize dcount(Location, dcountAccuracy) by AppDisplayName, AppId, UserPrincipalName, UserId, bin(startofday(TimeGenerated), 1d)
  | where dcount_Location > 2
  | make-series CountOfLocations = sum(dcount_Location) on TimeGenerated  step 1d by AppId, UserId
  | extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfLocations, sensitivity, -1, 'linefit')
  | mv-expand CountOfLocations to typeof(double), TimeGenerated to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
  | where Anomalies > 0 and Baseline > 0
  | join kind=inner( AuthEvents | extend TimeStamp = startofday(TimeGenerated)) on UserId, AppId
  | extend SignInDetails = bag_pack("TimeGenerated", TimeGenerated1, "Location", Location, "Source", IPAddress, "Device", DeviceDetail_dynamic)
  | summarize SignInDetailsSet=make_set(SignInDetails, 1000) by UserId, UserPrincipalName, CountOfLocations, TimeGenerated, AppId, AppDisplayName
  | extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId   
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails:
  Application: AppDisplayName
alertDetailsOverride:
  alertDisplayNameFormat: Anomalous sign-in location by {{UserPrincipalName}} to {{AppDisplayName}}
  alertDescriptionFormat: |
    This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an
    individual application. This has detected {{UserPrincipalName}} signing into {{AppDisplayName}} from {{CountOfLocations}} 
    different locations.
version: 2.0.5
kind: Scheduled

Stages and Predicates

Parameters

let sensitivity = 2.5;
let dcountAccuracy = 1;

Union isfuzzy=True (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SigninLogs, AADNonInteractiveUserSignInLogs

Leg 1: SigninLogs

Leg 2: AADNonInteractiveUserSignInLogs

Applied to the Combined Result

| where TimeGenerated between (ago(7d) .. now()) | where ResultType == 0 | extend LocationDetails = LocationDetails_dynamic | extend Location = strcat(LocationDetails.countryOrRegion, "-", LocationDetails.state,"-", LocationDetails.city) | where Location != "--" | summarize dcount(Location, dcountAccuracy) by AppDisplayName, AppId, UserPrincipalName, UserId, bin(startofday(TimeGenerated), 1d) | where dcount_Location > 2 | make-series CountOfLocations = sum(dcount_Location) on TimeGenerated  step 1d by AppId, UserId | extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfLocations, sensitivity, -1, 'linefit') | mv-expand CountOfLocations to typeof(double), TimeGenerated to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long) | where Anomalies > 0 and Baseline > 0 | join kind=inner( AuthEvents | extend TimeStamp = startofday(TimeGenerated)) on UserId, AppId | extend SignInDetails = bag_pack("TimeGenerated", TimeGenerated1, "Location", Location, "Source", IPAddress, "Device", DeviceDetail_dynamic) | summarize SignInDetailsSet=make_set(SignInDetails, 1000) by UserId, UserPrincipalName, CountOfLocations, TimeGenerated, AppId, AppDisplayName | extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNamesummarize
AppIdsummarize
CountOfLocationssummarize
SignInDetailsSetsummarize
TimeGeneratedsummarize
UserIdsummarize
UserPrincipalNamesummarize
Nameextend
UPNSuffixextend

Apache - Private IP in URL

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

'Detects requests to unusual URL'

MITRE ATT&CK Coverage

Rule Body

id: db5f16f0-3afe-11ec-8d3d-0242ac130003
name: Apache - Private IP in URL
description: |
  'Detects requests to unusual URL'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ApacheHTTPServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  ApacheHTTPServer
  | where UrlOriginal matches regex @'(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.1[6-9]\.\d{1,3}\.\d{1,3})|(172\.2[0-9]\.\d{1,3}\.\d{1,3})|(172\.3[0-1]\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3})'
  | extend UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

ApacheHTTPServer

Stage 2: where

| where UrlOriginal matches regex @'(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.1[6-9]\.\d{1,3}\.\d{1,3})|(172\.2[0-9]\.\d{1,3}\.\d{1,3})|(172\.3[0-1]\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3})'

Stage 3: extend

| extend UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
UrlOriginalregex_match
    • (10.\d{1,3}.\d{1,3}.\d{1,3})
    • (172.1[6-9].\d{1,3}.\d{1,3})
    • (172.2[0-9].\d{1,3}.\d{1,3})
    • (172.3[0-1].\d{1,3}.\d{1,3})
    • (192.168.\d{1,3}.\d{1,3})
field:"UrlOriginal" kind:regex_match

Output Fields

These fields are emitted when the rule matches.

FieldSource
UrlCustomEntityextend

ApexOne - Possible exploit or execute operation

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

'Detects possible exploit or execute operation.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Privilege Escalation

Rule Body

id: e289d762-6cc2-11ec-90d6-0242ac120003
name: ApexOne - Possible exploit or execute operation
description: |
  'Detects possible exploit or execute operation.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1546
query: |
  TMApexOneEvent
  | where EventMessage has "Behavior Monitoring"
  | extend DeviceCustomNumber3 = coalesce(column_ifexists("FieldDeviceCustomNumber3", long(null)),DeviceCustomNumber3)
  | where Event_Type == 2048 or DeviceCustomNumber3 == 403 or DeviceCustomNumber3 == 601
  | extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.5
kind: Scheduled 

Stages and Predicates

Stage 1: source

TMApexOneEvent

Stage 2: where

| where EventMessage has "Behavior Monitoring"

Stage 3: extend

| extend DeviceCustomNumber3 = coalesce(column_ifexists("FieldDeviceCustomNumber3", long(null)),DeviceCustomNumber3)

Stage 4: where

| where Event_Type == 2048 or DeviceCustomNumber3 == 403 or DeviceCustomNumber3 == 601

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceCustomNumber3extend
AccountCustomEntityextend
IPCustomEntityextend

AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy

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

Detects creation of inline IAM policies that grant broad DynamoDB create, read, update, and delete permissions. Attackers may use this access expansion to manipulate data stores and escalate influence across cloud workloads.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b9be2aa6-911d-4131-8658-d2a537ed49f4
name: AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy
description: |
  Detects creation of inline IAM policies that grant broad DynamoDB create, read, update, and delete permissions.
  Attackers may use this access expansion to manipulate data stores and escalate influence across cloud workloads.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1098.003
query: |
  AWSCloudTrail
  | where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).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)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and ((Action contains "dynamodb:Create" or Action contains "dynamodb:Put") and (Action contains "dynamodb:Describe" or Action contains "dynamodb:Get" or Action contains "dynamodb:Scan")  and Action contains "dynamodb:Update" and Action contains "dynamodb:Delete") and Resource == "*" and 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, PolicyName, 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:
  PolicyName: PolicyName
  EventName: EventName
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS DynamoDB privilege escalation policy update by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline DynamoDB escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 4: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 5: mv-expand

| mvexpand Statement

Stage 6: 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)

Stage 7: extend

| extend Action = tostring(Action)

Stage 8: where

| where Effect =~ "Allow" and ((Action contains "dynamodb:Create" or Action contains "dynamodb:Put") and (Action contains "dynamodb:Describe" or Action contains "dynamodb:Get" or Action contains "dynamodb:Scan")  and Action contains "dynamodb:Update" and Action contains "dynamodb:Delete") and Resource == "*" and Condition == ""

Stage 9: 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 10: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • dynamodb:Create
  • dynamodb:Delete
  • dynamodb:Describe
  • dynamodb:Get
  • dynamodb:Put
  • dynamodb:Scan
  • dynamodb:Update
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • PutGroupPolicy corpus 12 (kusto 11, panther 1)
  • PutRolePolicy corpus 12 (kusto 11, panther 1)
  • PutUserPolicy corpus 12 (kusto 11, panther 1)
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend
Statementextend
Actionextend
Conditionextend
Effectextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

AWSCloudTrail - Privilege escalation with AdministratorAccess managed policy

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

Detects successful attachment of the AWS managed AdministratorAccess policy to users, roles, or groups. This action can immediately grant full administrative permissions and is a high-risk privilege escalation pattern that should be validated against approved administrative activity.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 139e7116-3884-4246-9978-c8f740770bdf
name: AWSCloudTrail - Privilege escalation with AdministratorAccess managed policy
description: |
  Detects successful attachment of the AWS managed AdministratorAccess policy to users, roles, or groups.
  This action can immediately grant full administrative permissions and is a high-risk privilege escalation pattern
  that should be validated against approved administrative activity.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1098.003
query: |
  AWSCloudTrail
  | where  EventName in ("AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
  | where tostring(parse_json(RequestParameters).policyArn) startswith "arn:aws:iam::aws:policy/AdministratorAccess"
  | 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]), "")
  | project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, UserIdentityArn, ResponseElements, RecipientAccountId, AccountName, AccountUPNSuffix
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
  EventSource: EventSource
  AWSRegion: AWSRegion
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS AdministratorAccess policy attachment by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} attaching AdministratorAccess in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where  EventName in ("AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: where

| where tostring(parse_json(RequestParameters).policyArn) startswith "arn:aws:iam::aws:policy/AdministratorAccess"

Stage 4: 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 5: project

| project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, UserIdentityArn, ResponseElements, RecipientAccountId, AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • AttachGroupPolicy corpus 17 (kusto 15, panther 2)
  • AttachRolePolicy corpus 17 (kusto 15, panther 2)
  • AttachUserPolicy corpus 17 (kusto 15, panther 2)
field:"aws::eventName" kind:in
RequestParameters.policyArnstarts_with
  • arn:aws:iam::aws:policy/AdministratorAccess transforms: tostring, kusto_accessor_expr:{"expression":"tostring(parse_json(RequestParameters).policyArn)","field":"RequestParameters.policyArn"}, kusto_dynamic_path
field:"RequestParameters.policyArn" kind:starts_with value:"arn:aws:iam::aws:policy/AdministratorAccess"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionproject
AccountNameproject
AccountUPNSuffixproject
AdditionalEventDataproject
EventNameproject
EventSourceproject
EventTypeNameproject
RecipientAccountIdproject
RequestParametersproject
ResponseElementsproject
SessionMfaAuthenticatedproject
SourceIpAddressproject
TimeGeneratedproject
UserAgentproject
UserIdentityAccountIdproject
UserIdentityArnproject
UserIdentityPrincipalidproject
UserIdentityUserNameproject

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

AWSCloudTrail - SAML update identity provider

#
Status
available
Severity
high
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Detects updates to AWS SAML identity provider configuration. Unauthorized changes can allow adversaries to issue trusted federation tokens and gain persistent or privileged access.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: bce1dcba-4948-414d-8838-6385afb9d496
name: AWSCloudTrail - SAML update identity provider
description: |
  Detects updates to AWS SAML identity provider configuration. Unauthorized changes can allow adversaries to
  issue trusted federation tokens and gain persistent or privileged access.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1078
query: |
    AWSCloudTrail
    | where EventName == "UpdateSAMLProvider" and isempty(ErrorCode) and isempty(ErrorMessage)
    | 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]), "")
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
  EventSource: EventSource
  AWSRegion: AWSRegion
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS SAML identity provider updated by {{AccountName}}'
  alertDescriptionFormat: 'Detected UpdateSAMLProvider from {{SourceIpAddress}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

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

Stage 3: 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]), "")

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

Bitglass - Multiple files shared with external entity

#
Status
available
Severity
medium
Time window
10m
Group by
User
Source
github.com/Azure/Azure-Sentinel

'Detects when multiple files shared with external entity.'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 09690f9b-33d1-4372-a6aa-eb7d3b3cdebc
name: Bitglass - Multiple files shared with external entity
description: |
  'Detects when multiple files shared with external entity.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Bitglass
    dataTypes:
      - Bitglass
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  let threshold = 5;
  Bitglass
  | where EventType =~ 'cloudaudit'
  | where Action =~ 'Action  DLPScan'
  | where EventMessage has_all ('External', 'Shared', 'DLP')
  | summarize f_cnt = makeset(SrcFileName) by User, bin(TimeGenerated, 10m)
  | where array_length(f_cnt) >= threshold
  | extend AccountCustomEntity = User
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5;

Stage 1: source

Bitglass

Stage 2: where

| where EventType =~ 'cloudaudit'

Stage 3: where

| where Action =~ 'Action  DLPScan'

Stage 4: where

| where EventMessage has_all ('External', 'Shared', 'DLP')

Stage 5: summarize

| summarize f_cnt = makeset(SrcFileName) by User, bin(TimeGenerated, 10m)

Stage 6: where

| where array_length(f_cnt) >= threshold

Stage 7: extend

| extend AccountCustomEntity = User

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • Action DLPScan
field:"Action" kind:eq value:"Action DLPScan"
EventMessagematch
  • DLP transforms: term
  • External transforms: term
  • Shared transforms: term
field:"EventMessage" kind:match
EventTypeeq
  • cloudaudit
field:"EventType" kind:eq value:"cloudaudit"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Usersummarize
f_cntsummarize
AccountCustomEntityextend

BloodHound Attack Path Finding - Reset a Tier Zero User's Password

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

BloodHound Attack Path Finding - Reset a Tier Zero User's Password

Rule Body

id: 7874ea85-ec86-4519-9aa6-7b7d8c8c4a21
name: BloodHound Attack Path Finding - Reset a Tier Zero User's Password
description: BloodHound Attack Path Finding - Reset a Tier Zero User's Password
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "AzureT0ResetPassword"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "AzureT0ResetPassword"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • AzureT0ResetPassword
field:"Finding" kind:eq value:"AzureT0ResetPassword"

Contrast ADR - EDR Alert Correlation

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
medium
Time window
5m
Group by
incidentId
Source
github.com/Azure/Azure-Sentinel

'Correlates Contrast ADR incidents with specific high-risk attack patterns including command injection, deserialization attacks, and file upload vulnerabilities. This rule identifies confirmed security events that require immediate attention from security teams.'

MITRE ATT&CK Coverage

Rule Body

id: c1c6ba64-134e-403b-b9a6-1bebc90809a4
name: Contrast ADR - EDR Alert Correlation
description: |
  'Correlates Contrast ADR incidents with specific high-risk attack patterns including command injection, deserialization attacks, and file upload vulnerabilities. This rule identifies confirmed security events that require immediate attention from security teams.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ContrastADRCCF
    dataTypes:
      - ContrastADRIncidents_CL
      - ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - DefenseEvasion
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1059
  - T1055
  - T1190
  - T1008
query: |
  ContrastADRIncidents_CL
  | join kind=inner (ContrastADRAttackEvents_CL | where rule in~("class-loader-manipulation", "cmd-injection-semantic-chained-commands", "cmd-injection-semantic-dangerous-paths", "cmd-injection-command-backdoors", "cmd-injection-process-hardening", "cmd-injection", "expression-language-injection", "jndi-injection", "ssjs-injection", "unsafe-file-upload", "untrusted-deserialization","xxe") | project-rename hostname = host_hostname) on incidentId
  //Please add your EDR table name in place of ContrastWAFLogs_CL and hostname's column name in place of hostname below and uncomment the queries below
  //| join kind = inner ( ContrastWAFLogs_CL
  //| where TimeGenerated >= ago(5m)) on hostname
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: hostname
alertDetailsOverride:
  alertDisplayNameFormat: 'EDR Alert Confirmed {{result}} by Contrast ADR on {{application_name}}'
  alertDescriptionFormat: 'EDR Alert Confirmed {{result}} by Contrast ADR on {{application_name}}'
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: Selected
    groupByEntities:
      - Host
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.2

Stages and Predicates

Stage 1: source

ContrastADRIncidents_CL

Stage 2: join

| join kind=inner (ContrastADRAttackEvents_CL | where rule in~("class-loader-manipulation", "cmd-injection-semantic-chained-commands", "cmd-injection-semantic-dangerous-paths", "cmd-injection-command-backdoors", "cmd-injection-process-hardening", "cmd-injection", "expression-language-injection", "jndi-injection", "ssjs-injection", "unsafe-file-upload", "untrusted-deserialization","xxe") | project-rename hostname = host_hostname) on incidentId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
rulein
  • class-loader-manipulation
  • cmd-injection
  • cmd-injection-command-backdoors
  • cmd-injection-process-hardening
  • cmd-injection-semantic-chained-commands
  • cmd-injection-semantic-dangerous-paths
  • expression-language-injection
  • jndi-injection
  • ssjs-injection
  • unsafe-file-upload
  • untrusted-deserialization
  • xxe
field:"rule" kind:in

CreepyDrive URLs

#
Severity
high
Time window
1d
Author
Thomas McElroy
Source
github.com/Azure/Azure-Sentinel

'CreepyDrive uses OneDrive for command and control. This detection identifies URLs specific to CreepyDrive.'

MITRE ATT&CK Coverage

Rule Body

id: b6d03b88-4d27-49a2-9c1c-29f1ad2842dc
name: CreepyDrive URLs
description: |
  'CreepyDrive uses OneDrive for command and control. This detection identifies URLs specific to CreepyDrive.'
severity: High
requiredDataConnectors:
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
relevantTechniques:
  - T1567.002
  - T1102.002
tags:
  - POLONIUM
query: |
  let oneDriveCalls = dynamic(['graph.microsoft.com/v1.0/me/drive/root:/Documents/data.txt:/content','graph.microsoft.com/v1.0/me/drive/root:/Documents/response.json:/content']);
  let oneDriveCallsRegex = dynamic([@'graph\.microsoft\.com\/v1\.0\/me\/drive\/root\:\/Uploaded\/.*\:\/content',@'graph\.microsoft\.com\/v1\.0\/me\/drive\/root\:\/Downloaded\/.*\:\/content']);
  CommonSecurityLog
  | where RequestURL has_any (oneDriveCalls) or RequestURL matches regex tostring(oneDriveCallsRegex[0]) or RequestURL matches regex tostring(oneDriveCallsRegex[1])
  | project TimeGenerated, DeviceVendor, DeviceProduct, DeviceAction, DestinationDnsDomain, DestinationIP, RequestURL, SourceIP, SourceHostName, RequestClientApplication
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SourceHostName
version: 1.0.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Thomas McElroy
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Let Binding: oneDriveCalls used in Stage 2

let oneDriveCalls = dynamic(['graph.microsoft.com/v1.0/me/drive/root:/Documents/data.txt:/content','graph.microsoft.com/v1.0/me/drive/root:/Documents/response.json:/content']);

Let Binding: oneDriveCallsRegex used in Stage 2

let oneDriveCallsRegex = dynamic([@'graph\.microsoft\.com\/v1\.0\/me\/drive\/root\:\/Uploaded\/.*\:\/content',@'graph\.microsoft\.com\/v1\.0\/me\/drive\/root\:\/Downloaded\/.*\:\/content']);

Stage 1: source

CommonSecurityLog

Stage 2: where

| where RequestURL has_any (oneDriveCalls) or RequestURL matches regex tostring(oneDriveCallsRegex[0]) or RequestURL matches regex tostring(oneDriveCallsRegex[1])

Stage 3: project

| project TimeGenerated, DeviceVendor, DeviceProduct, DeviceAction, DestinationDnsDomain, DestinationIP, RequestURL, SourceIP, SourceHostName, RequestClientApplication

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RequestURLmatch
  • graph.microsoft.com/v1.0/me/drive/root:/Documents/data.txt:/content transforms: term
  • graph.microsoft.com/v1.0/me/drive/root:/Documents/response.json:/content transforms: term
field:"RequestURL" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationDnsDomainproject
DestinationIPproject
DeviceActionproject
DeviceProductproject
DeviceVendorproject
RequestClientApplicationproject
RequestURLproject
SourceHostNameproject
SourceIPproject
TimeGeneratedproject

CYFIRMA - Brand Intelligence - Social Media Handle Impersonation Detected High Rule

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"Detects high-severity alerts related to impersonation of official social media handles associated with your brand. These spoofed accounts may be used for phishing, disinformation, or fraud campaigns, posing significant reputational and security risks."

MITRE ATT&CK Coverage

Rule Body

id: 22f49d67-7da7-4809-8d07-89e4478aa6b0
name: CYFIRMA - Brand Intelligence - Social Media Handle Impersonation Detected High Rule
description: |
  "Detects high-severity alerts related to impersonation of official social media handles associated with your brand. 
  These spoofed accounts may be used for phishing, disinformation, or fraud campaigns, posing significant reputational and security risks."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaBrandIntelligenceAlertsDC
    dataTypes:
      - CyfirmaBISocialHandlersAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Reconnaissance
  - ResourceDevelopment
  - InitialAccess
relevantTechniques:
  - T1589.003
  - T1591.002
  - T1585.001
  - T1566.002
query: |
  // High severity - Social Media Handle Impersonation
  let timeFrame = 5m;
  CyfirmaBISocialHandlersAlerts_CL 
    | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
    | extend
        Description=description,
        FirstSeen=first_seen,
        LastSeen=last_seen,
        RiskScore=risk_score,
        AlertUID=alert_uid,
        UID=uid,
        AssetType=asset_type,
        AssetValue=asset_value,
        Impact=impact,
        Recommendation=recommendation,
        SourceSype=source_type,
        ProviderName="CYFIRMA",
        ProductName="DeCYFIR/DeTCT"
    | project 
        TimeGenerated,
        Description,
        RiskScore,
        FirstSeen,
        LastSeen,
        AlertUID,
        UID,
        AssetType,
        AssetValue,
        Impact,
        Recommendation,
        SourceSype,
        ProductName,
        ProviderName
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  Impact: Impact
  Recommendation: Recommendation
  SourceSype: SourceSype
  Description: Description
  RiskScore: RiskScore
  TimeGenerated: TimeGenerated
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Social Media Handle Impersonation Detected - {{AssetType}}  :  {{AssetValue}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaBISocialHandlersAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=asset_value,
      Impact=impact,
      Recommendation=recommendation,
      SourceSype=source_type,
      ProviderName="CYFIRMA",
      ProductName="DeCYFIR/DeTCT"

Stage 4: project

| project 
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      Recommendation,
      SourceSype,
      ProductName,
      ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
AssetTypeproject
AssetValueproject
Descriptionproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
Recommendationproject
RiskScoreproject
SourceSypeproject
TimeGeneratedproject
UIDproject

DopplePaymer Procdump

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

This query was originally published in the threat analytics report, Doppelpaymer: More human-operated ransomware. There is also a related blog. DoppelPaymer is ransomware that is spread manually by human operators. These operators have exhibited extensive knowledge of system administration and common network security misconfigurations. For example, they use SysInternal utilities such as ProcDump to dump credentials from LSASS. They often use these stolen credentials to turn off security software, run malicious commands, and spread malware throughout an organization. The following query detects ProcDump being used to dump credentials from LSASS. The See also section below lists links to other queries associated with DoppelPaymer. References: https://msrc-blog.microsoft.com/2019/11/20/customer-guidance-for-the-dopplepaymer-ransomware/ https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/DoppelPaymer.KM!MTB https://docs.microsoft.com/sysinternals/downloads/procdump https://docs.microsoft.com/windows-server/security/credentials-protection-and-management/configuring-additional-lsa-protection

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: 1be34fb9-f81b-47ae-84fb-465e6686d76c
name: DopplePaymer Procdump
description: |
  This query was originally published in the threat analytics report, Doppelpaymer: More human-operated ransomware. There is also a related blog.
  DoppelPaymer is ransomware that is spread manually by human operators. These operators have exhibited extensive knowledge of system administration and common network security misconfigurations. For example, they use SysInternal utilities such as ProcDump to dump credentials from LSASS. They often use these stolen credentials to turn off security software, run malicious commands, and spread malware throughout an organization.
  The following query detects ProcDump being used to dump credentials from LSASS.
  The See also section below lists links to other queries associated with DoppelPaymer.
  References:
  https://msrc-blog.microsoft.com/2019/11/20/customer-guidance-for-the-dopplepaymer-ransomware/
  https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/DoppelPaymer.KM!MTB
  https://docs.microsoft.com/sysinternals/downloads/procdump
  https://docs.microsoft.com/windows-server/security/credentials-protection-and-management/configuring-additional-lsa-protection
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1003
tags:
  - DoppelPaymer
  - Ransomware
  - Procdump
  - Credential Dumping
query: |
  // Dumping of LSASS memory using procdump
  DeviceProcessEvents
  // Command lines that include "lsass" and -accepteula or -ma flags used in procdump
  | where (ProcessCommandLine has "lsass" and (ProcessCommandLine has "-accepteula" or
  ProcessCommandLine contains "-ma"))
  // Omits possible FPs where the full command is just "procdump.exe lsass"
  or (FileName in~ ('procdump.exe','procdump64.exe') and ProcessCommandLine has 'lsass')
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where (ProcessCommandLine has "lsass" and (ProcessCommandLine has "-accepteula" or
ProcessCommandLine contains "-ma"))
or (FileName in~ ('procdump.exe','procdump64.exe') and ProcessCommandLine has 'lsass')

Stage 3: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 4: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNamein
  • procdump.exe corpus 2 (splunk 1, kusto 1)
  • procdump64.exe corpus 2 (splunk 1, kusto 1)
field:"process_basename" kind:in
ProcessCommandLinecontains
  • -ma corpus 3 (kusto 2, sigma 1)
field:"CommandLine" kind:contains value:"-ma"
ProcessCommandLinematch
  • -accepteula transforms: term corpus 2 (kusto 2)
  • lsass transforms: term corpus 2 (kusto 2)
field:"CommandLine" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostNameextend
DnsDomainextend

Failed AWS Console logons but success logon to AzureAD

#
Severity
medium
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies a list of IP addresses with a minimum number (default of 5) of failed logon attempts to AWS Console. Uses that list to identify any successful Microsoft Entra ID logons from these IPs within the same timeframe.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 910124df-913c-47e3-a7cd-29e1643fa55e
name: Failed AWS Console logons but success logon to AzureAD
description: |
  'Identifies a list of IP addresses with a minimum number (default of 5) of failed logon attempts to AWS Console.
  Uses that list to identify any successful Microsoft Entra ID logons from these IPs within the same timeframe.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
     - SigninLogs
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110

query: |
  //Adjust this threshold to fit environment
  let  signin_threshold = 5;
  //Make a list of IPs with failed AWS console logins
  let aws_fails = AWSCloudTrail
  | where EventName == "ConsoleLogin"
  | extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
  | where LoginResult != "Success"
  | where SourceIpAddress != "127.0.0.1"
  | summarize count() by SourceIpAddress
  | where count_ >  signin_threshold
  | summarize make_set(SourceIpAddress);
  //See if any of those IPs have sucessfully logged into Azure AD.
  SigninLogs
  | where ResultType in ("0", "50125", "50140")
  | where IPAddress in (aws_fails)
  | extend Reason = "Multiple failed AWS Console logins from IP address"
  | extend timestamp = TimeGenerated, AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.2.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let signin_threshold = 5;

Let Binding: aws_fails used in Stage 3

let aws_fails = AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
| where LoginResult != "Success"
| where SourceIpAddress != "127.0.0.1"
| summarize count() by SourceIpAddress
| where count_ >  signin_threshold
| summarize make_set(SourceIpAddress);

Stage 1: source

SigninLogs

Stage 2: where

| where ResultType in ("0", "50125", "50140")

Stage 3: where

| where IPAddress in (aws_fails)

Stage 4: extend

| extend Reason = "Multiple failed AWS Console logins from IP address"

Stage 5: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IPAddressin
  • aws_fails
field:"src_ip" kind:in value:"aws_fails"
ResultTypein
  • 0 corpus 2 (kusto 2)
  • 50125
  • 50140
field:"ResultType" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Reasonextend
AccountNameextend
AccountUPNSuffixextend
timestampextend

Hypervisor Host Settings Updated

#
Status
available
Severity
informational
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Detects when hypervisor host settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.

Rule Body

id: 580da4d1-8e39-4420-855d-3d4f653fe127
name: Hypervisor Host Settings Updated
description: Detects when hypervisor host settings are updated in Veeam Backup & Replication.
  This might indicate configuration changes that require review.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 25800\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 25800

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
  MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Jamf Protect - Network Threats

#
Status
available
Severity
informational
Source
github.com/Azure/Azure-Sentinel

'Creates an incident based based on Jamf Protect's Network Threat Event Stream alerts.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 44da53c3-f3b0-4b70-afff-f79275cb9442
name: Jamf Protect - Network Threats
description: |
  'Creates an incident based based on Jamf Protect's Network Threat Event Stream alerts.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: JamfProtect
    dataTypes: 
      - jamfprotect_CL
suppressionDuration: PT5H
suppressionEnabled: false
tactics:
  - InitialAccess
relevantTechniques:
  - T1133
query: |
  JamfProtectThreatEvents
  | where EventResult == "Blocked"
      and isnotempty(EventSeverity)
  | extend Tactics = "Initial Access"
  | extend Techniques = "T1566"
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Network Threat detected on {{DvcHostname}}"
  alertDescriptionFormat: "A Network Threat has been {{EventResult}} on {{DvcHostname}}"
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: EventSeverity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: EventReportUrl
    - alertProperty: ProviderName
      value: EventVendor
    - alertProperty: ProductName
      value: EventProduct
    - alertProperty: RemediationSteps
      value: EventResult
    - alertProperty: Techniques
      value: Techniques
customDetails:
  Category: ThreatCategory
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: Hostname
  - identifier: OSFamily
    columnName: DvcOs
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: DstIpAddr
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: SrcUsermail
  - identifier: FullName
    columnName: SrcUsername
- entityType: URL
  fieldMappings:
  - identifier: Url
    columnName: DnsQueryName
version: 1.0.5
kind: NRT

Stages and Predicates

Stage 1: source

JamfProtectThreatEvents

Stage 2: where

| where EventResult == "Blocked"
    and isnotempty(EventSeverity)

Stage 3: extend

| extend Tactics = "Initial Access"

Stage 4: extend

| extend Techniques = "T1566"

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Tacticsextend
Techniquesextend

M2131_AssetStoppedLogging

#
Status
available
Severity
medium
Time window
14d
Group by
ResourceId
Source
github.com/Azure/Azure-Sentinel

'This alert is designed to monitor assets within the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when a monitored asset fails to provide a heartbeat within 24 hours.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 4be5b645-1d08-49e4-b58d-07294ff19223
name: M2131_AssetStoppedLogging
description: |
  'This alert is designed to monitor assets within the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when a monitored asset fails to provide a heartbeat within 24 hours.'
severity: Medium
status: Available 
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  let LastHeartbeatTime = Heartbeat
  | summarize LastHeartbeat_Time = arg_max(TimeGenerated, *) by ResourceId;
  Heartbeat
  | where TimeGenerated > ago(14d)
  | summarize LastHeartbeat_Hours = datetime_diff("hour",now(), max(TimeGenerated)) by ResourceId
  | where ResourceId <> ""
  | where ResourceId <> "None"
  | join kind=inner (LastHeartbeatTime) on ResourceId
  | where LastHeartbeat_Hours > 24
  | project LastHeartbeat_Hours, LastHeartbeat_Time, Computer, ComputerIP, Category, OSType, OSName, ResourceId, SubscriptionId, ResourceGroup, RemoteIPCountry 
  | sort by LastHeartbeat_Hours desc
entityMappings:
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Let Binding: LastHeartbeatTime used in Stage 6

let LastHeartbeatTime = Heartbeat
| summarize LastHeartbeat_Time = arg_max(TimeGenerated, *) by ResourceId;

Stage 1: source

Heartbeat

Stage 2: where

| where TimeGenerated > ago(14d)

Stage 3: summarize

| summarize LastHeartbeat_Hours = datetime_diff("hour",now(), max(TimeGenerated)) by ResourceId

Stage 4: where

| where ResourceId <> ""

Stage 5: where

| where ResourceId <> "None"

Stage 6: join

| join kind=inner (LastHeartbeatTime) on ResourceId

Stage 7: where

where LastHeartbeat_Hours > 24

Stage 8: project

project Category, Computer, ComputerIP, LastHeartbeat_Hours, LastHeartbeat_Time, OSName, OSType, RemoteIPCountry, ResourceGroup, ResourceId, SubscriptionId

Stage 9: sort

sort by LastHeartbeat_Hours

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Computerproject
ComputerIPproject
LastHeartbeat_Hoursproject
LastHeartbeat_Timeproject
OSNameproject
OSTypeproject
RemoteIPCountryproject
ResourceGroupproject
ResourceIdproject
SubscriptionIdproject

Microsoft Entra ID Local Device Join Information and Transport Key Registry Keys Access

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

This detection uses Windows security events to detect suspicious access attempts by the same process to registry keys that provide information about an Microsoft Entra ID joined or registered devices and Transport keys (tkpub / tkpriv). This information can be used to export the Device Certificate (dkpub / dkpriv) and Transport key (tkpub/tkpriv). These set of keys can be used to impersonate existing Microsoft Entra ID joined devices. This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable objects: HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin (Microsoft Entra ID joined devices) HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin (Microsoft Entra ID registered devices) HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey (Transport Key) Make sure you set the SACL to propagate to its sub-keys. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_service_agent.yml Reference: https://aadinternals.com/post/deviceidentity/

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rule Body

id: a356c8bd-c81d-428b-aa36-83be706be034
name: Microsoft Entra ID Local Device Join Information and Transport Key Registry Keys Access 
description: |
  'This detection uses Windows security events to detect suspicious access attempts by the same process to registry keys that provide information about an Microsoft Entra ID joined or registered devices and Transport keys (tkpub / tkpriv).
   This information can be used to export the Device Certificate (dkpub / dkpriv) and Transport key (tkpub/tkpriv).
   These set of keys can be used to impersonate existing Microsoft Entra ID joined devices.
   This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable objects:
   HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin (Microsoft Entra ID joined devices)
   HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin (Microsoft Entra ID registered devices)
   HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey (Transport Key)
   Make sure you set the SACL to propagate to its sub-keys. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_service_agent.yml
   Reference: https://aadinternals.com/post/deviceidentity/'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Discovery
relevantTechniques:
  - T1012
tags:
  - SimuLand
  - ATR
  - AADInternals
query: |
  // AADJoined or Register Device Registry Keys
  let aadJoinRoot = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\CloudDomainJoin\\JoinInfo\\";
  let aadRegisteredRoot = "\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WorkplaceJoin";
  // Transport Key Registry Key
  let keyTransportKey = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\Cryptography\\Ngc\\KeyTransportKey\\";
  (union isfuzzy=true
  (
  // Access to Object Requested
  SecurityEvent
  | where EventID == '4656'
  | where EventData has aadJoinRoot or EventData has aadRegisteredRoot
  | extend EventData = parse_xml(EventData).EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
  | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
  | where ObjectType == 'Key'
  | where ObjectName startswith aadJoinRoot and SubjectLogonId != '0x3e7' //Local System
  | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  | join kind=innerunique (
      SecurityEvent
      | where EventID == '4656'
      | where EventData has keyTransportKey
      | extend EventData = parse_xml(EventData).EventData.Data
      | mv-expand bagexpansion=array EventData
      | evaluate bag_unpack(EventData)
      | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
      | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
      | extend ObjectName = column_ifexists("ObjectName", ""),ObjectType = column_ifexists("ObjectType", "")
      | where ObjectType == 'Key'
      | where ObjectName startswith keyTransportKey and SubjectLogonId != '0x3e7' //Local System
      | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  ) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
  | project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, tostring(Process), ProcessName, ProcessId, EventID
  ),
  // Accessing Object
  (
  SecurityEvent
  | where EventID == '4663'
  | where ObjectType == 'Key'
  | where (ObjectName startswith aadJoinRoot or ObjectName contains aadRegisteredRoot) and SubjectLogonId != '0x3e7' //Local System
  | extend Account = SubjectAccount
  | join kind=innerunique (
      SecurityEvent
      | where EventID == '4663'
      | where ObjectType == 'Key'
      | where ObjectName has keyTransportKey and SubjectLogonId != '0x3e7' //Local System
      | extend Account = SubjectAccount
  ) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
  | project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, Process, ProcessName, ProcessId, EventID
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
  )
  )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let aadJoinRoot = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\CloudDomainJoin\\JoinInfo\\";
let aadRegisteredRoot = "\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WorkplaceJoin";
let keyTransportKey = "\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\Cryptography\\Ngc\\KeyTransportKey\\";

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, SecurityEvent

Leg 1: SecurityEvent

SecurityEvent
| where EventID == '4656'
| where EventData has aadJoinRoot or EventData has aadRegisteredRoot
| extend EventData = parse_xml(EventData).EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
| where ObjectType == 'Key'
| where ObjectName startswith aadJoinRoot and SubjectLogonId != '0x3e7'
| extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
| join kind=innerunique (
    SecurityEvent
    | where EventID == '4656'
    | where EventData has keyTransportKey
    | extend EventData = parse_xml(EventData).EventData.Data
    | mv-expand bagexpansion=array EventData
    | evaluate bag_unpack(EventData)
    | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
    | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
    | extend ObjectName = column_ifexists("ObjectName", ""),ObjectType = column_ifexists("ObjectType", "")
    | where ObjectType == 'Key'
    | where ObjectName startswith keyTransportKey and SubjectLogonId != '0x3e7'
    | extend ProcessId = column_ifexists("ProcessId", ""), Process = split(ProcessName, '\\', -1)[-1],Account = strcat(SubjectDomainName, "\\", SubjectUserName)
) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
| project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, tostring(Process), ProcessName, ProcessId, EventID

Leg 2: SecurityEvent

SecurityEvent
| where EventID == '4663'
| where ObjectType == 'Key'
| where (ObjectName startswith aadJoinRoot or ObjectName contains aadRegisteredRoot) and SubjectLogonId != '0x3e7'
| extend Account = SubjectAccount
| join kind=innerunique (
    SecurityEvent
    | where EventID == '4663'
    | where ObjectType == 'Key'
    | where ObjectName has keyTransportKey and SubjectLogonId != '0x3e7'
    | extend Account = SubjectAccount
) on $left.Computer == $right.Computer and $left.SubjectLogonId == $right.SubjectLogonId and $left.ProcessId == $right.ProcessId
| project TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, SubjectLogonId, ObjectName, Process, ProcessName, ProcessId, EventID
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDatamatch
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\CloudDomainJoin\JoinInfo\ transforms: term
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\ transforms: term
  • \SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin transforms: term
field:"EventData" kind:match
EventIDeq
  • 4656 corpus 4 (kusto 4)
  • 4663 corpus 3 (kusto 3)
field:"EventID" kind:eq
ObjectNamecontains
  • \SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin
field:"ObjectName" kind:contains value:"\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin"
ObjectNamematch
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\ transforms: term
field:"ObjectName" kind:match value:"\REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\"
ObjectNamestarts_with
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\CloudDomainJoin\JoinInfo\
  • \REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\
field:"ObjectName" kind:starts_with
ObjectTypeeq
  • Key corpus 4 (kusto 4)
field:"ObjectType" kind:eq value:"Key"
SubjectLogonIdne
  • 0x3e7
  • 0x3e7
field:"LogonId" kind:ne

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
Computerproject
EventIDproject
ObjectNameproject
Processproject
ProcessIdproject
ProcessNameproject
SubjectDomainNameproject
SubjectLogonIdproject
SubjectUserNameproject
TimeGeneratedproject
DnsDomainextend
HostNameextend

Netskope - Data Movement Tracking (Upload/Download Monitoring)

#
Status
available
Severity
informational
Time window
1h
Group by
CsUsername, XCCountry, XCDevice
Source
github.com/Azure/Azure-Sentinel

Tracks file uploads and downloads, monitoring data movement direction, size, and destination. Provides visibility into data flow patterns.

MITRE ATT&CK Coverage

TacticTechniques
Collection
Exfiltration

Rule Body

id: cf103180-cb81-4796-921d-3cc7eef4e817
name: Netskope - Data Movement Tracking (Upload/Download Monitoring)
description: |
  Tracks file uploads and downloads, monitoring data movement direction, size, and destination. Provides visibility into data flow patterns.
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: NetskopeWebTxConnector
    dataTypes:
      - NetskopeWebTransactions_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - Collection
relevantTechniques:
  - T1567
  - T1074
query: |
  let significantSizeMB = 50;
  NetskopeWebTransactions_CL
  | where TimeGenerated > ago(1h)
  | where isnotempty(CsUsername)
  | where CsBytes > 0 or ScBytes > 0 or XRsFileSize > 0
  | extend 
      Direction = case(
          CsBytes > ScBytes, 'Upload',
          ScBytes > CsBytes, 'Download',
          'Unknown'),
      TransferSize = max_of(CsBytes, ScBytes, XRsFileSize)
  | summarize 
      TotalUploadBytes = sumif(CsBytes, Direction == 'Upload'),
      TotalDownloadBytes = sumif(ScBytes, Direction == 'Download'),
      UploadCount = countif(Direction == 'Upload'),
      DownloadCount = countif(Direction == 'Download'),
      UniqueFiles = dcount(XCsAppObjectName),
      Files = make_set(XCsAppObjectName, 20),
      FileTypes = make_set(XRsFileType),
      Apps = make_set(XCsApp),
      Destinations = make_set(CsHost, 20)
      by CsUsername, XCDevice, XCCountry, bin(TimeGenerated, 1h)
  | extend 
      TotalUploadMB = round(TotalUploadBytes / 1048576.0, 2),
      TotalDownloadMB = round(TotalDownloadBytes / 1048576.0, 2),
      TotalTransferMB = round((TotalUploadBytes + TotalDownloadBytes) / 1048576.0, 2)
  | where TotalTransferMB > significantSizeMB
  | extend DataFlowSummary = strcat('Upload: ', TotalUploadMB, ' MB (', UploadCount, ' ops), Download: ', TotalDownloadMB, ' MB (', DownloadCount, ' ops)')
  | project 
      TimeGenerated,
      User = CsUsername,
      Device = XCDevice,
      Country = XCCountry,
      TotalUploadMB,
      TotalDownloadMB,
      TotalTransferMB,
      UploadOperations = UploadCount,
      DownloadOperations = DownloadCount,
      UniqueFiles,
      FilesSample = Files,
      FileTypes,
      Applications = Apps,
      Destinations,
      DataFlowSummary
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: User
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let significantSizeMB = 50;

Stage 1: source

NetskopeWebTransactions_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where isnotempty(CsUsername)

Stage 4: where

| where CsBytes > 0 or ScBytes > 0 or XRsFileSize > 0

Stage 5: extend

| extend 
    Direction = case(
        CsBytes > ScBytes, 'Upload',
        ScBytes > CsBytes, 'Download',
        'Unknown'),
    TransferSize = max_of(CsBytes, ScBytes, XRsFileSize)
Direction =
ifCsBytes > ScBytes'Upload'
elifScBytes > CsBytes'Download'
else'Unknown'

Stage 6: summarize

| summarize 
    TotalUploadBytes = sumif(CsBytes, Direction == 'Upload'),
    TotalDownloadBytes = sumif(ScBytes, Direction == 'Download'),
    UploadCount = countif(Direction == 'Upload'),
    DownloadCount = countif(Direction == 'Download'),
    UniqueFiles = dcount(XCsAppObjectName),
    Files = make_set(XCsAppObjectName, 20),
    FileTypes = make_set(XRsFileType),
    Apps = make_set(XCsApp),
    Destinations = make_set(CsHost, 20)
    by CsUsername, XCDevice, XCCountry, bin(TimeGenerated, 1h)

Stage 7: extend

| extend 
    TotalUploadMB = round(TotalUploadBytes / 1048576.0, 2),
    TotalDownloadMB = round(TotalDownloadBytes / 1048576.0, 2),
    TotalTransferMB = round((TotalUploadBytes + TotalDownloadBytes) / 1048576.0, 2)

Stage 8: where

| where TotalTransferMB > significantSizeMB

Stage 9: extend

| extend DataFlowSummary = strcat('Upload: ', TotalUploadMB, ' MB (', UploadCount, ' ops), Download: ', TotalDownloadMB, ' MB (', DownloadCount, ' ops)')

Stage 10: project

| project 
    TimeGenerated,
    User = CsUsername,
    Device = XCDevice,
    Country = XCCountry,
    TotalUploadMB,
    TotalDownloadMB,
    TotalTransferMB,
    UploadOperations = UploadCount,
    DownloadOperations = DownloadCount,
    UniqueFiles,
    FilesSample = Files,
    FileTypes,
    Applications = Apps,
    Destinations,
    DataFlowSummary

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Applicationsproject
Countryproject
DataFlowSummaryproject
Destinationsproject
Deviceproject
DownloadOperationsproject
FileTypesproject
FilesSampleproject
TimeGeneratedproject
TotalDownloadMBproject
TotalTransferMBproject
TotalUploadMBproject
UniqueFilesproject
UploadOperationsproject
Userproject

Non-admin guest

#
Status
available
Severity
low
Time window
6h
Source
github.com/Azure/Azure-Sentinel

'This query searches for guest is not an admin in Azure'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 9B6558C4-BA23-40AC-B95F-42F8A29A3B35
name: Non-admin guest
description: |
  'This query searches for guest is not an admin in Azure'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: SenservaPro
    dataTypes:
      - SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  SenservaPro_CL
  | where ControlName_s == 'UserNonAdminGuest'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ControlName_s
      - identifier: AadTenantId
        columnName: TenantId
      - identifier: DisplayName
        columnName: TenantDisplayName_s 
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: Group_s
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourceSystem
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SenservaPro_CL

Stage 2: where

| where ControlName_s == 'UserNonAdminGuest'

Indicators

These rows show field, operator, and value matches.

Ping Federate - New user SSO success login

#
Status
available
Severity
low
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects new user SSO success login.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence

Rule Body

id: 05282c91-7aaf-4d76-9a19-6dc582e6a411
name: Ping Federate - New user SSO success login
description: |
  'Detects new user SSO success login.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1136
query: |
  let known_usrs = 
  PingFederateEvent
  | where TimeGenerated between (ago(14d) .. (1d))
  | where isnotempty(DstUserName)
  | summarize makeset(DstUserName);
  PingFederateEvent
  | where EventType =~ 'SSO'
  | where EventMessage has 'success'
  | where DstUserName !in (known_usrs)
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: known_usrs used in Stage 4

let known_usrs = PingFederateEvent
| where TimeGenerated between (ago(14d) .. (1d))
| where isnotempty(DstUserName)
| summarize makeset(DstUserName);

Stage 1: source

PingFederateEvent

Stage 2: where

| where EventType =~ 'SSO'

Stage 3: where

| where EventMessage has 'success'

Stage 4: where

| where DstUserName !in (known_usrs)

Stage 5: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstUserNameeqknown_usrsexcludes:DstUserName field:"DstUserName" value:"known_usrs"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Possible Phishing with CSL and Network Sessions

#
Status
available
Severity
medium
Time window
1d
Group by
$left.DestinationIP, $left.RecipientObjectId, $left.RemoteUrl, $right.AccountObjectId, $right.DstIpAddr, $right.RequestURL, AlertId, DestinationIP, DeviceAction, DeviceName, DeviceProduct, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessAccountUpn, InitiatingProcessFileName, NetworkMessageId, RecipientEmailAddress, RequestClientApplication, SourceIP, TimeGenerated, UrlClickedByUserSid
Source
github.com/Azure/Azure-Sentinel

This query looks for malicious URL clicks in phishing email recognized by MDO in correlation with CommonSecurityLogs(CSL) & NetworkSession events. If your workspace doesnt have one of the many data sources required for ASIM it may give informational error which can be safely ignored.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Command & Control

Telemetry Coverage

Rule Body

id: 6c3a1258-bcdd-4fcd-b753-1a9bc826ce12
name: Possible Phishing with CSL and Network Sessions
description: |
    'This query looks for malicious URL clicks in phishing email recognized by MDO in correlation with CommonSecurityLogs(CSL) & NetworkSession events. 
    If your workspace doesnt have one of the many data sources required for ASIM it may give informational error which can be safely ignored.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
  dataTypes:
  - AlertEvidence
  - EmailEvents
  - IdentityInfo
  - DeviceEvents
  - DeviceNetworkEvents
- connectorId: Zscaler
  dataTypes:
  - CommonSecurityLog
- connectorId: Fortinet
  dataTypes:
  - CommonSecurityLog
- connectorId: CheckPoint
  dataTypes:
  - CommonSecurityLog
- connectorId: PaloAltoNetworks
  dataTypes:
  - CommonSecurityLog
- connectorId: AWSS3
  datatypes:
  - AWSVPCFlow
- connectorId: WindowsForwardedEvents
  dataTypes:
  - WindowsEvent
- connectorId: SecurityEvents
  dataTypes:
  - SecurityEvent
- connectorId: WindowsSecurityEvents
  dataTypes:
  - SecurityEvent
- connectorId: MicrosoftSysmonForLinux
  dataTypes:
  - Syslog
- connectorId: AzureNSG
  dataTypes:
  - AzureDiagnostics
- connectorId: AzureMonitor(VMInsights)
  dataTypes:
  - VMConnection
- connectorId: AIVectraStream
  dataTypes:
  - VectraStream_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
  - T1566
  - T1102
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.5
query: |
    //SuspiciousUrlClicked
    AlertEvidence
    | where ServiceSource =~ "Microsoft Defender for Office 365"
    | where EntityType =~ "Url"
    | project AlertId, RemoteUrl
    | join kind=inner (
    AlertEvidence
    | where ServiceSource =~ "Microsoft Defender for Office 365"
    | where EntityType =~ "MailMessage"
    | project AlertId, NetworkMessageId
    )
    on AlertId
    | distinct RemoteUrl, NetworkMessageId
    | join EmailEvents on NetworkMessageId
    | distinct RemoteUrl, NetworkMessageId, RecipientEmailAddress, RecipientObjectId
    | join kind = inner IdentityInfo on $left.RecipientObjectId  == $right.AccountObjectId
    | distinct RemoteUrl, NetworkMessageId, RecipientEmailAddress , RecipientObjectId, AccountSID
    | join kind = inner  
    (DeviceEvents
    | where ActionType =~ "BrowserLaunchedToOpenUrl"| where isnotempty(RemoteUrl)
    | project  UrlClickedByUserSid = RemoteUrl,
    InitiatingProcessAccountSid, DeviceName, DeviceId, InitiatingProcessFileName,
    InitiatingProcessAccountUpn, InitiatingProcessAccountName, InitiatingProcessAccountDomain
    )
    on $left.AccountSID == $right.InitiatingProcessAccountSid and $left.RemoteUrl == $right.UrlClickedByUserSid
    | distinct  RemoteUrl, NetworkMessageId, RecipientEmailAddress, RecipientObjectId,
     AccountSID, UrlClickedByUserSid, DeviceName, DeviceId, InitiatingProcessFileName,
     InitiatingProcessAccountUpn, InitiatingProcessAccountName, InitiatingProcessAccountDomain
    |  join kind=inner
    (
    //Suspicious url clicked found in common security logs
    CommonSecurityLog
    | project TimeGenerated, DeviceVendor, DeviceProduct, DeviceAction, DestinationDnsDomain, DestinationIP, RequestURL, SourceIP, SourceHostName, RequestClientApplication
    ) on $left.RemoteUrl== $right.RequestURL
    |  join kind=inner
    (
    //Find the relevant network sessions
    _Im_NetworkSession
    | where isnotempty(DstIpAddr)
    | where not(ipv4_is_private(DstIpAddr))
    | project TimeGenerated, SrcIpAddr, SrcPortNumber, DstIpAddr, DstPortNumber, DstBytes, SrcBytes
    ) on  $left.DestinationIP == $right.DstIpAddr //The relevant network session being projected 
    | summarize count() by TimeGenerated, RecipientEmailAddress, UrlClickedByUserSid, InitiatingProcessAccountUpn, InitiatingProcessAccountName, InitiatingProcessAccountDomain,
    DeviceName, InitiatingProcessFileName, DeviceProduct, DeviceAction, SourceIP, DestinationIP, RequestClientApplication
    | extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
    | extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
    | extend RecipientEmailName = tostring(split(RecipientEmailAddress,'@',0)[0]), RecipientEmailUPNSuffix = tostring(split(RecipientEmailAddress,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName 
        columnName: InitiatingProcessAccountUpn
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: UPNSuffix
        columnName: InitiatingProcessAccountDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName 
        columnName: RecipientEmailAddress
      - identifier: Name
        columnName: RecipientEmailName
      - identifier: UPNSuffix
        columnName: RecipientEmailUPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName 
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
version: 1.1.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AlertEvidence

Stage 2: where

| where ServiceSource =~ "Microsoft Defender for Office 365"

Stage 3: where

| where EntityType =~ "Url"

Stage 4: project

| project AlertId, RemoteUrl

Stage 5: join

| join kind=inner (
AlertEvidence
| where ServiceSource =~ "Microsoft Defender for Office 365"
| where EntityType =~ "MailMessage"
| project AlertId, NetworkMessageId
)
on AlertId

Stage 6: distinct

distinct NetworkMessageId, RemoteUrl

Stage 7: join

| join EmailEvents on NetworkMessageId

Stage 8: distinct

distinct NetworkMessageId, RecipientEmailAddress, RecipientObjectId, RemoteUrl

Stage 9: join

| join kind = inner IdentityInfo on $left.RecipientObjectId  == $right.AccountObjectId

Stage 10: distinct

distinct AccountSID, NetworkMessageId, RecipientEmailAddress, RecipientObjectId, RemoteUrl

Stage 11: join

| join kind = inner  
(DeviceEvents
| where ActionType =~ "BrowserLaunchedToOpenUrl"| where isnotempty(RemoteUrl)
| project  UrlClickedByUserSid = RemoteUrl,
InitiatingProcessAccountSid, DeviceName, DeviceId, InitiatingProcessFileName,
InitiatingProcessAccountUpn, InitiatingProcessAccountName, InitiatingProcessAccountDomain
)
on $left.AccountSID == $right.InitiatingProcessAccountSid and $left.RemoteUrl == $right.UrlClickedByUserSid

Stage 12: distinct

distinct AccountSID, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessAccountUpn, InitiatingProcessFileName, NetworkMessageId, RecipientEmailAddress, RecipientObjectId, RemoteUrl, UrlClickedByUserSid

Stage 13: join

| join kind=inner
(
CommonSecurityLog
| project TimeGenerated, DeviceVendor, DeviceProduct, DeviceAction, DestinationDnsDomain, DestinationIP, RequestURL, SourceIP, SourceHostName, RequestClientApplication
) on $left.RemoteUrl== $right.RequestURL

Stage 14: join

| join kind=inner
(
_Im_NetworkSession
| where isnotempty(DstIpAddr)
| where not(ipv4_is_private(DstIpAddr))
| project TimeGenerated, SrcIpAddr, SrcPortNumber, DstIpAddr, DstPortNumber, DstBytes, SrcBytes
) on  $left.DestinationIP == $right.DstIpAddr

Stage 15: summarize

summarize by TimeGenerated, RecipientEmailAddress, UrlClickedByUserSid, InitiatingProcessAccountUpn, InitiatingProcessAccountName, InitiatingProcessAccountDomain, DeviceName, InitiatingProcessFileName, DeviceProduct, DeviceAction, SourceIP, DestinationIP, RequestClientApplication

Stage 16: extend (3 consecutive steps)

extend DomainIndex, HostName, HostNameDomain, RecipientEmailName, RecipientEmailUPNSuffix

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DstIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationIPsummarize
DeviceActionsummarize
DeviceNamesummarize
DeviceProductsummarize
InitiatingProcessAccountDomainsummarize
InitiatingProcessAccountNamesummarize
InitiatingProcessAccountUpnsummarize
InitiatingProcessFileNamesummarize
RecipientEmailAddresssummarize
RequestClientApplicationsummarize
SourceIPsummarize
TimeGeneratedsummarize
UrlClickedByUserSidsummarize
DomainIndexextend
HostNameextend
HostNameDomainextend
RecipientEmailNameextend
RecipientEmailUPNSuffixextend

Potential Password Spray Attack

#
Status
available
Severity
medium
Time window
5m
Group by
City, Country, TimeGenerated, client_ipAddress_s
Source
github.com/Azure/Azure-Sentinel

This query searches for failed attempts to log into the Okta console from more than 15 various users within a 5 minute timeframe from the same source. This is a potential indication of a password spray attack.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: e27dd7e5-4367-4c40-a2b7-fcd7e7a8a508
name: Potential Password Spray Attack
description: |
  This query searches for failed attempts to log into the Okta console from more than 15 various users within a 5 minute timeframe from the same source. This is a potential indication of a password spray attack.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: OktaSSO
    dataTypes:
      - Okta_CL
  - connectorId: OktaSSOv2
    dataTypes:
      - OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110.003
query: |
  // Adjust threshold for failed logins to reduce noise
  let FailureThreshold = 15;
  let FailedEvents = OktaSSO
  | where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
  | summarize dcount(actor_alternateId_s) by client_ipAddress_s, bin(TimeGenerated, 5m)
  | where dcount_actor_alternateId_s > FailureThreshold
  | project client_ipAddress_s, TimeGenerated;
  OktaSSO
  | where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
  | summarize Users = make_set(actor_alternateId_s) by client_ipAddress_s, City = column_ifexists('client_geographicalContext_city_s', ""), Country = column_ifexists('client_geographicalContext_country_s', ""), bin(TimeGenerated, 5m)
  | join kind=inner (FailedEvents) on client_ipAddress_s, TimeGenerated
  | project TimeGenerated, client_ipAddress_s, Users, DistinctUsers = array_length(Users), City, Country
  | sort by TimeGenerated desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: client_ipAddress_s
customDetails:
  Users: Users
  City: City
  Country: Country
alertDetailsOverride:
  alertDisplayNameFormat: 'Potential password spray attack from IP {{client_ipAddress_s}}'
  alertDescriptionFormat: '{{DistinctUsers}} distinct users failed authentication from {{client_ipAddress_s}} in a 5 minute window.'
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let FailureThreshold = 15;

Let Binding: FailedEvents used in Stage 4

let FailedEvents = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")
| summarize dcount(actor_alternateId_s) by client_ipAddress_s, bin(TimeGenerated, 5m)
| where dcount_actor_alternateId_s > FailureThreshold
| project client_ipAddress_s, TimeGenerated;

Stage 1: source

OktaSSO

Stage 2: where

| where eventType_s =~ "user.session.start" and outcome_reason_s in ("VERIFICATION_ERROR","INVALID_CREDENTIALS")

Stage 3: summarize

| summarize Users = make_set(actor_alternateId_s) by client_ipAddress_s, City = column_ifexists('client_geographicalContext_city_s', ""), Country = column_ifexists('client_geographicalContext_country_s', ""), bin(TimeGenerated, 5m)

Stage 4: join

| join kind=inner (FailedEvents) on client_ipAddress_s, TimeGenerated

Stage 5: project

project City, Country, DistinctUsers, TimeGenerated, Users, client_ipAddress_s

Stage 6: sort

sort by TimeGenerated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
dcount_actor_alternateId_sgt
  • 15 transforms: number
field:"dcount_actor_alternateId_s" kind:gt value:"15"
eventType_seq
  • user.session.start corpus 4 (kusto 3, elastic 1)
  • user.session.start
field:"okta::eventType" kind:eq
outcome_reason_sin
  • INVALID_CREDENTIALS corpus 6 (elastic 5, kusto 1)
  • INVALID_CREDENTIALS
  • VERIFICATION_ERROR
  • VERIFICATION_ERROR
field:"okta::outcome.reason" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Cityproject
Countryproject
DistinctUsersproject
TimeGeneratedproject
Usersproject
client_ipAddress_sproject

Process-Level Anomaly

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
medium
Time window
1d
Group by
application
Source
github.com/Azure/Azure-Sentinel

'Triggers an incident when the same process name appears in 50 or more alerts across multiple devices, suggesting widespread activity.'

MITRE ATT&CK Coverage

Rule Body

id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name: Process-Level Anomaly
description: |
  'Triggers an incident when the same process name appears in 50 or more alerts across multiple devices, suggesting widespread activity.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MorphisecCCF
    dataTypes:
      - Morphisec
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
  - Execution
  - DefenseEvasion
relevantTechniques:
  - T1059
  - T1204
query: |
  MorphisecAlerts_CL
  | where threatMessageArrivalTime >= ago(24h)
  | summarize 
      AlertCount = dcount(id),
      DeviceCount = dcount(hostname)
      by application
  | where AlertCount >= 50 and DeviceCount > 1
customDetails:
  ProcessName: application
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AllEntities
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

MorphisecAlerts_CL

Stage 2: where

| where threatMessageArrivalTime >= ago(24h)

Stage 3: summarize

| summarize 
    AlertCount = dcount(id),
    DeviceCount = dcount(hostname)
    by application

Stage 4: where

| where AlertCount >= 50 and DeviceCount > 1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertCountge
  • 50 transforms: number
field:"AlertCount" kind:ge value:"50"
DeviceCountgt
  • 1 transforms: number
field:"DeviceCount" kind:gt value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertCountsummarize
DeviceCountsummarize
applicationsummarize

Radiflow - Suspicious Malicious Activity Detected

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

'Generates an incident when malware is detected by Radiflow's iSID.'

MITRE ATT&CK Coverage

TacticTechniques
Evasion
Inhibit Response Function

Rule Body

id: ecac26b8-147d-478a-9d50-99be4bf14019
name: Radiflow - Suspicious Malicious Activity Detected
description: |
  'Generates an incident when malware is detected by Radiflow's iSID.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: RadiflowIsid
    dataTypes:
      - RadiflowEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- InhibitResponseFunction
relevantTechniques:
- T0851
query: |
  RadiflowEvent
  | where DeviceProduct =~ 'iSID'
  | where EventClassID in (57, 58, 59, 60, 176, 178, 181)
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Suspicious Malicious Activity Detected: {{EventMessage}}'
  alertDescriptionFormat: |-
    A possible malware has been detected in the network as specified in the details of the incident. Please verify the source of this activity.

    Message: {{EventMessage}}
    Source device: {{{SourceIP}} 
    Destination device (if any): {{DestinationIP}}
  alertSeverityColumnName: EventSeverity
  alertDynamicProperties: []
suppressionEnabled: false
suppressionDuration: 5h
customDetails:
  SourceMAC: SourceMACAddress
  SourceType: SourceType
  SourceVendor: SourceVendor
  SourceVLAN: SourceVLAN
  DestinationMAC: DestinationMACAddress
  DestinationType: DestinationType
  DestinationVendor: DestinationVendor
  Port: Port
  Protocol: Protocol
  SourceIP: SourceIP
  DestinationIP: DestinationIP
  SourceHostName: SourceHostName
  DestinationHostName: DestinationHostName
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: SourceHostName
  - identifier: NetBiosName
    columnName: SourceHostName
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: DestinationHostName
  - identifier: NetBiosName
    columnName: DestinationHostName
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SourceIP
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: DestinationIP
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

RadiflowEvent

Stage 2: where

| where DeviceProduct =~ 'iSID'

Stage 3: where

| where EventClassID in (57, 58, 59, 60, 176, 178, 181)

Indicators

These rows show field, operator, and value matches.

Samsung Knox - Password Lockout Events

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
high
Source
github.com/Azure/Azure-Sentinel

When maximum password attempts have been reached, and the Knox device is locked out. This is based on a threshold set in the MDM device policy

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: fbff0a97-1972-4df8-a78c-254ccb9879ef
name: Samsung Knox - Password Lockout Events
version: 1.0.2
kind: NRT
description: When maximum password attempts have been reached, and the Knox device is locked out.
  This is based on a threshold set in the MDM device policy
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SamsungDCDefinition
    dataTypes:
      - Samsung_Knox_User_CL
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  Samsung_Knox_User_CL 
  | where Name == "PASSWORD_LOCKOUT"
  and MitreTtp has "T1110"
suppressionEnabled: false
suppressionDuration: PT5H
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert

Stages and Predicates

Stage 1: source

Samsung_Knox_User_CL

Stage 2: where

| where Name == "PASSWORD_LOCKOUT"
and MitreTtp has "T1110"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MitreTtpmatch
  • T1110 transforms: term
field:"MitreTtp" kind:match value:"T1110"
Nameeq
  • PASSWORD_LOCKOUT
field:"Name" kind:eq value:"PASSWORD_LOCKOUT"

TI Map IP Entity to SigninLogs

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.IPAddress, IPAddress, Id, ObservableValue
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: edfc9d8a-6fb3-49e2-80c9-fea15d941799
name: TI Map IP Entity to SigninLogs
description: |
  'This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let Signins = materialize(union isfuzzy=true
    (SigninLogs
    | where TimeGenerated >= ago(dt_lookBack)),
    (AADNonInteractiveUserSignInLogs
    | where TimeGenerated >= ago(dt_lookBack)
    | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)));
  let SigninIPs = Signins | summarize make_list(IPAddress);
  let TI = materialize(ThreatIntelIndicators
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
       | extend NetworkSourceIP = toupper(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = NetworkSourceIP
    | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
    | where TI_ipEntity in (SigninIPs)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | extend Description = tostring(parse_json(Data).description)
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
  TI
     | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (Signins) on $left.TI_ipEntity == $right.IPAddress
  | project-rename SigninLogs_TimeGenerated = TimeGenerated
  | where SigninLogs_TimeGenerated < ValidUntil
  | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), StatusReason = tostring(Status.failureReason)
  | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by Id, IPAddress
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project SigninLogs_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, TI_ipEntity, IPAddress, UserPrincipalName, AppDisplayName, StatusCode, StatusDetails, StatusReason, NetworkSourceIP, Type, Url
  | extend timestamp = SigninLogs_TimeGenerated, Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.3.2
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let TI is inlined into the numbered stages below.

Let Binding: Signins used in Stage 15

let Signins = materialize(union isfuzzy=true
  (SigninLogs
  | where TimeGenerated >= ago(dt_lookBack)),
  (AADNonInteractiveUserSignInLogs
  | where TimeGenerated >= ago(dt_lookBack)
  | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)));

Let Binding: SigninIPs used in Stage 9

let SigninIPs = Signins | summarize make_list(IPAddress);

Stages 1 to 13 define let TI as the rule's main pipeline source. Stages 14 to 23 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")

Stage 4: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend

| extend TI_ipEntity = NetworkSourceIP

Stage 8: extend

| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
Url =
ifObservableKey == "url:value"ObservableValue
else""

Stage 9: where

| where TI_ipEntity in (SigninIPs)

Stage 10: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 11: extend

| extend Description = tostring(parse_json(Data).description)

Stage 12: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 13: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 14: project-reorder

TI
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity

Stage 15: join

join kind=innerunique (Signins) on $left.TI_ipEntity, $right.IPAddress

Stage 16: project-rename

project-rename

Stage 17: where

where SigninLogs_TimeGenerated < ValidUntil

Stage 18: extend

extend StatusCode, StatusDetails, StatusReason

Stage 19: summarize

summarize SigninLogs_TimeGenerated by Id, IPAddress

Stage 20: extend

extend Description

Stage 21: extend

extend ActivityGroupNames

Stage 22: project

project ActivityGroupNames, AppDisplayName, Confidence, Description, IPAddress, Id, NetworkSourceIP, SigninLogs_TimeGenerated, StatusCode, StatusDetails, StatusReason, TI_ipEntity, Type, Url, UserPrincipalName, ValidUntil

Stage 23: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppDisplayNameproject
Confidenceproject
Descriptionproject
IPAddressproject
Idproject
NetworkSourceIPproject
SigninLogs_TimeGeneratedproject
StatusCodeproject
StatusDetailsproject
StatusReasonproject
TI_ipEntityproject
Typeproject
Urlproject
UserPrincipalNameproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

User account enabled and disabled within 10 mins

#
Severity
medium
Time window
25h
Group by
Computer, TargetAccount
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies when a user account is enabled and then disabled within 10 minutes. This can be an indication of compromise and an adversary attempting to hide in the noise.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 3d023f64-8225-41a2-9570-2bd7c2c4535e
name: User account enabled and disabled within 10 mins
description: |
  'Identifies when a user account is enabled and then disabled within 10 minutes. This can be an indication of compromise and an adversary attempting to hide in the noise.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent  
queryFrequency: 1d
queryPeriod: 25h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T1078
query: |
  let timeframe = 1d;
  let spanoftime = 10m;
  let threshold = 0;
    (union isfuzzy=true
      (SecurityEvent
      | where TimeGenerated > ago(timeframe+spanoftime)
      // A user account was enabled
      | where EventID == 4722
      | where AccountType =~ "User"
      | where TargetAccount !endswith "$"
      | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
      TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
      AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName
      ),
      (
      WindowsEvent
      | where TimeGenerated > ago(timeframe+spanoftime)
      // A user account was enabled
      | where EventID == 4722
      | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
      | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
      | where AccountType =~ "User"
      | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
      | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
      | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
      | where TargetAccount !endswith "$"
      | extend Activity="4722 - A user account was enabled."
      | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
      | extend TargetSid = tostring(EventData.TargetSid)
      | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
      | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
      TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
      AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName
      )
    )
  | join kind= inner (
    (union isfuzzy=true
      (SecurityEvent
      | where TimeGenerated > ago(timeframe)
      // A user account was disabled
      | where EventID == 4725
      | where AccountType =~ "User"
      | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
      TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
      AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
      ),
      (WindowsEvent
      | where TimeGenerated > ago(timeframe)
      // A user account was disabled
      | where EventID == 4725
      | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
      | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
      | where AccountType =~ "User"
      | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
      | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
      | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
      | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
      | extend TargetSid = tostring(EventData.TargetSid)
      | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
      | extend Activity = "4725 - A user account was disabled."
      | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
      TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
      AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
      )
    )
  ) on Computer, TargetAccount
  | where DisableTime - EnableTime < spanoftime
  | extend TimeDelta = DisableTime - EnableTime
  | where tolong(TimeDelta) >= threshold
  | project TimeDelta, EnableTime, EnableEventID, EnableActivity, Computer, TargetAccount, TargetSid, TargetUserName, TargetDomainName, UserPrincipalName, 
  AccountUsedToEnable, SIDofAccountUsedToEnable, DisableTime, DisableEventID, DisableActivity, AccountUsedToDisable, SIDofAccountUsedToDisable, 
  EnabledBySubjectUserName, EnabledBySubjectDomainName, DisabledBySubjectUserName, DisabledBySubjectDomainName
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUsedToEnable
      - identifier: Name
        columnName: EnabledBySubjectUserName
      - identifier: NTDomain
        columnName: EnabledBySubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUsedToDisable
      - identifier: Name
        columnName: DisabledBySubjectUserName
      - identifier: NTDomain
        columnName: DisabledBySubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetAccount
      - identifier: Name
        columnName: TargetUserName
      - identifier: NTDomain
        columnName: TargetDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: TargetSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.2.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let timeframe = 1d;
let spanoftime = 10m;
let threshold = 0;

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent

Leg 1: SecurityEvent

SecurityEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    | where EventID == 4722
    | where AccountType =~ "User"
    | where TargetAccount !endswith "$"
    | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName

Leg 2: WindowsEvent

WindowsEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    | where EventID == 4722
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType =~ "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | where TargetAccount !endswith "$"
    | extend Activity="4722 - A user account was enabled."
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName

Applied to the Combined Result

| join kind= inner (
  (union isfuzzy=true
    (SecurityEvent
    | where TimeGenerated > ago(timeframe)
    | where EventID == 4725
    | where AccountType =~ "User"
    | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
    ),
    (WindowsEvent
    | where TimeGenerated > ago(timeframe)
    | where EventID == 4725
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType =~ "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | extend Activity = "4725 - A user account was disabled."
    | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
    )
  )
) on Computer, TargetAccount
| where DisableTime - EnableTime < spanoftime
| extend TimeDelta = DisableTime - EnableTime
| where tolong(TimeDelta) >= threshold
| project TimeDelta, EnableTime, EnableEventID, EnableActivity, Computer, TargetAccount, TargetSid, TargetUserName, TargetDomainName, UserPrincipalName, 
AccountUsedToEnable, SIDofAccountUsedToEnable, DisableTime, DisableEventID, DisableActivity, AccountUsedToDisable, SIDofAccountUsedToDisable, 
EnabledBySubjectUserName, EnabledBySubjectDomainName, DisabledBySubjectUserName, DisabledBySubjectDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TargetAccountends_with$excludes:TargetAccount field:"TargetAccount" value:"$"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountUsedToDisableproject
AccountUsedToEnableproject
Computerproject
DisableActivityproject
DisableEventIDproject
DisableTimeproject
DisabledBySubjectDomainNameproject
DisabledBySubjectUserNameproject
EnableActivityproject
EnableEventIDproject
EnableTimeproject
EnabledBySubjectDomainNameproject
EnabledBySubjectUserNameproject
SIDofAccountUsedToDisableproject
SIDofAccountUsedToEnableproject
TargetAccountproject
TargetDomainNameproject
TargetSidproject
TargetUserNameproject
TimeDeltaproject
UserPrincipalNameproject
HostNameextend
HostNameDomainextend

Back to all Kusto rules