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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0AddSecret" |
Cisco SEG - Unexpected attachment
#'Detects possibly malicious attachments.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalFields | match |
| field:"AdditionalFields" kind:match value:"ESAAttachmentDetails" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"Incoming" |
attachment | ends_with |
| field:"attachment" kind:ends_with |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
attachment | extend |
AccountCustomEntity | extend |
Dataverse - Hierarchy security manipulation
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EntityName | eq |
| field:"EntityName" kind:eq |
Fields.Name | eq |
| field:"Fields.Name" kind:eq |
Fields.Value | eq |
| field:"Fields.Value" kind:eq value:"False" |
Message | eq |
| field:"Message" kind:eq value:"Update" |
Position.Name | eq |
| field:"Position.Name" kind:eq value:"positionid" |
Position.Value | in |
| field:"Position.Value" kind:in value:"[]" |
Target.Name | eq |
| field:"Target.Name" kind:eq value:"systemuserid" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
InstanceUrl | project |
Message | project |
PositionId | project |
TimeGenerated | project |
UPNSuffix | project |
UserId | project |
Dataverse - Honeypot instance activity
#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
| Tactic | Techniques |
|---|---|
| Discovery | |
| Exfiltration |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIp | is_not_null | field:"ClientIp" kind:is_not_null | |
EntityName | in |
| field:"EntityName" kind:in |
InstanceUrl | in |
| field:"InstanceUrl" kind:in value:"honeypot_dataverse_instances" |
Message | in |
| field:"Message" kind:in value:"UserSignIn" |
UserId | ne |
| field:"user" kind:ne value:"Unknown" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserId | eq | honeypot_authorized_users | excludes:UserId field:"UserId" value:"honeypot_authorized_users" |
UserId | ends_with | @onmicrosoft.com | excludes:UserId field:"UserId" value:"@onmicrosoft.com" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
Entities | project |
InstanceUrl | project |
Messages | project |
Severity | project |
TimeEnd | project |
TimeStart | project |
UPNSuffix | project |
UserId | project |
Digital Guardian - Possible SMTP protocol abuse
#'Detects possible SMTP protocol abuse.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
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.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
GTI - Actively Exploited Vulnerability Detected
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Impact |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ExploitationState | in |
| field:"ExploitationState" kind:in |
_c.start_cpe | is_not_null | field:"_c.start_cpe" kind:is_not_null |
Imperva - Request from unexpected IP address to admin panel
#'Detects requests from unexpected IP addresses to admin panel.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
QueryString | contains |
| field:"QueryString" kind:contains value:"/admin" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:SrcIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
Microsoft Entra ID Health Monitoring Agent Registry Keys Access
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | match |
| field:"EventData" kind:match value:"\REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent" |
EventID | eq |
| field:"EventID" kind:eq |
ObjectName | eq |
| field:"ObjectName" kind:eq value:"\REGISTRY\MACHINE\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent" |
ObjectType | eq |
| field:"ObjectType" kind:eq value:"Key" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Process | in | Microsoft.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.exe | excludes:Process |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | summarize |
Computer | summarize |
EndTime | summarize |
EventID | summarize |
ObjectName | summarize |
ObjectType | summarize |
Process | summarize |
ProcessName | summarize |
StartTime | summarize |
SubjectDomainName | summarize |
SubjectUserName | summarize |
HostName | extend |
HostNameDomain | extend |
NTDomain | extend |
Name | extend |
New EXE deployed via Default Domain or Default Domain Controller Policies (ASIM Version)
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Lateral Movement |
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
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 =DomainIndex != -1substring(DvcHostname, (DomainIndex + 1))DvcHostnameStage 8: project-away
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Process | match |
| field:"Process" kind:match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Process | eq | known_processes | excludes:Process field:"Process" value:"known_processes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CommandLine | summarize |
DvcHostname | summarize |
FirstSeen | summarize |
LastSeen | summarize |
Process | summarize |
HostName | extend |
HostNameDomain | extend |
PIM Elevation Request Rejected
#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
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add member to role request denied (PIM activation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | eq |
| field:"azure_ad::activity_display_name" kind:eq value:"Add member to role request denied (PIM activation)" |
InitiatedBy.user | is_not_null | field:"InitiatedBy.user" kind:is_not_null | |
ResourceItem.type | eq |
| field:"ResourceItem.type" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
TargetName | extend |
TargetUPNSuffix | extend |
InitiatedByName | extend |
InitiatedByUPNSuffix | extend |
SSH - Potential Brute Force
#'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
| Tactic | Techniques |
|---|---|
| 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 =ComputerList == 1tostring(ComputerList[0])strcat("SeeComputerListField", "_", tostring(hash(tostring(ComputerList))))Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PerHourCount | gt |
| field:"PerHourCount" kind:gt value:"15" |
ProcessName | eq |
| field:"process_name" kind:eq value:"sshd" |
SyslogMessage | contains |
| field:"SyslogMessage" kind:contains value:"Failed password for invalid user" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ComputerList | summarize |
EndTime | summarize |
IPAddress | summarize |
ResourceIdList | summarize |
StartTime | summarize |
UserList | summarize |
HostName | extend |
Account | extend |
ResourceId | extend |
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.
"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
| Tactic | Techniques |
|---|---|
| Collection |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
customProps_RuleId_s | eq |
| field:"customProps_RuleId_s" kind:eq value:"TRIS0002" |
priority_s | eq |
| field:"priority_s" kind:eq |
Ubiquiti - RDP from external source
#'Detects remote to local (R2L) RDP connection.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DstIpAddr | cidr_match |
| field:"dest_ip" kind:cidr_match |
DstPortNumber | eq |
| field:"DstPortNumber" kind:eq value:"3389" |
EventCategory | eq |
| field:"EventCategory" kind:eq value:"firewall" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:SrcIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |