Detection rules › Kusto

Kusto rules: 07

BloodHound Attack Path Finding - Add Secret to Tier Zero Principal

#

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 Secret to Tier Zero Principal

Rule Body

id: 01ee9ec6-ca69-478d-8830-8fa598ae73d9
name: BloodHound Attack Path Finding - Add Secret to Tier Zero Principal
description: BloodHound Attack Path Finding - Add Secret to Tier Zero Principal
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureT0AddSecret\""
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 == "AzureT0AddSecret"

Indicators

These rows show field, operator, and value matches.

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

Cisco SEG - Unexpected attachment

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

'Detects possibly malicious attachments.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: f8ba18c4-81e3-4db0-8f85-4989f2ed2ade
name: Cisco SEG - Unexpected attachment
description: |
  'Detects possibly malicious attachments.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  CiscoSEGEvent
  | where NetworkDirection =~ 'Incoming'
  | where tostring(AdditionalFields) has 'ESAAttachmentDetails'
  | extend attachment = replace_string(tostring(extract(@'"ESAAttachmentDetails":"{(.*?):', 1, tostring(AdditionalFields))), "'", "")
  | where attachment endswith '.ps1' or attachment endswith '.lnk' or attachment endswith '.exe'
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSEGEvent

Stage 2: where

| where NetworkDirection =~ 'Incoming'

Stage 3: where

| where tostring(AdditionalFields) has 'ESAAttachmentDetails'

Stage 4: extend

| extend attachment = replace_string(tostring(extract(@'"ESAAttachmentDetails":"{(.*?):', 1, tostring(AdditionalFields))), "'", "")

Stage 5: where

| where attachment endswith '.ps1' or attachment endswith '.lnk' or attachment endswith '.exe'

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
attachmentextend
AccountCustomEntityextend

Dataverse - Hierarchy security manipulation

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

Identifies suspicious behaviors in hierarchy security including: - Hierarchy security disabled. - User assigns themselves as a manager. - User assigns themselves to a monitored position.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 2df0adf5-92a8-4ee0-a123-3eb5be1eed02
kind: Scheduled
name: Dataverse - Hierarchy security manipulation
description: |
  Identifies suspicious behaviors in hierarchy security including:
  - Hierarchy security disabled.
  - User assigns themselves as a manager.
  - User assigns themselves to a monitored position.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1548
  - T1078
query: |
  let monitored_position_ids = dynamic([
      // Enter a list of monitored position ID (guids)
      //"79380ac5-da2a-ed11-9db1-000d3a58d546"
      ]);
  let query_frequency = 1h;
  let security_disabled_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "Update" and EntityName == "organization"
      | mv-expand Fields
      | where Fields.Name == "ishierarchicalsecuritymodelenabled"
      | where Fields.Value == "False"
      | extend Message = "Hierarchy security has been disabled"
      | project TimeGenerated, UserId, ClientIp, InstanceUrl, Message;
  let assign_self_as_manager_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "Update" and EntityName == "systemuser"
      | mv-expand Fields
      | where Fields.Name == "parentsystemuserid"
      | extend ModifiedManager = tostring(Fields.Value)
      | where SystemUserId == ModifiedManager
      | extend Message = "User added self as manager of another user";
  let assign_self_to_position_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "Update" and EntityName == "systemuser"
      | mv-expand Position = Fields
      | where Position.Name == "positionid" and tostring(Position.Value) in (monitored_position_ids)
      | mv-expand Target = Fields
      | where Target.Name == "systemuserid"
      | extend UserAssigned = tostring(Target.Value)
      | where SystemUserId == UserAssigned
      | extend
          Message = "User assigned self to a monitored position",
          PositionId = tostring(Position.Value);
  union
      security_disabled_events,
      assign_self_as_manager_events,
      assign_self_to_position_events
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      TimeGenerated,
      UserId,
      ClientIp,
      InstanceUrl,
      Message,
      PositionId,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Suspicious hierarchy security modifications
    in {{InstanceUrl}} '
  alertDescriptionFormat: '{{Message}}. Events detected for user {{UserId}}.'
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 1h;

Let Binding: monitored_position_ids

let monitored_position_ids = dynamic([
    ]);

Union (3 Sources)

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

Leg 1: security_disabled_events

Leg 2: assign_self_as_manager_events

Leg 3: assign_self_to_position_events

Applied to the Combined Result

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1]) | project
    TimeGenerated,
    UserId,
    ClientIp,
    InstanceUrl,
    Message,
    PositionId,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EntityNameeq
  • organization corpus 3 (kusto 3)
  • systemuser
field:"EntityName" kind:eq
Fields.Nameeq
  • ishierarchicalsecuritymodelenabled transforms: kusto_dynamic_path
  • parentsystemuserid transforms: kusto_dynamic_path
field:"Fields.Name" kind:eq
Fields.Valueeq
  • False transforms: kusto_dynamic_path
field:"Fields.Value" kind:eq value:"False"
Messageeq
  • Update corpus 5 (kusto 5)
field:"Message" kind:eq value:"Update"
Position.Nameeq
  • positionid transforms: kusto_dynamic_path
field:"Position.Name" kind:eq value:"positionid"
Position.Valuein
  • [] transforms: tostring, kusto_dynamic_path
field:"Position.Value" kind:in value:"[]"
Target.Nameeq
  • systemuserid transforms: kusto_dynamic_path
