Detection rules › Kusto

Insider Risk_Microsoft Purview Insider Risk Management Alert Observed

Severity
high
Time window
6h
Group by
SystemAlertId
Source
github.com/Azure/Azure-Sentinel

'This alert is triggered when a Microsoft Purview Insider Risk Management alert is recieved in Microsoft Sentinel via the Microsoft Purview Insider Risk Management Connector. The alert extracts usernames from security alerts to provide UserPrincipalName, Alert Name, Reporting Product Name, Status, Alert Link, Previous Alerts Links, Time Generated. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see Learn about insider risk management'

MITRE ATT&CK coverage

TacticTechniques
ExecutionT1204 User Execution

Rule body kusto

id: 69660e65-0e5c-4700-8b99-5caf59786606
name: Insider Risk_Microsoft Purview Insider Risk Management Alert Observed
description: |
  'This alert is triggered when a Microsoft Purview Insider Risk Management alert is recieved in Microsoft Sentinel via the Microsoft Purview Insider Risk Management Connector. The alert extracts usernames from security alerts to provide UserPrincipalName, Alert Name, Reporting Product Name, Status, Alert Link, Previous Alerts Links, Time Generated. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see [Learn about insider risk management](https://docs.microsoft.com/microsoft-365/compliance/insider-risk-management)'
severity: High
requiredDataConnectors:
  - connectorId: OfficeATP
    dataTypes:
      - SecurityAlert (Office 365)
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  let AlertLinks = SecurityAlert
  | summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
  | mv-expand todynamic(Entities)
  | where Entities["Type"] =~ "account"
  | extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
            Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
  | extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
  | extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
  | where UPN <> ""
  | summarize PreciousSecurityAlertLinks=make_set(AlertLink) by UPN;
  SecurityAlert
  | where ProductName == "Microsoft 365 Insider Risk Management"
      | summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
      | mv-expand todynamic(Entities)
      | where Entities["Type"] =~ "account"
      | extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
          Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
      | extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
  | extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
  | join kind=inner (AlertLinks) on UPN
  | extend UserPrincipalName = UPN
  | extend PreviousAlertsLinks = strcat(PreciousSecurityAlertLinks)
  // | lookup kind=inner _GetWatchlist('<Your Watchlist Name>') on $left.UserPrincipalName == $right.SearchKey
  | project UserPrincipalName, AlertName, ProductName, Tactics, Status, AlertLink, PreviousAlertsLinks, TimeGenerated
  | sort by TimeGenerated desc
  | extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 3d
    matchingMethod: Selected
    groupByEntities:
      - Account
version: 1.1.3
kind: Scheduled

Stages and Predicates

Let binding: AlertLinks

let AlertLinks = SecurityAlert
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
          Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize PreciousSecurityAlertLinks=make_set(AlertLink) by UPN;

Stage 1: source

SecurityAlert

Stage 2: where

| where ProductName == "Microsoft 365 Insider Risk Management"

Stage 3: summarize

| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId

Stage 4: mv-expand

| mv-expand todynamic(Entities)

Stage 5: where

| where Entities["Type"] =~ "account"

Stage 6: extend (3 consecutive steps)

| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
        Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)

Stage 7: join

| join kind=inner (AlertLinks) on UPN

Stage 8: extend

| extend UserPrincipalName = UPN

Stage 9: extend

| extend PreviousAlertsLinks = strcat(PreciousSecurityAlertLinks)

Stage 10: project

| project UserPrincipalName, AlertName, ProductName, Tactics, Status, AlertLink, PreviousAlertsLinks, TimeGenerated

Stage 11: sort

| sort by TimeGenerated desc

Stage 12: extend

| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ProductNameeq
  • Microsoft 365 Insider Risk Management transforms: cased
Typeeq
  • account

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AlertLinkproject
AlertNameproject
PreviousAlertsLinksproject
ProductNameproject
Statusproject
Tacticsproject
TimeGeneratedproject
UserPrincipalNameproject
AccountNameextend
AccountUPNSuffixextend