Detection rules › Kusto

Insider Risk_High User Security Incidents Correlation

Severity
high
Time window
7d
Group by
AlertId, IncidentName, IncidentNumber, IncidentUrl, ProductName, Severity, SystemAlertId, UPN
Source
github.com/Azure/Azure-Sentinel

'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see Investigate incidents with Microsoft Sentinel.'

MITRE ATT&CK coverage

TacticTechniques
Execution

Rule body

id: 28a75d10-9b75-4192-9863-e452c3ad24db
name: Insider Risk_High User Security Incidents Correlation
description: |
  'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see [Investigate incidents with Microsoft Sentinel]( https://docs.microsoft.com/azure/sentinel/investigate-cases).'
severity: High
requiredDataConnectors:
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert (MDATP)
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert (ASC)
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert (ASC for IoT)
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (MCAS)
  - connectorId: OfficeATP
    dataTypes:
      - SecurityAlert (Office 365)
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  let PreviousIncidents =
  SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | 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 PreviousIncidents = make_set(IncidentNumber) by UPN;
  let LastTimeObserved =
  SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | 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 arg_max(TimeGenerated, IncidentName) by UPN;
    SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | 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 count() by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName
  | extend SecurityIncidents = count_
  | where SecurityIncidents > 5 //Adjust & Tune within Organzational Requirements
  | join (LastTimeObserved) on UPN
  | project-rename LastObserved = TimeGenerated, LastIncident = IncidentNumber
  | project-away IncidentName, count_, UPN1, IncidentName1, Severity, IncidentUrl
  | join kind=inner (PreviousIncidents) on UPN
  // | lookup kind=inner _GetWatchlist('<Your Watchlist Name>') on $left.UPN == $right.SearchKey
  | project UPN, SecurityIncidents, LastIncident, ProductName, LastObserved, PreviousIncidents
  | sort by SecurityIncidents desc
  | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1])
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UPN
      - 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.2
kind: Scheduled

Stages and Predicates

Stage 0: let

let PreviousIncidents =
SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId
| 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 PreviousIncidents = make_set(IncidentNumber) by UPN;
let LastTimeObserved =
SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId
| 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 arg_max(TimeGenerated, IncidentName) by UPN;

Stage 1: source

SecurityIncident

Stage 2: summarize

| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber

Stage 3: mv-expand

| mv-expand AlertIds

Stage 4: extend

| extend AlertId = tostring(AlertIds)

Stage 5: join

| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId

Stage 6: summarize

summarize NumberOfUpdates by SystemAlertId

Stage 7: mv-expand

mv-expand

Stage 8: where

where Type =~ "account"

Stage 9: extend (3 consecutive steps)

extend AadTenantId, AadUserId, Host, Href_, IsDomainJoined, NTDomain, Name, Sid, UPN, UPNSuffix

Stage 10: where

where UPN !~ ""

Stage 11: summarize

summarize by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName

Stage 12: extend

extend SecurityIncidents

Stage 13: where

where SecurityIncidents > 5

Stage 14: join

join (LastTimeObserved) on UPN

Stage 15: project-rename

project-rename

Stage 16: project-away

project-away IncidentName, IncidentName1, IncidentUrl, Severity, UPN1, count_

Stage 17: join

join kind=inner (PreviousIncidents) on UPN

Stage 18: project

project LastIncident, LastObserved, PreviousIncidents, ProductName, SecurityIncidents, UPN

Stage 19: sort

sort by SecurityIncidents

Stage 20: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
LastIncidentproject
LastObservedproject
PreviousIncidentsproject
ProductNameproject
SecurityIncidentsproject
UPNproject
AccountNameextend
AccountUPNSuffixextend