field:"Target.Name" kind:eq value:"systemuserid"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
InstanceUrlproject
Messageproject
PositionIdproject
TimeGeneratedproject
UPNSuffixproject
UserIdproject

Dataverse - Honeypot instance activity

#
Status
available
Severity
medium
Time window
1d
Group by
ClientIp, InstanceUrl, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies activities in a predefined Honeypot Dataverse instance. Alerts when either sign-in to the Honeypot is detected or when monitored Dataverse tables in the Honeypot are accessed. Note: Requires a dedicated Honeypot Dataverse instance in Power Platform with auditing enabled.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 11650b85-d8cc-49c4-8c04-a8a739635983
kind: Scheduled
name: Dataverse - Honeypot instance activity
description: |
  Identifies activities in a predefined Honeypot Dataverse instance. Alerts when either sign-in to the Honeypot is detected or when monitored Dataverse tables in the Honeypot are accessed.

  Note: Requires a dedicated Honeypot Dataverse instance in Power Platform with auditing enabled.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - Exfiltration
relevantTechniques:
  - T1538
  - T1526
  - T1567
query: |
  let honeypot_dataverse_instances = dynamic(["https://myinstance.crm.dynamics.com/"]);
  let honeypot_authorized_users = dynamic(["scanner@mydomain.com"]);
  let monitored_dataverse_entities = dynamic(["contact", "account", "opportunity", "lead", "competitor"]);
  let query_frequency = 1h;
  DataverseActivity
  | where TimeGenerated >= ago(query_frequency)
  | where InstanceUrl in (honeypot_dataverse_instances)
  | where UserId !in (honeypot_authorized_users)
  | where UserId !endswith "@onmicrosoft.com"
      and UserId != "Unknown"
      and isnotempty(ClientIp)
  | where Message in ("UserSignIn") or EntityName in (monitored_dataverse_entities)
  | summarize
      TimeStart = min(TimeGenerated),
      TimeEnd = max(TimeGenerated),
      Entities = make_set(EntityName, 10),
      Messages = make_set(Message, 10)
      by UserId, ClientIp, InstanceUrl
  | extend Severity = iif(array_length(set_difference(Messages, dynamic(["UserSignIn"]))) > 0, "Medium", "Low")
  | extend CloudAppId = int(32780)
  | extend AccountName = tostring(split(UserId, '@')[0])
  | extend UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      TimeStart,
      TimeEnd,
      UserId,
      ClientIp,
      InstanceUrl,
      Messages,
      Entities,
      Severity,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Honeytoken activity detected in {{InstanceUrl}} '
  alertDescriptionFormat: '{{UserId}} from {{ClientIp}} was detected in the Dataverse
    honeypot instance: {{InstanceUrl}}'
  alertSeverityColumnName: Severity
version: 3.2.0

Stages and Predicates

Parameters

let honeypot_dataverse_instances = dynamic(["https://myinstance.crm.dynamics.com/"]);
let honeypot_authorized_users = dynamic(["scanner@mydomain.com"]);
let monitored_dataverse_entities = dynamic(["contact", "account", "opportunity", "lead", "competitor"]);
let query_frequency = 1h;

Stage 1: source

DataverseActivity

Stage 2: where

| where TimeGenerated >= ago(query_frequency)

Stage 3: where

| where InstanceUrl in (honeypot_dataverse_instances)

Stage 4: where

| where UserId !in (honeypot_authorized_users)

Stage 5: where

| where UserId !endswith "@onmicrosoft.com"
    and UserId != "Unknown"
    and isnotempty(ClientIp)

Stage 6: where

| where Message in ("UserSignIn") or EntityName in (monitored_dataverse_entities)

Stage 7: summarize

| summarize
    TimeStart = min(TimeGenerated),
    TimeEnd = max(TimeGenerated),
    Entities = make_set(EntityName, 10),
    Messages = make_set(Message, 10)
    by UserId, ClientIp, InstanceUrl

Stage 8: extend (4 consecutive steps)

| extend Severity = iif(array_length(set_difference(Messages, dynamic(["UserSignIn"]))) > 0, "Medium", "Low")
| extend CloudAppId = int(32780)
| extend AccountName = tostring(split(UserId, '@')[0])
| extend UPNSuffix = tostring(split(UserId, '@')[1])

Stage 9: project

| project
    TimeStart,
    TimeEnd,
    UserId,
    ClientIp,
    InstanceUrl,
    Messages,
    Entities,
    Severity,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientIpis_not_null
  • (no value, null check)
field:"ClientIp" kind:is_not_null
EntityNamein
  • account
  • competitor
  • contact
  • lead
  • opportunity
field:"EntityName" kind:in
InstanceUrlin
  • honeypot_dataverse_instances
field:"InstanceUrl" kind:in value:"honeypot_dataverse_instances"
Messagein
  • UserSignIn corpus 3 (kusto 3)
field:"Message" kind:in value:"UserSignIn"
UserIdne
  • Unknown corpus 2 (kusto 2)
field:"user" kind:ne value:"Unknown"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserIdeqhoneypot_authorized_usersexcludes:UserId field:"UserId" value:"honeypot_authorized_users"
UserIdends_with@onmicrosoft.comexcludes:UserId field:"UserId" value:"@onmicrosoft.com"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
Entitiesproject
InstanceUrlproject
Messagesproject
Severityproject
TimeEndproject
TimeStartproject
UPNSuffixproject
UserIdproject

