Detection rules › Kusto
Kusto rules: 1d
Azure DevOps Agent Pool Created Then Deleted
#'As well as adding build agents to an existing pool to execute malicious activity within a pipeline, an attacker could create a complete new agent pool and use this for execution. Azure DevOps allows for the creation of agent pools with Azure hosted infrastructure or self-hosted infrastructure. Given the additional customizability of self-hosted agents this detection focuses on the creation of new self-hosted pools. To further reduce false positive rates the detection looks for pools created and deleted relatively quickly (within 7 days by default), as an attacker is likely to remove a malicious pool once used in order to reduce/remove evidence of their activity.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: acfdee3f-b794-404a-aeba-ef6a1fa08ad1
name: Azure DevOps Agent Pool Created Then Deleted
description: |
'As well as adding build agents to an existing pool to execute malicious activity within a pipeline, an attacker could create a complete new agent pool and use this for execution.
Azure DevOps allows for the creation of agent pools with Azure hosted infrastructure or self-hosted infrastructure. Given the additional customizability of self-hosted agents this detection focuses on the creation of new self-hosted pools.
To further reduce false positive rates the detection looks for pools created and deleted relatively quickly (within 7 days by default), as an attacker is likely to remove a malicious pool once used in order to reduce/remove evidence of their activity.'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1578.002
query: |
let lookback = 14d;
let timewindow = 7d;
ADOAuditLogs
| where TimeGenerated > ago(lookback)
| where OperationName =~ "Library.AgentPoolCreated"
| extend AgentCloudId = tostring(Data.AgentCloudId)
| extend PoolType = iif(isnotempty(AgentCloudId), "Azure VMs", "Self Hosted")
// Comment this line out to include cloud pools as well
| where PoolType == "Self Hosted"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend AgentPoolId = tostring(Data.AgentPoolId)
| extend IsHosted = tostring(Data.IsHosted)
| extend IsLegacy = tostring(Data.IsLegacy)
| extend timekey = bin(TimeGenerated, timewindow)
// Join only with pools deleted in the same window
| join (ADOAuditLogs
| where TimeGenerated > ago(lookback)
| where OperationName =~ "Library.AgentPoolDeleted"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend AgentPoolId = tostring(Data.AgentPoolId)
| extend timekey = bin(TimeGenerated, timewindow)) on AgentPoolId, timekey
| project-reorder TimeGenerated, ActorUPN, UserAgent, IpAddress, AuthenticationMechanism, OperationName, AgentPoolName, IsHosted, IsLegacy, Data
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let timewindow = 7d;
Stage 1: source
ADOAuditLogs
Stage 2: where
| where TimeGenerated > ago(lookback)
Stage 3: where
| where OperationName =~ "Library.AgentPoolCreated"
Stage 4: extend
| extend AgentCloudId = tostring(Data.AgentCloudId)
Stage 5: extend
| extend PoolType = iif(isnotempty(AgentCloudId), "Azure VMs", "Self Hosted")
Stage 6: where
| where PoolType == "Self Hosted"
Stage 7: extend (5 consecutive steps)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend AgentPoolId = tostring(Data.AgentPoolId)
| extend IsHosted = tostring(Data.IsHosted)
| extend IsLegacy = tostring(Data.IsLegacy)
| extend timekey = bin(TimeGenerated, timewindow)
Stage 8: join
| join (ADOAuditLogs
| where TimeGenerated > ago(lookback)
| where OperationName =~ "Library.AgentPoolDeleted"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend AgentPoolId = tostring(Data.AgentPoolId)
| extend timekey = bin(TimeGenerated, timewindow)) on AgentPoolId, timekey
Stage 9: project-reorder
project-reorder
Stage 10: extend
extend timestamp
Stage 11: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq |
PoolType | eq |
| field:"PoolType" kind:eq value:"Self Hosted" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AgentCloudId | extend |
PoolType | extend |
AgentPoolName | extend |
AgentPoolId | extend |
IsHosted | extend |
IsLegacy | extend |
timekey | extend |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
BloodHound Attack Path Finding - Add Resource-Based Constrained Delegation Privileges on Tier Zero Computers
#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 - Add Resource-Based Constrained Delegation Privileges on Tier Zero Computers
Rule Body
id: ca509c4b-d30f-4bad-b59c-4d0ddd0fc822
name: BloodHound Attack Path Finding - Add Resource-Based Constrained Delegation Privileges
on Tier Zero Computers
description: BloodHound Attack Path Finding - Add Resource-Based Constrained Delegation
Privileges on Tier Zero Computers
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 == "T0AddAllowedToAct"
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 == "T0AddAllowedToAct"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0AddAllowedToAct" |
BloodHound Attack Path Finding - Get Keys on Tier Zero Key Vault
#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 - Get Keys on Tier Zero Key Vault
Rule Body
id: fbeabff4-fad7-4458-8098-c6426388cf3d
name: BloodHound Attack Path Finding - Get Keys on Tier Zero Key Vault
description: BloodHound Attack Path Finding - Get Keys on Tier Zero Key Vault
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 == "AzureT0GetKeys"
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 == "AzureT0GetKeys"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0GetKeys" |
BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9 Scenario B Privileges
#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 - Non-Tier Zero Principals With ADCS ESC9 Scenario B Privileges
Rule Body
id: 93a17e41-1504-4ef3-8a1e-7ad15a32ce98
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9 Scenario
B Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9
Scenario B Privileges
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 == "T0ADCSESC9b"
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 == "T0ADCSESC9b"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC9b" |
Cloud Gateway Deleted
#Detects when a cloud gateway is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: 72239d5b-ef96-4f15-896f-6cdcd9c53ca7
name: Cloud Gateway Deleted
description: Detects when a cloud gateway is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
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 == 24140\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 == 24140
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:"24140" |
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 |
Detect Unknown process using SMB or WinRM
#WinRM and SMB are popular network protocols to perform lateral movement by adversaries (while there are some others as well). When an unknown process is performing SMB or WinRM network connections, this might indicate that a malware process is trying to move laterally to other devices in your network. > [!WARNING] > This detection rule is the base for the detection. You will need to add environment specific finetuning in order to limit the BP detections on legitimate processes
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
References
Telemetry Coverage
Rule Body
DeviceNetworkEvents
| where TimeGenerated > ago(1h)
| where RemotePort in ("5985", "5986", "445")
| where ActionType in~ ("ConnectionSuccess", "ConnectionAttempt",
"ConnectionFailed", "ConnectionRequest")
| where isnotempty(InitiatingProcessSHA256)
| invoke FileProfile(InitiatingProcessSHA256)
| where isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000
Stages and Predicates
Stage 1: source
DeviceNetworkEvents
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where RemotePort in ("5985", "5986", "445")
Stage 4: where
| where ActionType in~ ("ConnectionSuccess", "ConnectionAttempt",
"ConnectionFailed", "ConnectionRequest")
Stage 5: where
| where isnotempty(InitiatingProcessSHA256)
Stage 6: invoke
| invoke FileProfile(InitiatingProcessSHA256)
Stage 7: where
| where isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | in |
| field:"ActionType" kind:in |
GlobalPrevalence | is_not_null | field:"GlobalPrevalence" kind:is_not_null | |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"1000" |
InitiatingProcessSHA256 | is_not_null | field:"InitiatingProcessSHA256" kind:is_not_null | |
RemotePort | in |
| field:"DestinationPort" kind:in |
External Repository Deleted
#Detects when an external repository is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: cc47b9d6-a10e-4c5e-94a1-c73a68273e2d
name: External Repository Deleted
description: Detects when an external repository is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 32200\n| extend RepositoryName\
\ = extract(\"RepositoryName=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n\
\ Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource\
\ = original_host,\n EventId = instanceId,\n UserName = user,\n [\"Object\
\ Name\"] = RepositoryName,\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 == 32200
Stage 3: extend
| extend RepositoryName = extract("RepositoryName=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
["Object Name"] = RepositoryName,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"32200" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Object Name | project |
Severity | project |
UserName | project |
GWorkspace - An Outbound Relay has been added to a G Suite Domain
#Detects outbound relays may be added to collect email.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | TOGGLE_OUTBOUND_RELAY: Outbound Relay Change |
Rule Body
id: ead87cd6-5da7-11ec-bf63-0242ac130002
name: GWorkspace - An Outbound Relay has been added to a G Suite Domain
description: |
'Detects outbound relays may be added to collect email.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleWorkspaceReportsAPI
dataTypes:
- GWorkspaceActivityReports
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1114
query: |
GWorkspaceActivityReports
| where EventMessage has "TOGGLE_OUTBOUND_RELAY"
| extend AccountCustomEntity = ActorEmail
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
GWorkspaceActivityReports
Stage 2: where
| where EventMessage has "TOGGLE_OUTBOUND_RELAY"
Stage 3: extend
| extend AccountCustomEntity = ActorEmail
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"TOGGLE_OUTBOUND_RELAY" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
New External User Granted Admin Role
#This query will detect instances where a newly invited external user is granted an administrative role. By default this query will alert on any granted administrative role, however this can be modified using the roles variable if false positives occur in your environment. The maximum delta between invite and escalation to admin is 60 minues, this can be configured using the deltaBetweenInviteEscalation variable.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Invite external user |
| Azure | Redeem external user invite |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
Rule Body
id: d7424fd9-abb3-4ded-a723-eebe023aaa0b
name: New External User Granted Admin Role
description: |
'This query will detect instances where a newly invited external user is granted an administrative role.
By default this query will alert on any granted administrative role, however this can be modified using the roles variable if false positives occur in your environment. The maximum delta between invite and escalation to admin is 60 minues, this can be configured using the deltaBetweenInviteEscalation variable.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098.001
query: |
// Administrative roles to look for, default is all admin roles
let roles = dynamic(["Administrator", "Admin"]);
// The maximum distances between and invite and acceptance
let maxTimeBetweenInviteAccept = 30min;
// The delta (minutes) between the invite being sent and the account being escalated
let deltaBetweenInviteEscalation = 60;
// Collect external user invitations
let invite = AuditLogs
| where Category =~ "UserManagement"
| where OperationName =~ "Invite external user"
| extend Target = tostring(TargetResources[0].["userPrincipalName"])
| extend InviteInitiator = tostring(InitiatedBy.["user"].["userPrincipalName"])
| where isnotempty(InviteInitiator);
// Collect redeem events
let redeem = AuditLogs
| where Category =~ "UserManagement"
| where OperationName =~ "Redeem external user invite"
| where Result =~ "success"
| extend Target = tostring(TargetResources[0].["displayName"]) | extend Target = tostring(extract(@"UPN\:\s(.+)\,\sEmail",1,Target))
| where isnotempty(Target);
// Union the inivtation and redeem data then run the sequence_detect kusto plugin
invite
| union redeem
| order by TimeGenerated
| project TimeGenerated, Target, InviteInitiator, OperationName, TenantId
| evaluate sequence_detect(TimeGenerated, maxTimeBetweenInviteAccept, maxTimeBetweenInviteAccept, invite=(OperationName has "Invite external user"), redeem=(OperationName has "Redeem external user invite"), Target)
| join kind=innerunique (
AuditLogs
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-expand TargetResources
// Limit to external accounts
| where TargetResources.userPrincipalName has "EXT"
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
// Perform check for admin roles
| where RoleName has_any(roles)
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize by TimeGenerated, OperationName, RoleName, Target, Initiator, Result
) on Target
// Calculate delta between the invite and the account escalation
| extend delta = datetime_diff("minute", TimeGenerated, invite_TimeGenerated)
| where delta <= deltaBetweenInviteEscalation
| project InvitationTime=invite_TimeGenerated, RedeemTime=redeem_TimeGenerated, GrantTime=TimeGenerated, ExternalUser=Target, RoleGranted=RoleName, AdminInitiator=Initiator, MinsBetweenInviteAndEscalation=delta
| extend ExternalUserName = tostring(split(ExternalUser, '@', 0)[0]), ExternalUserUPNSuffix = tostring(split(ExternalUser, '@', 1)[0])
| extend AdminInitiatorName = tostring(split(AdminInitiator, '@', 0)[0]), AdminInitiatorUPNSuffix = tostring(split(AdminInitiator, '@', 1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ExternalUserName
- identifier: UPNSuffix
columnName: ExternalUserUPNSuffix
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AdminInitiatorName
- identifier: UPNSuffix
columnName: AdminInitiatorUPNSuffix
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let roles = dynamic(["Administrator", "Admin"]);
let maxTimeBetweenInviteAccept = 30min;
let deltaBetweenInviteEscalation = 60;
let invite and let redeem are inlined into the numbered stages below.
Stages 1 to 6 define let invite as the rule's main pipeline source. Stages 7 to 24 combine it with further branches.
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "UserManagement"
Stage 3: where
| where OperationName =~ "Invite external user"
Stage 4: extend
| extend Target = tostring(TargetResources[0].["userPrincipalName"])
Stage 5: extend
| extend InviteInitiator = tostring(InitiatedBy.["user"].["userPrincipalName"])
Stage 6: where
| where isnotempty(InviteInitiator)
Stage 7: union
union of 1 branches
Stage 8: source
AuditLogs
Stage 9: where
| where Category =~ "UserManagement"
Stage 10: where
| where OperationName =~ "Redeem external user invite"
Stage 11: where
| where Result =~ "success"
Stage 12: extend
| extend Target = tostring(TargetResources[0].["displayName"])
Stage 13: extend
| extend Target = tostring(extract(@"UPN\:\s(.+)\,\sEmail",1,Target))
Stage 14: where
| where isnotempty(Target)
Stage 15: sort
sort by TimeGenerated
Stage 16: project
project InviteInitiator, OperationName, Target, TenantId, TimeGenerated
Stage 17: evaluate
evaluate
Stage 18: join
| join kind=innerunique (
AuditLogs
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-expand TargetResources
| where TargetResources.userPrincipalName has "EXT"
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
| where RoleName has_any(roles)
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize by TimeGenerated, OperationName, RoleName, Target, Initiator, Result
) on Target
Stage 19: extend
extend delta
Stage 20: where
where delta <= 60
Stage 21: project
project AdminInitiator, ExternalUser, GrantTime, InvitationTime, MinsBetweenInviteAndEscalation, RedeemTime, RoleGranted
Stage 22: extend
extend ExternalUserName, ExternalUserUPNSuffix
Stage 23: extend
extend AdminInitiatorName, AdminInitiatorUPNSuffix
Stage 24: summarize aggregation inside the join branch
summarize by TimeGenerated, OperationName, RoleName, Target, Initiator, Result
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | in |
| field:"AADOperationType" kind:in |
ActivityDisplayName | match |
| field:"ActivityDisplayName" kind:match |
Category | eq |
| field:"Category" kind:eq |
Initiator | ne |
| field:"Initiator" kind:ne |
InviteInitiator | is_not_null | field:"InviteInitiator" kind:is_not_null | |
OperationName | eq |
| field:"OperationName" kind:eq |
Result | eq |
| field:"Result" kind:eq value:"success" |
RoleName | match |
| field:"RoleName" kind:match |
Target | is_not_null | field:"Target" kind:is_not_null | |
TargetResources.userPrincipalName | match |
| field:"TargetResources.userPrincipalName" kind:match value:"EXT" |
delta | le |
| field:"delta" kind:le value:"60" |
displayName_ | eq |
| field:"displayName_" kind:eq value:"Role.DisplayName" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Initiator | summarize |
OperationName | summarize |
Result | summarize |
RoleName | summarize |
Target | summarize |
TimeGenerated | summarize |
PLC Stop Command (Microsoft Defender for IoT)
#'This alert leverages Defender for IoT to detect PLC stop commands which could indicate improper configuration or malicious activity on the network such as a threat manipulating PLC programming to affect the function of the network.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Evasion |
Rule Body
id: a7d3f642-15d8-4e83-99ee-83ca3352525d
name: PLC Stop Command (Microsoft Defender for IoT)
description: |
'This alert leverages Defender for IoT to detect PLC stop commands which could indicate improper configuration or malicious activity on the network such as a threat manipulating PLC programming to affect the function of the network.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: IoT
dataTypes:
- SecurityAlert (ASC for IoT)
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T0858
query: |
let alertList = dynamic(["Controller Reset", "An S7 Stop PLC Command was Sent", "Controller Stop", "Excessive Restart Rate of an Outstation", "GE SRTP Stop PLC Command was Sent", "Outstation Restarted", "Outstation Restarts Frequently", "Profinet Device Factory Reset", "Slave Device Unrecoverable Failure", "Suspicion of Hardware Problems in Outstation"]);
SecurityAlert
| where ProviderName == "IoTSecurity"
| where AlertName has_any (alertList)
| extend ExtendedProperties = parse_json(ExtendedProperties)
| where tostring(ExtendedProperties.isNew) == "True"
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
entityMappings:
sentinelEntitiesMappings:
- columnName: Entities
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
Sensor: DeviceId
Protocol: Protocol
VendorOriginalId: VendorOriginalId
AlertManagementUri: AlertManagementUri
alertDetailsOverride:
alertDisplayNameFormat: (MDIoT) {{AlertName}}
alertDescriptionFormat: (MDIoT) {{Description}}
alertTacticsColumnName: Tactics
alertSeverityColumnName: AlertSeverity
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: RemediationSteps
value: RemediationSteps
- alertProperty: Techniques
value: Techniques
- alertProperty: ProductComponentName
value: ProductComponentName
- alertProperty: AlertLink
value: AlertLink
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: alertList
let alertList = dynamic(["Controller Reset", "An S7 Stop PLC Command was Sent", "Controller Stop", "Excessive Restart Rate of an Outstation", "GE SRTP Stop PLC Command was Sent", "Outstation Restarted", "Outstation Restarts Frequently", "Profinet Device Factory Reset", "Slave Device Unrecoverable Failure", "Suspicion of Hardware Problems in Outstation"]);
Stage 1: source
SecurityAlert
Stage 2: where
| where ProviderName == "IoTSecurity"
Stage 3: where
| where AlertName has_any (alertList)
Stage 4: extend
| extend ExtendedProperties = parse_json(ExtendedProperties)
Stage 5: where
| where tostring(ExtendedProperties.isNew) == "True"
Stage 6: extend
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
Stage 7: project
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | match |
| field:"AlertName" kind:match |
ExtendedProperties.isNew | eq |
| field:"ExtendedProperties.isNew" kind:eq value:"True" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IoTSecurity" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertManagementUri | project |
AlertName | project |
AlertSeverity | project |
Description | project |
DestDeviceAddress | project |
DeviceId | project |
Entities | project |
ProductComponentName | project |
ProductName | project |
Protocol | project |
RemediationSteps | project |
SourceDeviceAddress | project |
Tactics | project |
Techniques | project |
TimeGenerated | project |
VendorOriginalId | project |
Scheduled Task Hide
#This query detects attempts by malware to hide the scheduled task by deleting the SD (Security Descriptor) value. Removal of SD value results in the scheduled task disappearing from schtasks /query and Task Scheduler. The query requires auditing to be turned on for HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree registry hive as well as audit policy for registry auditing to be turned on. Reference: https://www.microsoft.com/security/blog/2022/04/12/tarrask-malware-uses-scheduled-tasks-for-defense-evasion/ Reference: https://4sysops.com/archives/audit-changes-in-the-windows-registry/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4657: A registry value was modified. |
Rule Body
id: 6dd2629c-534b-4275-8201-d7968b4fa77e
name: Scheduled Task Hide
description: |
'This query detects attempts by malware to hide the scheduled task by deleting the SD (Security Descriptor) value. Removal of SD value results in the scheduled task disappearing from schtasks /query and Task Scheduler.
The query requires auditing to be turned on for HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree registry hive as well as audit policy for registry auditing to be turned on.
Reference: https://www.microsoft.com/security/blog/2022/04/12/tarrask-malware-uses-scheduled-tasks-for-defense-evasion/
Reference: https://4sysops.com/archives/audit-changes-in-the-windows-registry/'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
tags:
- Tarrask
query: |
SecurityEvent
| where EventID == 4657
| 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, TargetAccount, Computer, EventSourceName, Channel, Task, Level, EventID, Activity, TargetLogonId, SourceComputerId, EventOriginId, Type, _ResourceId, TenantId, SourceSystem, ManagementGroupName, IpAddress, Account)
| extend ObjectName = column_ifexists('ObjectName', ""), OperationType = column_ifexists('OperationType', ""), ObjectValueName = column_ifexists('ObjectValueName', "")
| where ObjectName has 'Schedule\\TaskCache\\Tree' and ObjectValueName == "SD" and OperationType == "%%1906" // %%1906 - Registry value deleted
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(TargetAccount, @'\')[1]), AccountNTDomain = tostring(split(TargetAccount, @'\')[0])
| extend timestamp = TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetAccount
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 4657
Stage 3: extend
| extend EventData = parse_xml(EventData).EventData.Data
Stage 4: mv-expand
| mv-expand bagexpansion=array EventData
Stage 5: evaluate
| evaluate bag_unpack(EventData)
Stage 6: extend
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
Stage 7: evaluate
| evaluate pivot(Key, any(Value), TimeGenerated, TargetAccount, Computer, EventSourceName, Channel, Task, Level, EventID, Activity, TargetLogonId, SourceComputerId, EventOriginId, Type, _ResourceId, TenantId, SourceSystem, ManagementGroupName, IpAddress, Account)
Stage 8: extend
| extend ObjectName = column_ifexists('ObjectName', ""), OperationType = column_ifexists('OperationType', ""), ObjectValueName = column_ifexists('ObjectValueName', "")
Stage 9: where
| where ObjectName has 'Schedule\\TaskCache\\Tree' and ObjectValueName == "SD" and OperationType == "%%1906"
Stage 10: extend (4 consecutive steps)
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(TargetAccount, @'\')[1]), AccountNTDomain = tostring(split(TargetAccount, @'\')[0])
| extend timestamp = TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4657" |
ObjectName | match |
| field:"ObjectName" kind:match value:"Schedule\TaskCache\Tree" |
ObjectValueName | eq |
| field:"ObjectValueName" kind:eq value:"SD" |
OperationType | eq |
| field:"OperationType" kind:eq value:"%%1906" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventData | extend |
Key | extend |
Value | extend |
ObjectName | extend |
ObjectValueName | extend |
OperationType | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
timestamp | extend |
SOCRadar High or Critical Severity Alarm
#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 SOCRadar alarms with High or Critical severity levels that require immediate attention. These alarms typically indicate active threats such as credential exposure, ransomware mentions, or targeted attacks against the organization.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access |
Rule Body
id: 8f3e2c5a-7b91-4d6a-9e8f-1c4a2b5d7e3f
name: SOCRadar High or Critical Severity Alarm
description: |
'Detects SOCRadar alarms with High or Critical severity levels that require immediate attention. These alarms typically indicate active threats such as credential exposure, ransomware mentions, or targeted attacks against the organization.'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
- InitialAccess
relevantTechniques:
- T1589
- T1078
query: |
SOCRadar_Alarms_CL
| where Severity in ("High", "Critical")
| where Status == "OPEN"
| extend AlarmUrl = strcat("https://platform.socradar.com/company/", CompanyId, "/alarms/", AlarmId)
| extend AccountName = tostring(AlarmId)
| project TimeGenerated, AlarmId, Title, AlarmMainType, AlarmSubType, Severity, Status, AlarmUrl, AccountName
entityMappings:
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: AlarmMainType
- identifier: Category
columnName: AlarmSubType
- entityType: URL
fieldMappings:
- identifier: Url
columnName: AlarmUrl
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SOCRadar_Alarms_CL
Stage 2: where
| where Severity in ("High", "Critical")
Stage 3: where
| where Status == "OPEN"
Stage 4: extend
| extend AlarmUrl = strcat("https://platform.socradar.com/company/", CompanyId, "/alarms/", AlarmId)
Stage 5: extend
| extend AccountName = tostring(AlarmId)
Stage 6: project
| project TimeGenerated, AlarmId, Title, AlarmMainType, AlarmSubType, Severity, Status, AlarmUrl, AccountName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | in |
| field:"Severity" kind:in |
Status | eq |
| field:"Status" kind:eq value:"OPEN" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AlarmId | project |
AlarmMainType | project |
AlarmSubType | project |
AlarmUrl | project |
Severity | project |
Status | project |
TimeGenerated | project |
Title | project |
TI Map IP Entity to CommonSecurityLog
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: cdd1933b-ef94-48a4-b94a-18d45b902751
name: TI Map IP Entity to CommonSecurityLog
description: |
This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: CEF
dataTypes:
- CommonSecurityLog
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
let dt_lookBack = 1h; // Look back 1 hour for CommonSecurityLog events
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = 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 = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Perform a join between IP indicators and CommonSecurityLog events
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
// Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
| join kind=innerunique (
CommonSecurityLog
| where TimeGenerated >= ago(dt_lookBack)
| extend MessageIP = extract(IPRegex, 0, Message)
| extend CS_ipEntity = iff((not(ipv4_is_private(SourceIP)) and isnotempty(SourceIP)), SourceIP, DestinationIP)
| extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CS_ipEntity
// Filter out logs that occurred after the expiration of the corresponding indicator
| where CommonSecurityLog_TimeGenerated < ValidUntil
// Group the results by IndicatorId and CS_ipEntity, and keep the log entry with the latest timestamp
| summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id, CS_ipEntity
// Select the desired output fields
| project timestamp = CommonSecurityLog_TimeGenerated, SourceIP, DestinationIP, MessageIP, Message, DeviceVendor, DeviceProduct, Id, ValidUntil, Confidence, TI_ipEntity, CS_ipEntity, LogSeverity, DeviceAction, Type
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CS_ipEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.2.8
kind: Scheduled
Stages and Predicates
Parameters
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Stages 1 to 11 define let IP_Indicators as the rule's main pipeline source. Stages 12 to 16 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 = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
TI_ipEntity =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 8: extend
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =isempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIPTI_ipEntityStage 9: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 10: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 11: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 12: project-reorder
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 13: join
| join kind=innerunique (
CommonSecurityLog
| where TimeGenerated >= ago(dt_lookBack)
| extend MessageIP = extract(IPRegex, 0, Message)
| extend CS_ipEntity = iff((not(ipv4_is_private(SourceIP)) and isnotempty(SourceIP)), SourceIP, DestinationIP)
| extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CS_ipEntity
Stage 14: where
where CommonSecurityLog_TimeGenerated < ValidUntil
Stage 15: summarize
summarize CommonSecurityLog_TimeGenerated by Id, CS_ipEntity
Stage 16: project
project CS_ipEntity, Confidence, DestinationIP, DeviceAction, DeviceProduct, DeviceVendor, Id, LogSeverity, Message, MessageIP, SourceIP, TI_ipEntity, Type, ValidUntil, 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" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | 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:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CS_ipEntity | project |
Confidence | project |
DestinationIP | project |
DeviceAction | project |
DeviceProduct | project |
DeviceVendor | project |
Id | project |
LogSeverity | project |
Message | project |
MessageIP | project |
SourceIP | project |
TI_ipEntity | project |
Type | project |
ValidUntil | project |
timestamp | project |
User impersonation by Identity Protection alerts
#'This detection focuses on identifying user-related events involving IAM roles, groups, user access, and password changes. It examines instances where the user's IP address matches and alerts generated by Identity Protection share the same IP address. The analysis occurs within a time window of 1 hour, helping to flag potential cases of user impersonation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rules Detecting the Same Action
These rules filter on the same operation.
- ASL AWS Create Access Key (Splunk)
- ASL AWS IAM Failure Group Deletion (Splunk)
- ASL AWS IAM Successful Group Deletion (Splunk)
- ASL AWS New MFA Method Registered For User (Splunk)
- AWS CreateAccessKey (Splunk)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWS New MFA Method Registered For User (Splunk)
- AWSCloudTrail - Creation of Access Key for IAM User (Kusto)
Rule Body
id: 11c3d541-5fa5-49df-8218-d1c98584473b
name: User impersonation by Identity Protection alerts
description: |
'This detection focuses on identifying user-related events involving IAM roles, groups, user access, and password changes. It examines instances where the user's IP address matches and alerts generated by Identity Protection share the same IP address. The analysis occurs within a time window of 1 hour, helping to flag potential cases of user impersonation.'
severity: Medium
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert (IPC)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1134
query: |
// Retrieve SecurityAlerts generated within the last day
SecurityAlert
// Filter alerts for Azure Active Directory Identity Protection and High severity
| where ProductName has "Azure Active Directory Identity Protection"
| where AlertSeverity == "High"
// Extract IP address entities from the 'Entities' field
| extend ipAddress = extract(@'\b(?:\d{1,3}\.){3}\d{1,3}\b', 0, Entities)
// Filter out alerts without IP address entities
| where isnotempty(ipAddress)
// Summarize entities per unique combination of attributes
| summarize make_set(Entities)
by
AlertTime = TimeGenerated,
ipAddress,
AlertName,
ProductName,
AlertSeverity
// Perform an inner join with AWS CloudTrail events
| join kind=inner (
AWSCloudTrail
| where isempty(ErrorMessage)
| extend UserType = tostring(parse_json(RequestParameters).userType)
| where EventName in~ ("CreateRole", "DeleteRole", "CreateUser", "CreateAccessKey", "DeleteAccessKey", "CreateGroup", "AddUserToGroup", "ChangePassword", "DeleteGroup", "DeleteUser", "RemoveUserFromGroup", "CreateVirtualMFADevice", "DeleteLoginProfile")
| summarize
make_set(RequestParameters),
make_set(ResponseElements)
by
SourceIpAddress,
UserIdentityArn,
UserIdentityType,
EventName,
EventTime = TimeGenerated
)
on $left.ipAddress == $right.SourceIpAddress
// Filter results based on temporal correlation
| where AlertTime between ((EventTime - 1h) .. (EventTime + 1h))
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
AWSUser: UserIdentityArn
AlertIp : ipAddress
AlertName: AlertName
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where ProductName has "Azure Active Directory Identity Protection"
Stage 3: where
| where AlertSeverity == "High"
Stage 4: extend
| extend ipAddress = extract(@'\b(?:\d{1,3}\.){3}\d{1,3}\b', 0, Entities)
Stage 5: where
| where isnotempty(ipAddress)
Stage 6: summarize
| summarize make_set(Entities)
by
AlertTime = TimeGenerated,
ipAddress,
AlertName,
ProductName,
AlertSeverity
Stage 7: join
| join kind=inner (
AWSCloudTrail
| where isempty(ErrorMessage)
| extend UserType = tostring(parse_json(RequestParameters).userType)
| where EventName in~ ("CreateRole", "DeleteRole", "CreateUser", "CreateAccessKey", "DeleteAccessKey", "CreateGroup", "AddUserToGroup", "ChangePassword", "DeleteGroup", "DeleteUser", "RemoveUserFromGroup", "CreateVirtualMFADevice", "DeleteLoginProfile")
| summarize
make_set(RequestParameters),
make_set(ResponseElements)
by
SourceIpAddress,
UserIdentityArn,
UserIdentityType,
EventName,
EventTime = TimeGenerated
)
on $left.ipAddress == $right.SourceIpAddress
Stage 8: where
where /* macro: (AlertTime between ((EventTime - 1h) .. (EventTime + 1h))) */
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertSeverity | eq |
| field:"AlertSeverity" kind:eq value:"High" |
ErrorMessage | is_null | field:"ErrorMessage" kind:is_null | |
EventName | in |
| field:"EventName" kind:in |
ProductName | match |
| field:"ProductName" kind:match value:"Azure Active Directory Identity Protection" |
ipAddress | is_not_null | field:"ipAddress" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertName | summarize |
AlertSeverity | summarize |
AlertTime | summarize |
ProductName | summarize |
ipAddress | summarize |
Windows host username encoded in base64 web request
#This detection will identify network requests in HTTP proxy data that contains Base64 encoded usernames from machines in the DeviceEvents table. This technique was seen usee by POLONIUM in their RunningRAT tool.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceEvents action any: Defender event |
Rule Body
id: 6e715730-82c0-496c-983b-7a20c4590bd9
name: Windows host username encoded in base64 web request
description: |
'This detection will identify network requests in HTTP proxy data that contains Base64 encoded usernames from machines in the DeviceEvents table.
This technique was seen usee by POLONIUM in their RunningRAT tool.'
severity: Medium
requiredDataConnectors:
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- CommandAndControl
relevantTechniques:
- T1041
- T1071.001
tags:
- POLONIUM
query: |
let accountLookback = 3d;
let requestLookback = 3d;
let extraction_regex = @"(?:\?|&)[a-zA-Z0-9\%]*=([a-zA-Z0-9\/\+\=]*)";
// Collect account names and base64 encode them
DeviceEvents
| where TimeGenerated > ago(accountLookback)
| summarize make_set(DeviceId), make_set(DeviceName) by InitiatingProcessAccountName
| where isnotempty(InitiatingProcessAccountName)
| extend base64_user = base64_encode_tostring(InitiatingProcessAccountName)
| join (
// Collect requests and extract base64 parameters
CommonSecurityLog
| where TimeGenerated > ago(requestLookback)
| where isnotempty(RequestURL)
// Summarize early on the RequestURL
| summarize FirstRequest=min(TimeGenerated), LastRequest=max(TimeGenerated), NumberOfRequests=count() by RequestURL
| extend base64_candidate = extract_all(extraction_regex, RequestURL)
| mv-expand base64_candidate to typeof(string)
) on $left.base64_user == $right.base64_candidate
| project FirstRequest, LastRequest, NumberOfRequests, RequestURL, DeviceIds=set_DeviceId, DeviceNames=set_DeviceName, UserName=InitiatingProcessAccountName
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceNames
- entityType: URL
fieldMappings:
- identifier: Url
columnName: RequestURL
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserName
version: 1.0.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Thomas McElroy
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let accountLookback = 3d;
let requestLookback = 3d;
Let Binding: extraction_regex
let extraction_regex = @"(?:\?|&)[a-zA-Z0-9\%]*=([a-zA-Z0-9\/\+\=]*)";
Stage 1: source
DeviceEvents
Stage 2: where
| where TimeGenerated > ago(accountLookback)
Stage 3: summarize
| summarize make_set(DeviceId), make_set(DeviceName) by InitiatingProcessAccountName
Stage 4: where
| where isnotempty(InitiatingProcessAccountName)
Stage 5: extend
| extend base64_user = base64_encode_tostring(InitiatingProcessAccountName)
Stage 6: join
| join (
CommonSecurityLog
| where TimeGenerated > ago(requestLookback)
| where isnotempty(RequestURL)
| summarize FirstRequest=min(TimeGenerated), LastRequest=max(TimeGenerated), NumberOfRequests=count() by RequestURL
| extend base64_candidate = extract_all(extraction_regex, RequestURL)
| mv-expand base64_candidate to typeof(string)
) on $left.base64_user == $right.base64_candidate
Stage 7: project
project DeviceIds, DeviceNames, FirstRequest, LastRequest, NumberOfRequests, RequestURL, UserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
InitiatingProcessAccountName | is_not_null | field:"InitiatingProcessAccountName" kind:is_not_null | |
RequestURL | is_not_null | field:"RequestURL" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceIds | project |
DeviceNames | project |
FirstRequest | project |
LastRequest | project |
NumberOfRequests | project |
RequestURL | project |
UserName | project |