Detection rules › Kusto

Kusto rules: 1d

Azure DevOps Agent Pool Created Then Deleted

#
Status
available
Severity
high
Time window
14d
Group by
AgentPoolId, timekey
Source
github.com/Azure/Azure-Sentinel

'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

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.

FieldKindValuesSearch
OperationNameeq
  • Library.AgentPoolCreated
  • Library.AgentPoolDeleted
field:"OperationName" kind:eq
PoolTypeeq
  • Self Hosted
field:"PoolType" kind:eq value:"Self Hosted"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AgentCloudIdextend
PoolTypeextend
AgentPoolNameextend
AgentPoolIdextend
IsHostedextend
IsLegacyextend
timekeyextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

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.

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

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.

FieldKindValuesSearch
Findingeq
  • T0AddAllowedToAct
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.

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

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.

FieldKindValuesSearch
Findingeq
  • AzureT0GetKeys
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.

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

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.

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

Cloud Gateway Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Detect Unknown process using SMB or WinRM

#
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

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.

FieldKindValuesSearch
ActionTypein
  • ConnectionAttempt
  • ConnectionFailed
  • ConnectionRequest
  • ConnectionSuccess
field:"ActionType" kind:in
GlobalPrevalenceis_not_null
  • (no value, null check)
field:"GlobalPrevalence" kind:is_not_null
GlobalPrevalencelt
  • 1000 transforms: number
field:"GlobalPrevalence" kind:lt value:"1000"
InitiatingProcessSHA256is_not_null
  • (no value, null check)
field:"InitiatingProcessSHA256" kind:is_not_null
RemotePortin
  • 445 corpus 2 (kusto 2)
  • 5985
  • 5986
field:"DestinationPort" kind:in

External Repository Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Object Nameproject
Severityproject
UserNameproject

GWorkspace - An Outbound Relay has been added to a G Suite Domain

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

Detects outbound relays may be added to collect email.

MITRE ATT&CK Coverage

TacticTechniques
Collection

Telemetry Coverage

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.

FieldKindValuesSearch
EventMessagematch
  • TOGGLE_OUTBOUND_RELAY transforms: term
field:"EventMessage" kind:match value:"TOGGLE_OUTBOUND_RELAY"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

New External User Granted Admin Role

#
Status
available
Severity
medium
Time window
1d
Group by
Initiator, OperationName, Result, RoleName, Target, TimeGenerated
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
AADOperationTypein
  • Assign
  • AssignEligibleRole
field:"AADOperationType" kind:in
ActivityDisplayNamematch
  • Add eligible member to role transforms: term
  • Add member to role transforms: term
field:"ActivityDisplayName" kind:match
Categoryeq
  • RoleManagement
  • UserManagement corpus 7 (sigma 4, kusto 3)
field:"Category" kind:eq
Initiatorne
  • MS-PIM
  • MS-PIM-Fairfax
field:"Initiator" kind:ne
InviteInitiatoris_not_null
  • (no value, null check)
field:"InviteInitiator" kind:is_not_null
OperationNameeq
  • Invite external user corpus 2 (kusto 2)
  • Redeem external user invite
field:"OperationName" kind:eq
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"
RoleNamematch
  • Admin transforms: term
  • Administrator transforms: term
field:"RoleName" kind:match
Targetis_not_null
  • (no value, null check)
field:"Target" kind:is_not_null
TargetResources.userPrincipalNamematch
  • EXT transforms: term, kusto_dynamic_path
field:"TargetResources.userPrincipalName" kind:match value:"EXT"
deltale
  • 60 transforms: number
field:"delta" kind:le value:"60"
displayName_eq
  • Role.DisplayName
field:"displayName_" kind:eq value:"Role.DisplayName"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Initiatorsummarize
OperationNamesummarize
Resultsummarize
RoleNamesummarize
Targetsummarize
TimeGeneratedsummarize

PLC Stop Command (Microsoft Defender for IoT)

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

'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

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 used in Stage 3

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.

FieldKindValuesSearch
AlertNamematch
  • An S7 Stop PLC Command was Sent transforms: term
  • Controller Reset transforms: term
  • Controller Stop transforms: term
  • Excessive Restart Rate of an Outstation transforms: term
  • GE SRTP Stop PLC Command was Sent transforms: term
  • Outstation Restarted transforms: term
  • Outstation Restarts Frequently transforms: term
  • Profinet Device Factory Reset transforms: term
  • Slave Device Unrecoverable Failure transforms: term
  • Suspicion of Hardware Problems in Outstation transforms: term
field:"AlertName" kind:match
ExtendedProperties.isNeweq
  • True transforms: tostring, kusto_dynamic_path corpus 15 (kusto 15)
field:"ExtendedProperties.isNew" kind:eq value:"True"
ProviderNameeq
  • IoTSecurity corpus 15 (kusto 15)
field:"ProviderName" kind:eq value:"IoTSecurity"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertLinkproject
AlertManagementUriproject
AlertNameproject
AlertSeverityproject
Descriptionproject
DestDeviceAddressproject
DeviceIdproject
Entitiesproject
ProductComponentNameproject
ProductNameproject
Protocolproject
RemediationStepsproject
SourceDeviceAddressproject
Tacticsproject
Techniquesproject
TimeGeneratedproject
VendorOriginalIdproject

Radiflow - Unauthorized Command in Operational Device

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

'Generates an incident when an unauthorized command is detected in the network by Radiflow's iSID.'

MITRE ATT&CK Coverage

Rule Body

id: 4d90d485-6d47-417e-80ea-9cf956c1a671
name: Radiflow - Unauthorized Command in Operational Device
description: |
  'Generates an incident when an unauthorized command is detected in the network by Radiflow's iSID.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RadiflowIsid
    dataTypes:
      - RadiflowEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- LateralMovement
- InhibitResponseFunction
- ImpairProcessControl
relevantTechniques:
- T0858
- T0843
- T0816
- T0857
- T0836
- T0855
query: |
  RadiflowEvent
  | where DeviceProduct =~ 'iSID'
  | where 
      (
          EventClassID in (114, 115, 116, 117, 118, 119, 162)
          or EventMessage has 'Unauthorized'
      )
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: Unauthorized Command in Operational Device
  alertDescriptionFormat: "An unauthorized command has been detected. Please check the alert details for further information.\n\nMessage: {{EventMessage}}\nSource device: {{SourceIP}} \nDestination 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 (114, 115, 116, 117, 118, 119, 162)
        or EventMessage has 'Unauthorized'
    )

Indicators

These rows show field, operator, and value matches.

Scheduled Task Hide

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

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

TacticTechniques
Defense Impairment

Telemetry Coverage

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventDataextend
Keyextend
Valueextend
ObjectNameextend
ObjectValueNameextend
OperationTypeextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend
timestampextend

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.

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

'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

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AlarmIdproject
AlarmMainTypeproject
AlarmSubTypeproject
AlarmUrlproject
Severityproject
Statusproject
TimeGeneratedproject
Titleproject

TI Map IP Entity to CommonSecurityLog

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

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

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: extend

| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =
ifisempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIP
elseTI_ipEntity

Stage 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.

FieldKindValuesSearch
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CS_ipEntityproject
Confidenceproject
DestinationIPproject
DeviceActionproject
DeviceProductproject
DeviceVendorproject
Idproject
LogSeverityproject
Messageproject
MessageIPproject
SourceIPproject
TI_ipEntityproject
Typeproject
ValidUntilproject
timestampproject

User impersonation by Identity Protection alerts

#
Severity
medium
Time window
1d
Group by
$left.ipAddress, $right.SourceIpAddress, AlertName, AlertSeverity, AlertTime, ProductName, ipAddress
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
Privilege Escalation

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
AlertSeverityeq
  • High
field:"AlertSeverity" kind:eq value:"High"
ErrorMessageis_null
  • (no value, null check)
field:"ErrorMessage" kind:is_null
EventNamein
  • AddUserToGroup
  • ChangePassword
  • CreateAccessKey
  • CreateGroup
  • CreateRole
  • CreateUser
  • CreateVirtualMFADevice
  • DeleteAccessKey
  • DeleteGroup
  • DeleteLoginProfile
  • DeleteRole
  • DeleteUser
  • RemoveUserFromGroup
field:"EventName" kind:in
ProductNamematch
  • Azure Active Directory Identity Protection transforms: term
field:"ProductName" kind:match value:"Azure Active Directory Identity Protection"
ipAddressis_not_null
  • (no value, null check)
field:"ipAddress" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertNamesummarize
AlertSeveritysummarize
AlertTimesummarize
ProductNamesummarize
ipAddresssummarize

Windows host username encoded in base64 web request

#
Severity
medium
Time window
1d
Group by
$left.base64_user, $right.base64_candidate, InitiatingProcessAccountName
Author
Thomas McElroy
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

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 used in Stage 6

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.

FieldKindValuesSearch
InitiatingProcessAccountNameis_not_null
  • (no value, null check)
field:"InitiatingProcessAccountName" kind:is_not_null
RequestURLis_not_null
  • (no value, null check)
field:"RequestURL" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsproject
DeviceNamesproject
FirstRequestproject
LastRequestproject
NumberOfRequestsproject
RequestURLproject
UserNameproject

Back to all Kusto rules