Digital Guardian - Possible SMTP protocol abuse

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

'Detects possible SMTP protocol abuse.'

MITRE ATT&CK Coverage

Rule Body

id: a374a933-f6c4-4200-8682-70402a9054dd
name: Digital Guardian - Possible SMTP protocol abuse
description: |
  'Detects possible SMTP protocol abuse.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  DigitalGuardianDLPEvent
  | where NetworkApplicationProtocol =~ 'SMTP'
  | where DstPortNumber != 25
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

DigitalGuardianDLPEvent

Stage 2: where

| where NetworkApplicationProtocol =~ 'SMTP'

Stage 3: where

| where DstPortNumber != 25

Stage 4: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

GTI - Actively Exploited Vulnerability Detected

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

Detects vulnerabilities ingested from Google Threat Intelligence where the exploitation state is Confirmed or Wide, indicating the vulnerability is being actively exploited in the wild. Fires when such a record is ingested via the GTIVulnerabilities parser, prompting analyst triage and remediation.

MITRE ATT&CK Coverage

Rule Body

id: 4d284d59-1ce5-4ea7-8e97-52256b95aa10
name: GTI - Actively Exploited Vulnerability Detected
description: |
  Detects vulnerabilities ingested from Google Threat Intelligence where the exploitation state is
  Confirmed or Wide, indicating the vulnerability is being actively exploited in the wild. Fires when
  such a record is ingested via the GTIVulnerabilities parser, prompting analyst triage and remediation.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GTIVulnerabilitiesConnector
    dataTypes:
      - GTI_Vulnerabilities_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - Impact
relevantTechniques:
  - T1190
  - T1203
  - T1486
query: |
  GTIVulnerabilities
  | where ExploitationState in ("Confirmed", "Wide")
  | extend Cvssv3Score = coalesce(Cvssv3xBaseScore, Cvssv3xTranslatedBaseScore)
  | extend
      Vendor       = tostring(Cpes[0].start_cpe.vendor),
      Product      = tostring(Cpes[0].start_cpe.product),
      CpeVersion   = tostring(Cpes[0].start_cpe.version),
      VendorFixUrl = coalesce(tostring(VendorFixReferences[0].url), tostring(VendorFixReferences[0]))
  | mv-apply _c = Cpes on (
      where isnotnull(_c.start_cpe)
      | summarize AffectedVendors  = make_set(tostring(_c.start_cpe.vendor)),
                  AffectedProducts = make_set(tostring(_c.start_cpe.product))
    )
  | project
      TimeGenerated,
      CveId,
      VulnName,
      ExploitationState,
      ExploitationConsequence,
      ExploitReleaseDate,
      FirstExploitation,
      Cvssv3Score,
      Cvssv4xScore,
      EpssScore,
      EpssPercentile,
      Cvssv4xThreatExploitMaturity,
      RiskRating,
      Priority,
      RecentActivityRelativeChange,
      Vendor,
      Product,
      CpeVersion,
      AffectedVendors,
      AffectedProducts,
      Description,
      VendorFixUrl,
      VendorFixReferences,
      Tags,
      Type
alertDetailsOverride:
  alertDisplayNameFormat: 'GTI Actively Exploited: {{CveId}} ({{ExploitationState}})'
  alertDescriptionFormat: 'CVE: {{CveId}} | State: {{ExploitationState}} | Risk: {{RiskRating}}'
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: VendorFixUrl
customDetails:
  CveId: CveId
  ExploitState: ExploitationState
  ExploitConsequence: ExploitationConsequence
  Cvssv3Score: Cvssv3Score
  Cvssv4Score: Cvssv4xScore
  EpssScore: EpssScore
  EpssPercentile: EpssPercentile
  ExploitMaturity: Cvssv4xThreatExploitMaturity
  RiskRating: RiskRating
  Priority: Priority
  AffectedVendor: Vendor
  AffectedProduct: Product
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByCustomDetails:
      - CveId
eventGroupingSettings:
  aggregationKind: AlertPerResult
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GTIVulnerabilities

Stage 2: where

| where ExploitationState in ("Confirmed", "Wide")

Stage 3: extend

| extend Cvssv3Score = coalesce(Cvssv3xBaseScore, Cvssv3xTranslatedBaseScore)

Stage 4: extend

| extend
    Vendor       = tostring(Cpes[0].start_cpe.vendor),
    Product      = tostring(Cpes[0].start_cpe.product),
    CpeVersion   = tostring(Cpes[0].start_cpe.version),
    VendorFixUrl = coalesce(tostring(VendorFixReferences[0].url), tostring(VendorFixReferences[0]))

Stage 5: kusto:mv-apply

| mv-apply _c = Cpes on (
    where isnotnull(_c.start_cpe)
    | summarize AffectedVendors  = make_set(tostring(_c.start_cpe.vendor)),
                AffectedProducts = make_set(tostring(_c.start_cpe.product))
  )

Stage 6: project

| project
    TimeGenerated,
    CveId,
    VulnName,
    ExploitationState,
    ExploitationConsequence,
    ExploitReleaseDate,
    FirstExploitation,
    Cvssv3Score,
    Cvssv4xScore,
    EpssScore,
    EpssPercentile,
    Cvssv4xThreatExploitMaturity,
    RiskRating,
    Priority,
    RecentActivityRelativeChange,
    Vendor,
    Product,
    CpeVersion,
    AffectedVendors,
    AffectedProducts,
    Description,
    VendorFixUrl,
    VendorFixReferences,
    Tags,
    Type

