Detection rules › Kusto
Kusto rules: 1p
Affected rows stateful anomaly on database
#'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
| Tactic | Techniques |
|---|---|
| Impact |
Rules Detecting the Same Action
These rules filter on the same operation.
- Credential errors stateful anomaly on database (Kusto)
- Drop attempts stateful anomaly on database (Kusto)
- Execution attempts stateful anomaly on database (Kusto)
- Firewall errors stateful anomaly on database (Kusto)
- Firewall rule manipulation attempts stateful anomaly on database (Kusto)
- OLE object manipulation attempts stateful anomaly on database (Kusto)
- Outgoing connection attempts stateful anomaly on database (Kusto)
- Response rows stateful anomaly on database (Kusto)
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
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 =TimeGenerated >= ago(3600s)'detection'/* macro: (ago(trainingWindow) <= TimeGenerated) */'training''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 =N >= 20round((todouble((QuantityColumn - P99Val)) / todouble(((P99Val - P50Val) + 1))), 2)0.0ZScoreVal =N >= 20round((todouble((QuantityColumn - AvgVal)) / todouble((StdVal + 1))), 2)0.0Stage 11: extend
extend AnomalyScore, IsVolumeAnomalyOnVal
IsVolumeAnomalyOnVal =(ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ) and QuantityColumn > volumeThresholdHardcodedtruefalseStage 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"SQLSecurityAuditEvents" |
IsVolumeAnomalyOnVal | eq |
| field:"IsVolumeAnomalyOnVal" kind:eq value:"true" |
WindowType | eq |
| field:"WindowType" kind:eq |
WindowType | in |
| field:"WindowType" kind:in |
action_id_s | match |
| field:"action_id_s" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Database | summarize |
Anomalous sign-in location by user account and authenticating application
#'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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Success: Successful sign-in |
| Entra ID | SigninLogs sign-in event: Successful sign-in |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
- Detect non-admin requesting token for admin applications (Kusto)
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.
| Field | Source |
|---|---|
AppDisplayName | summarize |
AppId | summarize |
CountOfLocations | summarize |
SignInDetailsSet | summarize |
TimeGenerated | summarize |
UserId | summarize |
UserPrincipalName | summarize |
Name | extend |
UPNSuffix | extend |
Apache - Private IP in URL
#'Detects requests to unusual URL'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
UrlOriginal | regex_match |
| field:"UrlOriginal" kind:regex_match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UrlCustomEntity | extend |
ApexOne - Possible exploit or execute operation
#'Detects possible exploit or execute operation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceCustomNumber3 | eq |
| field:"DeviceCustomNumber3" kind:eq |
EventMessage | match |
| field:"EventMessage" kind:match value:"Behavior Monitoring" |
Event_Type | eq |
| field:"Event_Type" kind:eq value:"2048" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceCustomNumber3 | extend |
AccountCustomEntity | extend |
IPCustomEntity | extend |
AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
- AWSCloudTrail - Privilege escalation via DataPipeline policy (Kusto)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
AWSCloudTrail - Privilege escalation with AdministratorAccess managed policy
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM CompromisedKeyQuarantine Policy Attached to User (Elastic)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - CloudFormation policy created then used for privilege escalation (Kusto)
- AWSCloudTrail - Created CRUD S3 policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD KMS policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of DataPipeline policy and then privilege escalation (Kusto)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
RequestParameters.policyArn | starts_with |
| field:"RequestParameters.policyArn" kind:starts_with value:"arn:aws:iam::aws:policy/AdministratorAccess" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | project |
AccountName | project |
AccountUPNSuffix | project |
AdditionalEventData | project |
EventName | project |
EventSource | project |
EventTypeName | project |
RecipientAccountId | project |
RequestParameters | project |
ResponseElements | project |
SessionMfaAuthenticated | project |
SourceIpAddress | project |
TimeGenerated | project |
UserAgent | project |
UserIdentityAccountId | project |
UserIdentityArn | project |
UserIdentityPrincipalid | project |
UserIdentityUserName | project |
AWSCloudTrail - S3 bucket exposed via policy
#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
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| AWS | CloudTrail event PutBucketPolicy: Applies a resource-based bucket policy to an S3 bucket. |
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS S3 Bucket Policy Modified (Panther)
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.
| Field | Source |
|---|---|
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Principal | extend |
Principal_aws | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
AWSCloudTrail - SAML update identity provider
#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
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| AWS | CloudTrail event UpdateSAMLProvider: Updates the metadata document for an existing SAML provider in IAM. |
Rules Detecting the Same Action
These rules filter on the same operation.
- ASL AWS SAML Update identity provider (Splunk)
- AWS SAML Update identity provider (Splunk)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | eq |
| field:"aws::eventName" kind:eq value:"UpdateSAMLProvider" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| 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.
'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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Stealth | |
| Command & Control |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
rule | in |
| field:"rule" kind:in |
CreepyDrive URLs
#'CreepyDrive uses OneDrive for command and control. This detection identifies URLs specific to CreepyDrive.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
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
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
RequestURL | match |
| field:"RequestURL" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationDnsDomain | project |
DestinationIP | project |
DeviceAction | project |
DeviceProduct | project |
DeviceVendor | project |
RequestClientApplication | project |
RequestURL | project |
SourceHostName | project |
SourceIP | project |
TimeGenerated | project |
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.
"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
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Resource Development | |
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
Recommendation | project |
RiskScore | project |
SourceSype | project |
TimeGenerated | project |
UID | project |
DopplePaymer Procdump
#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
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
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 =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 4: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | in |
| field:"process_basename" kind:in |
ProcessCommandLine | contains |
| field:"CommandLine" kind:contains value:"-ma" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
DnsDomain | extend |
Failed AWS Console logons but success logon to AzureAD
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
IPAddress | in |
| field:"src_ip" kind:in value:"aws_fails" |
ResultType | in |
| field:"ResultType" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Reason | extend |
AccountName | extend |
AccountUPNSuffix | extend |
timestamp | extend |
Hypervisor Host Settings Updated
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"25800" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Jamf Protect - Network Threats
#'Creates an incident based based on Jamf Protect's Network Threat Event Stream alerts.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResult | eq |
| field:"EventResult" kind:eq value:"Blocked" |
EventSeverity | is_not_null | field:"EventSeverity" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Tactics | extend |
Techniques | extend |
M2131_AssetStoppedLogging
#'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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
LastHeartbeat_Hours | gt |
| field:"LastHeartbeat_Hours" kind:gt value:"24" |
ResourceId | ne |
| field:"ResourceId" kind:ne value:"None" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Computer | project |
ComputerIP | project |
LastHeartbeat_Hours | project |
LastHeartbeat_Time | project |
OSName | project |
OSType | project |
RemoteIPCountry | project |
ResourceGroup | project |
ResourceId | project |
SubscriptionId | project |
Microsoft Entra ID Local Device Join Information and Transport Key Registry Keys Access
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq |
ObjectName | contains |
| field:"ObjectName" kind:contains value:"\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin" |
ObjectName | match |
| field:"ObjectName" kind:match value:"\REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Cryptography\Ngc\KeyTransportKey\" |
ObjectName | starts_with |
| field:"ObjectName" kind:starts_with |
ObjectType | eq |
| field:"ObjectType" kind:eq value:"Key" |
SubjectLogonId | ne |
| field:"LogonId" kind:ne |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
Computer | project |
EventID | project |
ObjectName | project |
Process | project |
ProcessId | project |
ProcessName | project |
SubjectDomainName | project |
SubjectLogonId | project |
SubjectUserName | project |
TimeGenerated | project |
DnsDomain | extend |
HostName | extend |
Netskope - Data Movement Tracking (Upload/Download Monitoring)
#Tracks file uploads and downloads, monitoring data movement direction, size, and destination. Provides visibility into data flow patterns.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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 =CsBytes > ScBytes'Upload'ScBytes > CsBytes'Download''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.
| Field | Source |
|---|---|
Applications | project |
Country | project |
DataFlowSummary | project |
Destinations | project |
Device | project |
DownloadOperations | project |
FileTypes | project |
FilesSample | project |
TimeGenerated | project |
TotalDownloadMB | project |
TotalTransferMB | project |
TotalUploadMB | project |
UniqueFiles | project |
UploadOperations | project |
User | project |
Non-admin guest
#'This query searches for guest is not an admin in Azure'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ControlName_s | eq |
| field:"ControlName_s" kind:eq value:"UserNonAdminGuest" |
Ping Federate - New user SSO success login
#'Detects new user SSO success login.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"success" |
EventType | eq |
| field:"EventType" kind:eq value:"SSO" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstUserName | eq | known_usrs | excludes:DstUserName field:"DstUserName" value:"known_usrs" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Possible Phishing with CSL and Network Sessions
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"BrowserLaunchedToOpenUrl" |
DstIpAddr | is_not_null | field:"DstIpAddr" kind:is_not_null | |
EntityType | eq |
| field:"EntityType" kind:eq |
RemoteUrl | is_not_null | field:"RemoteUrl" kind:is_not_null | |
ServiceSource | eq |
| field:"ServiceSource" kind:eq value:"Microsoft Defender for Office 365" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstIpAddr | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:DstIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationIP | summarize |
DeviceAction | summarize |
DeviceName | summarize |
DeviceProduct | summarize |
InitiatingProcessAccountDomain | summarize |
InitiatingProcessAccountName | summarize |
InitiatingProcessAccountUpn | summarize |
InitiatingProcessFileName | summarize |
RecipientEmailAddress | summarize |
RequestClientApplication | summarize |
SourceIP | summarize |
TimeGenerated | summarize |
UrlClickedByUserSid | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
RecipientEmailName | extend |
RecipientEmailUPNSuffix | extend |
Potential Password Spray Attack
#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
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules Detecting the Same Action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
dcount_actor_alternateId_s | gt |
| field:"dcount_actor_alternateId_s" kind:gt value:"15" |
eventType_s | eq |
| field:"okta::eventType" kind:eq |
outcome_reason_s | in |
| field:"okta::outcome.reason" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
City | project |
Country | project |
DistinctUsers | project |
TimeGenerated | project |
Users | project |
client_ipAddress_s | project |
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.
'Triggers an incident when the same process name appears in 50 or more alerts across multiple devices, suggesting widespread activity.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertCount | ge |
| field:"AlertCount" kind:ge value:"50" |
DeviceCount | gt |
| field:"DeviceCount" kind:gt value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertCount | summarize |
DeviceCount | summarize |
application | summarize |
Radiflow - Suspicious Malicious Activity Detected
#'Generates an incident when malware is detected by Radiflow's iSID.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceProduct | eq |
| field:"DeviceProduct" kind:eq value:"iSID" |
EventClassID | in |
| field:"EventClassID" kind:in |
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.
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
MitreTtp | match |
| field:"MitreTtp" kind:match value:"T1110" |
Name | eq |
| field:"Name" kind:eq value:"PASSWORD_LOCKOUT" |
TI Map IP Entity to SigninLogs
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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
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
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 =ObservableKey == "url:value"ObservableValue""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.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
TI_ipEntity | in |
| field:"TI_ipEntity" kind:in value:"SigninIPs" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AppDisplayName | project |
Confidence | project |
Description | project |
IPAddress | project |
Id | project |
NetworkSourceIP | project |
SigninLogs_TimeGenerated | project |
StatusCode | project |
StatusDetails | project |
StatusReason | project |
TI_ipEntity | project |
Type | project |
Url | project |
UserPrincipalName | project |
ValidUntil | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
User account enabled and disabled within 10 mins
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountType | eq |
| field:"AccountType" kind:eq value:"User" |
EventID | eq |
| field:"EventID" kind:eq |
TimeDelta | ge |
| field:"TimeDelta" kind:ge value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TargetAccount | ends_with | $ | excludes:TargetAccount field:"TargetAccount" value:"$" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountUsedToDisable | project |
AccountUsedToEnable | project |
Computer | project |
DisableActivity | project |
DisableEventID | project |
DisableTime | project |
DisabledBySubjectDomainName | project |
DisabledBySubjectUserName | project |
EnableActivity | project |
EnableEventID | project |
EnableTime | project |
EnabledBySubjectDomainName | project |
EnabledBySubjectUserName | project |
SIDofAccountUsedToDisable | project |
SIDofAccountUsedToEnable | project |
TargetAccount | project |
TargetDomainName | project |
TargetSid | project |
TargetUserName | project |
TimeDelta | project |
UserPrincipalName | project |
HostName | extend |
HostNameDomain | extend |