Stage 7: summarize aggregation inside the mv-apply branch

summarize

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ExploitationStatein
  • Confirmed
  • Wide
field:"ExploitationState" kind:in
_c.start_cpeis_not_null
  • (no value, null check)
field:"_c.start_cpe" kind:is_not_null

Imperva - Request from unexpected IP address to admin panel

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

'Detects requests from unexpected IP addresses to admin panel.'

MITRE ATT&CK Coverage

Rule Body

id: 427c025d-c068-4844-8205-66879e89bcfa
name: Imperva - Request from unexpected IP address to admin panel
description: |
  'Detects requests from unexpected IP addresses to admin panel.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: ImpervaWAFCloudAPI
    dataTypes:
      - ImpervaWAFCloud
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  ImpervaWAFCloud
  | where QueryString contains @'/admin'
  | where ipv4_is_private(SrcIpAddr) == False
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

ImpervaWAFCloud

Stage 2: where

| where QueryString contains @'/admin'

Stage 3: where

| where ipv4_is_private(SrcIpAddr) == False

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddrcidr_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:SrcIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Microsoft Entra ID Health Monitoring Agent Registry Keys Access

#
Severity
medium
Time window
1d
Group by
Account, Computer, EventID, ObjectName, ObjectType, Process, ProcessName, SubjectDomainName, SubjectUserName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This detection uses Windows security events to detect suspicious access attempts to the registry key of Microsoft Entra ID Health monitoring agent. This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable object HKLM\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_monitoring_agent.yml

MITRE ATT&CK Coverage

TacticTechniques
Collection

Telemetry Coverage

Rule Body

id: f819c592-c5f9-4d5c-a79f-1e6819863533
name: Microsoft Entra ID Health Monitoring Agent Registry Keys Access
description: |
  'This detection uses Windows security events to detect suspicious access attempts to the registry key of Microsoft Entra ID Health monitoring agent.
  This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable object HKLM\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent.
  You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_monitoring_agent.yml
  '
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1005
tags:
  - SimuLand
query: |
  // ADHealth Monitoring Agent Registry Key
  let aadHealthMonAgentRegKey = "\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Microsoft Online\\Reporting\\MonitoringAgent";
  // Filter out known processes
  let aadConnectHealthProcs = dynamic ([
      'Microsoft.Identity.Health.Adfs.DiagnosticsAgent.exe',
      'Microsoft.Identity.Health.Adfs.InsightsService.exe',
      'Microsoft.Identity.Health.Adfs.MonitoringAgent.Startup.exe',
      'Microsoft.Identity.Health.Adfs.PshSurrogate.exe',
      'Microsoft.Identity.Health.Common.Clients.ResourceMonitor.exe',
      'Microsoft.Identity.Health.AadSync.MonitoringAgent.Startup.exe',
      'Microsoft.Identity.AadConnect.Health.AadSync.Host.exe',
      'Microsoft.Azure.ActiveDirectory.Synchronization.Upgrader.exe',
      'miiserver.exe'
  ]);
  (union isfuzzy=true
  (
  SecurityEvent
  | where EventID == '4656'
  | where EventData has aadHealthMonAgentRegKey
  | extend EventData = parse_xml(EventData).EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
  | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
  | extend ObjectName = column_ifexists("ObjectName", ""),
      ObjectType = column_ifexists("ObjectType", "")
  | where ObjectType == 'Key'
  | where ObjectName == aadHealthMonAgentRegKey
  | extend SubjectUserName = column_ifexists("SubjectUserName", ""),
      SubjectDomainName = column_ifexists("SubjectDomainName", ""),
      ProcessName = column_ifexists("ProcessName", "")
  | extend Process = split(ProcessName, '\\', -1)[-1],
      Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  | where Process !in (aadConnectHealthProcs)
  | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
  ),
  (
  WindowsEvent
  | where EventID == '4656' and EventData has aadHealthMonAgentRegKey
  | extend ObjectType = tostring(EventData.ObjectType)
  | where ObjectType == 'Key'
  | extend ObjectName = tostring(EventData.ObjectName)
  | where ObjectName == aadHealthMonAgentRegKey
  | extend ProcessName = tostring(EventData.ProcessName)
  | extend Process = tostring(split(ProcessName, '\\')[-1])
  | where Process !in (aadConnectHealthProcs)
  | extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend SubjectUserName = tostring(EventData.SubjectUserName)
  | extend SubjectDomainName = tostring(EventData.SubjectDomainName)
  | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
  ),
  (
  SecurityEvent
  | where EventID == '4663'
  | where ObjectType == 'Key'
  | where ObjectName == aadHealthMonAgentRegKey
  | extend Process = tostring(split(ProcessName, '\\', -1)[-1])
  | where Process !in (aadConnectHealthProcs)
  | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
  ),
    (
  WindowsEvent
  | where EventID == '4663' and EventData has aadHealthMonAgentRegKey
  | extend ObjectType = tostring(EventData.ObjectType)
  | where ObjectType == 'Key'
  | extend ObjectName = tostring(EventData.ObjectName)
  | where ObjectName == aadHealthMonAgentRegKey
  | extend ProcessName = tostring(EventData.ProcessName)
  | extend Process = tostring(split(ProcessName, '\\')[-1])
  | where Process !in (aadConnectHealthProcs)
  | extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend SubjectUserName = tostring(EventData.SubjectUserName)
  | extend SubjectDomainName = tostring(EventData.SubjectDomainName)
  | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
  )
  )
  // You can filter out potential machine accounts
  //| where AccountType != 'Machine'
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.1.6
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let aadHealthMonAgentRegKey = "\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Microsoft Online\\Reporting\\MonitoringAgent";

Let Binding: aadConnectHealthProcs

let aadConnectHealthProcs = dynamic ([
    'Microsoft.Identity.Health.Adfs.DiagnosticsAgent.exe',
    'Microsoft.Identity.Health.Adfs.InsightsService.exe',
    'Microsoft.Identity.Health.Adfs.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.Health.Adfs.PshSurrogate.exe',
    'Microsoft.Identity.Health.Common.Clients.ResourceMonitor.exe',
    'Microsoft.Identity.Health.AadSync.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.AadConnect.Health.AadSync.Host.exe',
    'Microsoft.Azure.ActiveDirectory.Synchronization.Upgrader.exe',
    'miiserver.exe'
]);

Union isfuzzy=true (4 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
| where EventID == '4656'
| where EventData has aadHealthMonAgentRegKey
| extend EventData = parse_xml(EventData).EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
| extend ObjectName = column_ifexists("ObjectName", ""),
    ObjectType = column_ifexists("ObjectType", "")
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend SubjectUserName = column_ifexists("SubjectUserName", ""),
    SubjectDomainName = column_ifexists("SubjectDomainName", ""),
    ProcessName = column_ifexists("ProcessName", "")
| extend Process = split(ProcessName, '\\', -1)[-1],
    Account = strcat(SubjectDomainName, "\\", SubjectUserName)
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName

Leg 2: WindowsEvent

WindowsEvent
| where EventID == '4656' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName

Leg 3: SecurityEvent

SecurityEvent
| where EventID == '4663'
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend Process = tostring(split(ProcessName, '\\', -1)[-1])
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName

Leg 4: WindowsEvent

WindowsEvent
| where EventID == '4663' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName

Applied to the Combined Result

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDatamatch
  • \REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent transforms: term
field:"EventData" kind:match value:"\REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent"
EventIDeq
  • 4656 corpus 4 (kusto 4)
  • 4663 corpus 3 (kusto 3)
field:"EventID" kind:eq
ObjectNameeq
  • \REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent
field:"ObjectName" kind:eq value:"\REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent"
ObjectTypeeq
  • Key
field:"ObjectType" kind:eq value:"Key"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ProcessinMicrosoft.Azure.ActiveDirectory.Synchronization.Upgrader.exe, Microsoft.Identity.AadConnect.Health.AadSync.Host.exe, Microsoft.Identity.Health.AadSync.MonitoringAgent.Startup.exe, Microsoft.Identity.Health.Adfs.DiagnosticsAgent.exe, Microsoft.Identity.Health.Adfs.InsightsService.exe, Microsoft.Identity.Health.Adfs.MonitoringAgent.Startup.exe, Microsoft.Identity.Health.Adfs.PshSurrogate.exe, Microsoft.Identity.Health.Common.Clients.ResourceMonitor.exe, miiserver.exeexcludes:Process

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
Computersummarize
EndTimesummarize
EventIDsummarize
ObjectNamesummarize
ObjectTypesummarize
Processsummarize
ProcessNamesummarize
StartTimesummarize
SubjectDomainNamesummarize
SubjectUserNamesummarize
HostNameextend
HostNameDomainextend
NTDomainextend
Nameextend

New EXE deployed via Default Domain or Default Domain Controller Policies (ASIM Version)

#
Severity
high
Time window
14d
Group by
CommandLine, DvcHostname, Process
Author
Microsoft Security Community
Source
github.com/Azure/Azure-Sentinel

This detection highlights executables deployed to hosts via either the Default Domain or Default Domain Controller Policies. These policies apply to all hosts or Domain Controllers and best practice is that these policies should not be used for deployment of files. A threat actor may use these policies to deploy files or scripts to all hosts in a domain. This query uses the ASIM parsers and will need them deployed before usage - https://docs.microsoft.com/azure/sentinel/normalization

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 0dd2a343-4bf9-4c93-a547-adf3658ddaec
name: New EXE deployed via Default Domain or Default Domain Controller Policies (ASIM Version)
description: |
  'This detection highlights executables deployed to hosts via either the Default Domain or Default Domain Controller Policies. These policies apply to all hosts or Domain Controllers and best practice is that these policies should not be used for deployment of files.
    A threat actor may use these policies to deploy files or scripts to all hosts in a domain.
    This query uses the ASIM parsers and will need them deployed before usage - https://docs.microsoft.com/azure/sentinel/normalization'
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvents
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - LateralMovement
relevantTechniques:
  - T1072
  - T1570
query: |
  let known_processes = (
    imProcess
    // Change these values if adjusting Query Frequency or Query Period
    | where TimeGenerated between(ago(14d)..ago(1d))
    | where Process has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
    | summarize by Process);
    imProcess
    // Change these values if adjusting Query Frequency or Query Period
    | where TimeGenerated > ago(1d)
    | where Process has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
    | where Process !in (known_processes)
    | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Process, CommandLine, DvcHostname
    | extend HostName = tostring(split(DvcHostname, ".")[0]), DomainIndex = toint(indexof(DvcHostname, '.'))
    | extend HostNameDomain = iff(DomainIndex != -1, substring(DvcHostname, DomainIndex + 1), DvcHostname)
    | project-away DomainIndex
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DvcHostname
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Community
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Let Binding: known_processes used in Stage 4

let known_processes = (
  imProcess
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where Process has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
  | summarize by Process);

Stage 1: source

imProcess

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where Process has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")

Stage 4: where

| where Process !in (known_processes)

Stage 5: summarize

| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Process, CommandLine, DvcHostname

Stage 6: extend

| extend HostName = tostring(split(DvcHostname, ".")[0]), DomainIndex = toint(indexof(DvcHostname, '.'))

Stage 7: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(DvcHostname, DomainIndex + 1), DvcHostname)
HostNameDomain =
ifDomainIndex != -1substring(DvcHostname, (DomainIndex + 1))
elseDvcHostname

Stage 8: project-away

| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Processmatch
  • Policies\{31B2F340-016D-11D2-945F-00C04FB984F9} transforms: term
  • Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9} transforms: term
field:"Process" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Processeqknown_processesexcludes:Process field:"Process" value:"known_processes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
DvcHostnamesummarize
FirstSeensummarize
LastSeensummarize
Processsummarize
HostNameextend
HostNameDomainextend

PIM Elevation Request Rejected

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

Identifies when a user is rejected for a privileged role elevation via PIM. Monitor rejections for indicators of attacker compromise of the requesting account. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-identity-management

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 7d7e20f8-3384-4b71-811c-f5e950e8306c
name: PIM Elevation Request Rejected
description: |
  'Identifies when a user is rejected for a privileged role elevation via PIM. Monitor rejections for indicators of attacker compromise of the requesting account.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-identity-management'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Persistence
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  AuditLogs
  | where ActivityDisplayName =~'Add member to role request denied (PIM activation)'
  | mv-apply ResourceItem = TargetResources on 
    (
        where ResourceItem.type =~ "Role"
        | extend Role = trim(@'"',tostring(ResourceItem.displayName))
    )
  | mv-apply ResourceItem = TargetResources on 
    (
        where ResourceItem.type =~ "User"
        | extend TargetUserPrincipalName = trim(@'"',tostring(ResourceItem.userPrincipalName))
    )
  | where isnotempty(InitiatedBy.user)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
  | project-reorder TimeGenerated, TargetUserPrincipalName, Role, OperationName, Result, ResultDescription
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName    
      - identifier: Name
        columnName: InitiatedByName      
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserPrincipalName   
      - identifier: Name
        columnName: TargetName      
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.8
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where ActivityDisplayName =~'Add member to role request denied (PIM activation)'

Stage 3: kusto:mv-apply

| mv-apply ResourceItem = TargetResources on 
  (
      where ResourceItem.type =~ "Role"
      | extend Role = trim(@'"',tostring(ResourceItem.displayName))
  )

Stage 4: kusto:mv-apply

| mv-apply ResourceItem = TargetResources on 
  (
      where ResourceItem.type =~ "User"
      | extend TargetUserPrincipalName = trim(@'"',tostring(ResourceItem.userPrincipalName))
  )

Stage 5: where

| where isnotempty(InitiatedBy.user)

Stage 6: extend (7 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Stage 7: project-reorder

| project-reorder TimeGenerated, TargetUserPrincipalName, Role, OperationName, Result, ResultDescription

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityDisplayNameeq
  • Add member to role request denied (PIM activation) corpus 3 (kusto 3)
field:"azure_ad::activity_display_name" kind:eq value:"Add member to role request denied (PIM activation)"
InitiatedBy.useris_not_null
  • (no value, null check)
field:"InitiatedBy.user" kind:is_not_null
ResourceItem.typeeq
  • Role transforms: kusto_dynamic_path
  • User transforms: kusto_dynamic_path
field:"ResourceItem.type" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
TargetNameextend
TargetUPNSuffixextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

SharePointFileOperation via previously unseen IPs

#
Status
available
Severity
medium
Time window
14d
Group by
ClientIP, Operation, Site_Url, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies anomalies using user behavior by setting a threshold for significant changes in file upload/download activities from new IP addresses. It establishes a baseline of typical behavior, compares it to recent activity, and flags deviations exceeding a default threshold of 25.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 4b11568b-3f5f-4ba1-80c8-7f1dc8390eb7
name: SharePointFileOperation via previously unseen IPs
description: |
  'Identifies anomalies using user behavior by setting a threshold for significant changes in file upload/download activities from new IP addresses. It establishes a baseline of typical behavior, compares it to recent activity, and flags deviations exceeding a default threshold of 25.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1030
query: |
  // Define a threshold for significant deviations
  let threshold = 25;
  // Define the name for the SharePoint File Operation record type
  let szSharePointFileOperation = "SharePointFileOperation";
  // Define an array of SharePoint operations of interest
  let szOperations = dynamic(["FileDownloaded", "FileUploaded"]);
  // Define the start and end time for the analysis period
  let starttime = 14d;
  let endtime = 1d;
  // Define a baseline of normal user behavior
  let userBaseline = OfficeActivity
  | where TimeGenerated between(ago(starttime)..ago(endtime))
  | where RecordType =~ szSharePointFileOperation
  | where Operation in~ (szOperations)
  | where isnotempty(UserAgent)
  | summarize Count = count() by UserId, Operation, Site_Url, ClientIP
  | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url, ClientIP;
  // Get recent user activity
  let recentUserActivity = OfficeActivity
  | where TimeGenerated > ago(endtime)
  | where RecordType =~ szSharePointFileOperation
  | where Operation in~ (szOperations)
  | where isnotempty(UserAgent)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by UserId, UserType, Operation, Site_Url, ClientIP, OfficeObjectId, OfficeWorkload, UserAgent;
  // Join the baseline and recent activity, and calculate the deviation
  let UserBehaviorAnalysis = userBaseline | join kind=inner (recentUserActivity) on UserId, Operation, Site_Url, ClientIP
  | extend Deviation = abs(RecentCount - AvgCount) / AvgCount;
  // Filter for significant deviations
  UserBehaviorAnalysis
  | where Deviation > threshold
  | project StartTimeUtc, EndTimeUtc, UserId, UserType, Operation, ClientIP, Site_Url, OfficeObjectId, OfficeWorkload, UserAgent, Deviation, Count=RecentCount
  | order by Count desc, ClientIP asc, Operation asc, UserId asc
  | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Site_Url
version: 2.0.4
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 25;
let szSharePointFileOperation = "SharePointFileOperation";
let szOperations = dynamic(["FileDownloaded", "FileUploaded"]);
let starttime = 14d;
let endtime = 1d;

let userBaseline and let UserBehaviorAnalysis are inlined into the numbered stages below.

Let Binding: recentUserActivity used in Stage 8

let recentUserActivity = OfficeActivity
| where TimeGenerated > ago(endtime)
| where RecordType =~ szSharePointFileOperation
| where Operation in~ (szOperations)
| where isnotempty(UserAgent)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by UserId, UserType, Operation, Site_Url, ClientIP, OfficeObjectId, OfficeWorkload, UserAgent;

Stages 1 to 9 define let UserBehaviorAnalysis as the rule's main pipeline source. Stages 10 to 13 use it.

Stage 1: source

OfficeActivity

Stage 2: where

where TimeGenerated between (ago(1209600s) .. ago(86400s))

Stage 3: where

where RecordType =~ "SharePointFileOperation"

Stage 4: where

where Operation in~ ("FileDownloaded", "FileUploaded")

Stage 5: where

where isnotempty(UserAgent)

Stage 6: summarize

summarize Count by UserId, Operation, Site_Url, ClientIP

Stage 7: summarize

summarize AvgCount by UserId, Operation, Site_Url, ClientIP

Stage 8: join

| join kind=inner (recentUserActivity) on UserId, Operation, Site_Url, ClientIP

Stage 9: extend

extend Deviation

Stage 10: where

where Deviation > 25

Stage 11: project

project ClientIP, Count, Deviation, EndTimeUtc, OfficeObjectId, OfficeWorkload, Operation, Site_Url, StartTimeUtc, UserAgent, UserId, UserType

Stage 12: sort

sort by ClientIP, Count, Operation, UserId

Stage 13: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Deviationgt
  • 25 transforms: number
field:"Deviation" kind:gt value:"25"
Operationin
  • FileDownloaded corpus 2 (kusto 2)
  • FileUploaded corpus 2 (kusto 2)
field:"Operation" kind:in
RecordTypeeq
  • SharePointFileOperation corpus 2 (kusto 2)
field:"RecordType" kind:eq value:"SharePointFileOperation"
UserAgentis_not_null
  • (no value, null check)
field:"UserAgent" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPproject
Countproject
Deviationproject
EndTimeUtcproject
OfficeObjectIdproject
OfficeWorkloadproject
Operationproject
Site_Urlproject
StartTimeUtcproject
UserAgentproject
UserIdproject
UserTypeproject
AccountNameextend
AccountUPNSuffixextend

SSH - Potential Brute Force

#
Severity
low
Time window
4h
Group by
Computer, IPAddress, _ResourceId, ip, user
Source
github.com/Azure/Azure-Sentinel

'Identifies an IP address that had 15 failed attempts to sign in via SSH in a 4 hour block during a 24 hour time period. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur. As an example - ComputerList is an array that we check for a single value and write that into the HostName field for use in the entity mapping within Sentinel.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: e1ce0eab-10d1-4aae-863f-9a383345ba88
name: SSH - Potential Brute Force
description: |
  'Identifies an IP address that had 15 failed attempts to sign in via SSH in a 4 hour block during a 24 hour time period.
   Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.
   As an example - ComputerList is an array that we check for a single value and write that into the HostName field for use in the entity mapping within Sentinel.'
severity: Low
requiredDataConnectors:
  - connectorId: Syslog
    dataTypes:
      - Syslog
  - connectorId: SyslogAma
    dataTypes:
      - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let threshold = 15;
  Syslog
  | where ProcessName =~ "sshd"
  | where SyslogMessage contains "Failed password for invalid user"
  | parse kind=relaxed SyslogMessage with * "invalid user " user " from " ip " port" port " ssh2" *
  // using distinct below as it has been seen that Syslog can duplicate entries depending on implementation
  | distinct TimeGenerated, Computer, user, ip, port, SyslogMessage, _ResourceId
  | summarize EventTimes = make_list(TimeGenerated), PerHourCount = count() by bin(TimeGenerated,4h), ip, Computer, user, _ResourceId
  | where PerHourCount > threshold
  | mvexpand EventTimes
  | extend EventTimes = tostring(EventTimes)
  | summarize StartTime = min(EventTimes), EndTime = max(EventTimes), UserList = make_set(user), ComputerList = make_set(Computer), ResourceIdList = make_set(_ResourceId), sum(PerHourCount) by IPAddress = ip
  // bringing through single computer and user if array only has 1, otherwise, referencing the column and hashing the ComputerList or UserList so we don't get accidental entity matches when reviewing alerts
  | extend HostName = iff(array_length(ComputerList) == 1, tostring(ComputerList[0]), strcat("SeeComputerListField","_", tostring(hash(tostring(ComputerList)))))
  | extend Account = iff(array_length(ComputerList) == 1, tostring(UserList[0]), strcat("SeeUserListField","_", tostring(hash(tostring(UserList)))))
  | extend ResourceId = iff(array_length(ResourceIdList) == 1, tostring(ResourceIdList[0]), strcat("SeeResourceIdListField","_", tostring(hash(tostring(ResourceIdList)))))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Account
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
version: 1.1.5
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 15;

Stage 1: source

Syslog

Stage 2: where

| where ProcessName =~ "sshd"

Stage 3: where

| where SyslogMessage contains "Failed password for invalid user"

Stage 4: parse

| parse kind=relaxed SyslogMessage with * "invalid user " user " from " ip " port" port " ssh2" *

Stage 5: distinct

| distinct TimeGenerated, Computer, user, ip, port, SyslogMessage, _ResourceId

Stage 6: summarize

| summarize EventTimes = make_list(TimeGenerated), PerHourCount = count() by bin(TimeGenerated,4h), ip, Computer, user, _ResourceId

Stage 7: where

| where PerHourCount > threshold

Stage 8: mv-expand

| mvexpand EventTimes

Stage 9: extend

| extend EventTimes = tostring(EventTimes)

Stage 10: summarize

| summarize StartTime = min(EventTimes), EndTime = max(EventTimes), UserList = make_set(user), ComputerList = make_set(Computer), ResourceIdList = make_set(_ResourceId), sum(PerHourCount) by IPAddress = ip

Stage 11: extend (3 consecutive steps)

| extend HostName = iff(array_length(ComputerList) == 1, tostring(ComputerList[0]), strcat("SeeComputerListField","_", tostring(hash(tostring(ComputerList)))))
| extend Account = iff(array_length(ComputerList) == 1, tostring(UserList[0]), strcat("SeeUserListField","_", tostring(hash(tostring(UserList)))))
| extend ResourceId = iff(array_length(ResourceIdList) == 1, tostring(ResourceIdList[0]), strcat("SeeResourceIdListField","_", tostring(hash(tostring(ResourceIdList)))))
HostName =
ifComputerList == 1tostring(ComputerList[0])
elsestrcat("SeeComputerListField", "_", tostring(hash(tostring(ComputerList))))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ComputerListsummarize
EndTimesummarize
IPAddresssummarize
ResourceIdListsummarize
StartTimesummarize
UserListsummarize
HostNameextend
Accountextend
ResourceIdextend

Theom - National IDs unencrypted

#

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

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

"Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0002 (National IDs have been observed in unencrypted data stores. Encrypt data at rest to comply with this CIS requirement)"

MITRE ATT&CK Coverage

Rule Body

id: a655f6d1-4ffa-4bc9-8b5d-2ec31cad09d4
name: Theom - National IDs unencrypted
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0002 (National IDs have been observed in unencrypted data stores. Encrypt data at rest to comply with this CIS requirement)"
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
  - T1530
query: |
   TheomAlerts_CL
     | where customProps_RuleId_s == "TRIS0002" and (priority_s == "P1" or priority_s == "P2")
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings: 
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where customProps_RuleId_s == "TRIS0002" and (priority_s == "P1" or priority_s == "P2")

Indicators

These rows show field, operator, and value matches.

Ubiquiti - RDP from external source

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

'Detects remote to local (R2L) RDP connection.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 95d5ca9b-72c5-4b80-ad5c-b6401cdc5e08
name: Ubiquiti - RDP from external source
description: |
  'Detects remote to local (R2L) RDP connection.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1133
query: |
  UbiquitiAuditEvent
  | where EventCategory =~ 'firewall'
  | where ipv4_is_private(SrcIpAddr) == 'False'
  | where ipv4_is_private(DstIpAddr)
  | where DstPortNumber == '3389'
  | extend IPCustomEntity = DstIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

UbiquitiAuditEvent

Stage 2: where

| where EventCategory =~ 'firewall'

Stage 3: where

| where ipv4_is_private(SrcIpAddr) == 'False'

Stage 4: where

| where ipv4_is_private(DstIpAddr)

Stage 5: where

| where DstPortNumber == '3389'

Stage 6: extend

| extend IPCustomEntity = DstIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DstIpAddrcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"dest_ip" kind:cidr_match
DstPortNumbereq
  • 3389
field:"DstPortNumber" kind:eq value:"3389"
EventCategoryeq
  • firewall
field:"EventCategory" kind:eq value:"firewall"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddrcidr_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:SrcIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Back to all Kusto rules