Detection rules › Kusto

Kusto rules: 1e

Azure secure score admin MFA

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

'This query searches for requiring multi-factor authentication (MFA) for all administrative roles makes it harder for attackers to access accounts. Administrative roles have higher permissions than typical users. If any of those accounts are compromised, critical devices and data is open to attack.'

MITRE ATT&CK Coverage

Rule Body

id: 9a15c3dd-f72b-49a4-bcb7-94406395661e
name: Azure secure score admin MFA
description: |
  'This query searches for requiring multi-factor authentication (MFA) for all administrative roles makes it harder for attackers to access accounts. Administrative roles have higher permissions than typical users. If any of those accounts are compromised, critical devices and data is open to attack.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SenservaPro
    dataTypes:
      - SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1529
  - T1498
query: |
  SenservaPro_CL
  | where ControlName_s == 'AzureSecureScoreAdminMFAV2'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ControlName_s
      - identifier: AadTenantId
        columnName: TenantId
      - identifier: DisplayName
        columnName: TenantDisplayName_s 
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: Group_s
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourceSystem
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

SenservaPro_CL

Stage 2: where

| where ControlName_s == 'AzureSecureScoreAdminMFAV2'

Indicators

These rows show field, operator, and value matches.

Claroty - Policy violation

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

'Detects Claroty policy violation events from ClarotyEvent when EventOriginalType or EventType contains 'Policy Violation'. Use this rule to identify policy enforcement events that may indicate unauthorized discovery or prohibited network activity. This rule expects ClarotyEvent data to be available in the workspace.'

MITRE ATT&CK Coverage

Rule Body

id: 3b22ac47-e02c-4599-a37a-57f965de17be
name: Claroty - Policy violation
description: |
  'Detects Claroty policy violation events from ClarotyEvent when EventOriginalType or EventType contains 'Policy
  Violation'. Use this rule to identify policy enforcement events that may indicate unauthorized discovery or prohibited network
  activity. This rule expects ClarotyEvent data to be available in the workspace.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1018
  - T1135
query: |
  ClarotyEvent
    | where EventOriginalType has 'Policy Violation' or EventType has 'Policy Violation'
    | project TimeGenerated, DstIpAddr, EventOriginalType, EventType
    | extend IPCustomEntity = DstIpAddr
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPCustomEntity
alertDetailsOverride:
  alertDisplayNameFormat: Claroty policy violation detected for {{IPCustomEntity}}
  alertDescriptionFormat: 'Claroty reported a policy violation for {{IPCustomEntity}}. EventOriginalType: {{EventOriginalType}}.
    EventType: {{EventType}}'
customDetails:
  EventOriginalType: EventOriginalType
  EventType: EventType
  DestinationIP: DstIpAddr
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

ClarotyEvent

Stage 2: where

| where EventOriginalType has 'Policy Violation' or EventType has 'Policy Violation'

Stage 3: project

| project TimeGenerated, DstIpAddr, EventOriginalType, EventType

Stage 4: extend

| extend IPCustomEntity = DstIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventOriginalTypematch
  • Policy Violation transforms: term
field:"EventOriginalType" kind:match value:"Policy Violation"
EventTypematch
  • Policy Violation transforms: term
field:"EventType" kind:match value:"Policy Violation"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstIpAddrproject
EventOriginalTypeproject
EventTypeproject
TimeGeneratedproject
IPCustomEntityextend

Cloudflare - Unexpected URI

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

'Detects client requests to unusual URI.'

MITRE ATT&CK Coverage

Rule Body

id: dcb797cd-a4cd-4306-897b-7991f71d7e27
name: Cloudflare - Unexpected URI
description: |
  'Detects client requests to unusual URI.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  Cloudflare
  | where HttpRequestMethod =~ 'GET'
  | where DstBytes != 0 or SrcBytes != 0
  | where ClientRequestURI matches regex @'(127\.\d{1,3}\.\d{1,3}\.\d{1,3})|(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.1[6-9]\.\d{1,3}\.\d{1,3})|(172\.2[0-9]\.\d{1,3}\.\d{1,3})|(172\.3[0-1]\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3})'
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

Cloudflare

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: where

| where DstBytes != 0 or SrcBytes != 0

Stage 4: where

| where ClientRequestURI matches regex @'(127\.\d{1,3}\.\d{1,3}\.\d{1,3})|(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.1[6-9]\.\d{1,3}\.\d{1,3})|(172\.2[0-9]\.\d{1,3}\.\d{1,3})|(172\.3[0-1]\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3})'

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientRequestURIregex_match
    • (127.\d{1,3}.\d{1,3}.\d{1,3})
    • (10.\d{1,3}.\d{1,3}.\d{1,3})
    • (172.1[6-9].\d{1,3}.\d{1,3})
    • (172.2[0-9].\d{1,3}.\d{1,3})
    • (172.3[0-1].\d{1,3}.\d{1,3})
    • (192.168.\d{1,3}.\d{1,3})
field:"ClientRequestURI" kind:regex_match
DstBytesne
  • 0
field:"DstBytes" kind:ne value:"0"
HttpRequestMethodeq
  • GET
field:"HttpRequestMethod" kind:eq value:"GET"
SrcBytesne
  • 0
field:"SrcBytes" kind:ne value:"0"

Create Incidents from IronDefense

#
Status
available
Severity
medium
Time window
5m
Group by
DeviceCustomString2, DeviceCustomString4
Source
github.com/Azure/Azure-Sentinel

'Creates incidents based on behavioral detections from IronDefense.'

Rule Body

id: 3cf46cb9-99d5-42ee-a43c-7bd88ea394a1
name: Create Incidents from IronDefense
description: |
  'Creates incidents based on behavioral detections from IronDefense.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: IronNetIronDefense
    dataTypes:
      - CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
relevantTechniques:
query: |
  CommonSecurityLog
  | where DeviceProduct == "IronDefense"
  | summarize arg_max(TimeGenerated, *) by DeviceCustomString4, DeviceCustomString2
  | extend Category = coalesce(column_ifexists("DeviceEventCategory",""),extract(@'cat=([^;]+)(\;|$)', 1, AdditionalExtensions))	
  | extend Subcategory = extract(@'subcat=([^;]+)(\;|$)', 1, AdditionalExtensions)
  | extend AlertDescription = strcat(Category, ': ', Subcategory)
  | extend Status = tostring(DeviceCustomString2)
  | extend IronVueUrl = replace(@'\\', @'', tostring(DeviceCustomString3))
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DestinationHostName
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: SourceHostName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
customDetails:
  IronDefenseStatus: Status
  AnalystRating: LogSeverity
  AlertCreatedTime: TimeGenerated
  IronVueUrl: IronVueUrl
  IronDefenseAlertId: DeviceCustomString4
  AnalystSeverity: DeviceCustomString1
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: | 
    {{Category}}: {{Subcategory}} ({{DeviceCustomString4}})
  alertDescriptionFormat: | 
    IronDefense detected suspicious activity on {{TimeGenerated}} and categorized it as "{{AlertDescription}}".  View full details in IronVue: {{IronVueUrl}}
  alertSeverityColumnName: LogSeverity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceProduct == "IronDefense"

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceCustomString4, DeviceCustomString2

Stage 4: extend (5 consecutive steps)

| extend Category = coalesce(column_ifexists("DeviceEventCategory",""),extract(@'cat=([^;]+)(\;|$)', 1, AdditionalExtensions))
| extend Subcategory = extract(@'subcat=([^;]+)(\;|$)', 1, AdditionalExtensions)
| extend AlertDescription = strcat(Category, ': ', Subcategory)
| extend Status = tostring(DeviceCustomString2)
| extend IronVueUrl = replace(@'\\', @'', tostring(DeviceCustomString3))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceCustomString2summarize
DeviceCustomString4summarize
Categoryextend
Subcategoryextend
AlertDescriptionextend
Statusextend
IronVueUrlextend

CYFIRMA - High severity TOR Node Network Indicators - Block Recommended Rule

#

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.

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

"This KQL query identifies network-based indicators from CYFIRMA intelligence that are associated with the role 'TOR'. These indicators may include IP addresses, domains, and URLs related to Tor network activity. Threat actors often use Tor for anonymous communication, command and control, data exfiltration, and evasion of network defenses."

MITRE ATT&CK Coverage

Rule Body

id: fa53ac37-a646-4106-91b6-ce478a1b5323
name: CYFIRMA - High severity TOR Node Network Indicators - Block Recommended Rule
description: |
  "This KQL query identifies network-based indicators from CYFIRMA intelligence that are associated with the role 'TOR'. 
  These indicators may include IP addresses, domains, and URLs related to Tor network activity. 
  Threat actors often use Tor for anonymous communication, command and control, data exfiltration, and evasion of network defenses."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  //TOR Node Network Indicators - Block Recommended
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'TOR'
  | extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
  | extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
  | extend parsed = parse_json(extensions)
  | extend extensionKeys = bag_keys(parsed)
  | mv-expand extensionKeys
  | extend extensionKeyStr = tostring(extensionKeys)
  | extend ext = parsed[extensionKeyStr]
  | extend props = ext.properties
  | extend 
      extension_id = extensionKeyStr,
      ASN_Owner = props.asn_owner,
      ASN = props.asn,
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project
      IPv4,
      IPv6,
      URL,
      Domain,
      ThreatActors,
      RecommendedActions,
      Sources,
      Roles,
      Country,
      IPAbuse,
      name,
      Description,
      ConfidenceScore,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      SecurityVendors,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - CommandAndControl
  - Exfiltration
  - InitialAccess
  - Persistence
  - Reconnaissance
relevantTechniques:
  - T1090
  - T1572
  - T1048
  - T1071
  - T1189
  - T1505
  - T1595
  - T1090.003
  - T1048.002
  - T1071.001
  - T1505.003
  - T1595.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence TOR Node Network Indicators - Block Recommended - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  Created: created
  Modified: modified
  ValidFrom: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
  IPAbuse: IPAbuse
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv4
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv6
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaIndicators_CL

Stage 2: where

| where ConfidenceScore >= 80
    and TimeGenerated between (ago(timeFrame) .. now())
    and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'TOR'

Stage 3: extend (6 consecutive steps)

| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)

Stage 4: mv-expand

| mv-expand extensionKeys

Stage 5: extend (4 consecutive steps)

| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend 
    extension_id = extensionKeyStr,
    ASN_Owner = props.asn_owner,
    ASN = props.asn,
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 6: project

| project
    IPv4,
    IPv6,
    URL,
    Domain,
    ThreatActors,
    RecommendedActions,
    Sources,
    Roles,
    Country,
    IPAbuse,
    name,
    Description,
    ConfidenceScore,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    SecurityVendors,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
patterncontainsfile:hashesexcludes:pattern field:"pattern" value:"file:hashes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConfidenceScoreproject
Countryproject
Descriptionproject
Domainproject
IPAbuseproject
IPv4project
IPv6project
IndicatorIDproject
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
URLproject
createdproject
modifiedproject
nameproject
valid_fromproject

Detect service account login on new device

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

This detection rule tries to flag suspicious logins on devices from service accounts, for which these service accounts did not login into those devices for the last 14 days. This might indicate that the service account is compromised and is being used for lateral movement into the environment. Most service accounts have a fairly static set of devices they authenticate to. Because of this, it is easier to flag deviations for service accounts compared to user accounts. However, some service accounts are known to dynamically log into devices based on observed events (susch as the MDI service accounts). Because of this some environment specific finetuning might be needed to reduce BP detections.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

// Get all enabled service accounts
let service_acc = (
    IdentityInfo
    | where Timestamp > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);
// Get the history service account logins
let historic_events = (
    DeviceLogonEvents
    | where Timestamp between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);
// Get the account logins done over Network
DeviceLogonEvents
| where Timestamp > ago(1h)
| where LogonType == "Network"
| where ActionType == "LogonSuccess"
| extend AccountName = tolower(AccountName)
// Join inner to only get known service account logins
| join kind=inner service_acc on AccountName
// Join inner to get a list of the historic device logins for the service accounts
| join kind=inner historic_events on AccountName
// Only get sign-ins where Device is not in the history logins
| extend HistoricLogins = tostring(HistoricLogins)
| where HistoricLogins !contains DeviceName
// Make output better
| project-away AccountName1, AccountName2
// Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG
| where AccountName != "gsma_mdi$"
// Environment specific finetuning - begin
// Environment specific finetuning - end

// Get all enabled service accounts
let service_acc = (
    IdentityInfo
    | where TimeGenerated > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);
// Get the history service account logins
let historic_events = (
    DeviceLogonEvents
    | where TimeGenerated between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);
// Get the account logins done over Network
DeviceLogonEvents
| where TimeGenerated > ago(1h)
| where LogonType == "Network"
| where ActionType == "LogonSuccess"
| extend AccountName = tolower(AccountName)
// Join inner to only get known service account logins
| join kind=inner service_acc on AccountName
// Join inner to get a list of the historic device logins for the service accounts
| join kind=inner historic_events on AccountName
// Only get sign-ins where Device is not in the history logins
| extend HistoricLogins = tostring(HistoricLogins)
| where HistoricLogins !contains DeviceName
// Make output better
| project-away AccountName1, AccountName2
// Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG
| where AccountName != "gsma_mdi$"
// Environment specific finetuning - begin
// Environment specific finetuning - end

Stages and Predicates

Let Binding: service_acc used in Stages 1, 8

let service_acc = (
    IdentityInfo
    | where Timestamp > ago(7d)
    | where Type == "ServiceAccount" and IsAccountEnabled == 1
    | distinct AccountName = tolower(AccountName)
);

Let Binding: historic_events used in Stages 2, 9

let historic_events = (
    DeviceLogonEvents
    | where Timestamp between (ago(14d) .. ago(1h))
    | where ActionType == "LogonSuccess"
    | extend AccountName = tolower(AccountName)
    | join kind=inner service_acc on AccountName
    | summarize HistoricLogins = make_set(DeviceName) by AccountName
);

Stage 1: source

let service_acc

Stage 2: source

let historic_events

Stage 3: source

DeviceLogonEvents

Stage 4: where

where Timestamp > ago(3600s)

Stage 5: where

where LogonType == "Network"

Stage 6: where

where ActionType == "LogonSuccess"

Stage 7: extend

extend AccountName

Stage 8: join

join kind=inner (service_acc) on AccountName

Stage 9: join

join kind=inner (historic_events) on AccountName

Stage 10: extend

extend HistoricLogins

Stage 11: where

where HistoricLogins !contains DeviceName

Stage 12: project-away

project-away AccountName1, AccountName2

Stage 13: where

where AccountName != "gsma_mdi$"

Stage 14: summarize aggregation inside the join branch

summarize by AccountName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNamesummarize

Guardian- Additional check JSON Policy Violation Detection

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

'This alert creates an incident when Additional check JSON Policy Violation detected from the Guardian.'

Rule Body

id: 9ff28525-3c6f-47ba-806b-ffd913406989
name: Guardian- Additional check JSON Policy Violation Detection
description: |
  'This alert creates an incident when Additional check JSON Policy Violation detected from the Guardian.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'JSON'
  | where Severity =~ 'Informational'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Additional check JSON Policy Violation detection
  alertDescriptionFormat: |
    This query detects Additional check JSON Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'JSON'

Stage 3: where

| where Severity =~ 'Informational'

Indicators

These rows show field, operator, and value matches.

Guardian- Same Input/Output Language Detection Policy Violation Detection

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

'This alert creates an incident when Same Input/Output Language Detection Policy Violation detected from the Guardian.'

Rule Body

id: b2841802-c53c-4667-be8a-9ea8771c944a
name: Guardian- Same Input/Output Language Detection Policy Violation Detection
description: |
  'This alert creates an incident when Same Input/Output Language Detection Policy Violation detected from the Guardian.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Same Input/Output Language Detection'
  | where Severity =~ 'Informational'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Same Input/Output Language Detection Policy Violation detection
  alertDescriptionFormat: |
    This query detects Same Input/Output Language Detection Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Same Input/Output Language Detection'

Stage 3: where

| where Severity =~ 'Informational'

Indicators

These rows show field, operator, and value matches.

Hunt devices supporting MDE Containment

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

This hunting query can help you finding which Defender for Endpoint enrolled devices support device containment. This is being done by looking at the client version and estimated time the version was available.

References

Telemetry Coverage

Rule Body

// Paste your query here
// Gets the onboarded windows devices and checks containment support nuances
let onboardedWindows = DeviceInfo
| where OnboardingStatus == "Onboarded" and OSPlatform contains "Windows"
| distinct DeviceId, DeviceName, ClientVersion, OSPlatform
| parse ClientVersion with Major:int "." Minor:int "." Build:int "." Revision:int
// Reference: https://learn.microsoft.com/en-us/defender-endpoint/windows-whatsnew
| extend Date = case(
    Minor >= 8760, "July-2024", 
    Minor >= 8750, "May-2024",
    Minor >= 8735, "Feb-2024",
    Minor >= 8672, "Dec-2023",
    Minor >= 8560, "Sept-2023",
    Minor > 8295, "May-2023",
    Minor == 8295 and Revision >= 1023, "May-2023",
    Minor == 8295 and Revision between (1019 .. 1023), "Jan/Feb-2023",
    Minor > 8210, "Dec-2022", 
    Minor == 8210 and Build >= 22621 and Revision >= 1016, "Dec-2022", 
    Minor == 8210 and not(Build >= 22621 and Revision >= 1016), "Aug-2022", 
    "< Aug-2022"
)
// Containment without AH Audit supported from Nov-2022
// Containment with AH Audit supported from Mar-2023
| extend Containment = case(
    Minor >= 8295, "Supported with AH Audit",
    (Minor == 8210 and Build >= 22621 and Revision >= 1016) or Minor > 8210, "Supported without AH Audit",
    "Unsupported"
);
// Gets onboarded non-windows devices, since containment is not supported here
let onboardedNonWindows = DeviceInfo
| where OnboardingStatus == "Onboarded" and OSPlatform !contains "Windows"
| distinct DeviceId, DeviceName, ClientVersion, OSPlatform
| extend Containment = "Unsupported";
// Get not-onboarded Servers
let notOnboardedServers = DeviceInfo
| where OnboardingStatus != "Onboarded" and DeviceType == "Server"
| distinct DeviceId, DeviceName, ClientVersion, OSPlatform
| extend Containment = "Unsupported";
// Union all and show diagram
union onboardedNonWindows, onboardedWindows, notOnboardedServers
| summarize count() by Containment
| render piechart

Stages and Predicates

Union (3 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: onboardedNonWindows, onboardedWindows, notOnboardedServers

Leg 1: onboardedNonWindows

Leg 2: onboardedWindows

Leg 3: notOnboardedServers

Applied to the Combined Result

| summarize count() by Containment | render piechart

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
OSPlatformcontainsWindowsexcludes:OSPlatform field:"OSPlatform" value:"Windows"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Containmentsummarize

Lateral Movement via DCOM

#
Status
available
Severity
medium
Time window
1h
Group by
CommandLine, Computer, Description, EventID, Image, ParentCommandLine, ParentImage, ParentProcessGuid, ParentUser, ProcessGuid, User
Source
github.com/Azure/Azure-Sentinel

This query detects a fairly uncommon attack technique using the Windows Distributed Component Object Model (DCOM) to make a remote execution call to another computer system and gain lateral movement throughout the network. Ref: http://thenegative.zone/incident response/2017/02/04/MMC20.Application-Lateral-Movement-Analysis.html

MITRE ATT&CK Coverage

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 1: Process creation

Rule Body

id: 50cbf34a-4cdd-45d7-b3f5-8b53a1d0d14f
name: Lateral Movement via DCOM
description: | 
  'This query detects a fairly uncommon attack technique using the Windows Distributed Component Object Model (DCOM) to make a remote execution call to another computer system and gain lateral movement throughout the network.
  Ref: http://thenegative.zone/incident%20response/2017/02/04/MMC20.Application-Lateral-Movement-Analysis.html'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1021.003
query: |
  Event
  | where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1
  | parse EventData with * 'CommandLine">' CommandLine "<" * 'ParentCommandLine">' ParentCommandLine "<" *
  | where ParentCommandLine =~ "C:\\Windows\\System32\\svchost.exe -k DcomLaunch" and CommandLine =~ "C:\\Windows\\System32\\mmc.exe -Embedding"
  | parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" * 'Description">' Description "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description
  | extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: CommandLine
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: User
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

Event

Stage 2: where

| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1

Stage 3: parse

| parse EventData with * 'CommandLine">' CommandLine "<" * 'ParentCommandLine">' ParentCommandLine "<" *

Stage 4: where

| where ParentCommandLine =~ "C:\\Windows\\System32\\svchost.exe -k DcomLaunch" and CommandLine =~ "C:\\Windows\\System32\\mmc.exe -Embedding"

Stage 5: parse

| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" * 'Description">' Description "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description

Stage 7: extend

| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
Computersummarize
Descriptionsummarize
EndTimesummarize
EventIDsummarize
Imagesummarize
ParentCommandLinesummarize
ParentImagesummarize
ParentProcessGuidsummarize
ParentUsersummarize
ProcessGuidsummarize
StartTimesummarize
Usersummarize
DnsDomainextend
HostNameextend

M2131_EventLogManagementPostureChanged_EL3

#
Status
available
Severity
medium
Time window
7d
Group by
AssessedResourceId, MaturityLevel, RecommendationDisplayName
Source
github.com/Azure/Azure-Sentinel

'This alert is desinged to monitor Azure policies aligned with the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when EL3 policy compliance falls below 70% within a 1 week timeframe.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 672bfd77-4542-4ef1-acf9-e006dcd70c51
name: M2131_EventLogManagementPostureChanged_EL3
description: |
  'This alert is desinged to monitor Azure policies aligned with the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when EL3 policy compliance falls below 70% within a 1 week timeframe.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  SecurityRecommendation
  | where RecommendationDisplayName <> ""
  | extend MaturityLevel=iff(RecommendationDisplayName has_any("agent","extension","retention","logs encryption","collect","retained","log profile","CloudTrail","metric","AWS","GCP","DNS","Auditing","Flow","logging","usage"), "Event Logging (EL0)",
  iff(RecommendationDisplayName has_any("container registries","logic apps","Enhanced monitoring","IoT Hub","Event Hub","App Service", "Kubernetes","updates","email", "automation", "adaptive"), "Advanced Event Logging (EL3)",
  iff(RecommendationDisplayName has_any("signatures","CMK","CMEK","double encryption","managed key","KMS","container","Watcher"), "Intermediate Event Logging (EL2)",
  iff(RecommendationDisplayName has_any("Exploit Guard","endpoint protection","Antimalware","health","VPC","Defender","Vulnerabilities","vulnerability","diagnostic","Key","activity log alert",""), "Basic Event Logging (EL1)","Other"))))
  | summarize arg_max(TimeGenerated, *) by RecommendationDisplayName, AssessedResourceId, MaturityLevel
  | summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by MaturityLevel
  | extend PassedControls = (Passed/todouble(Total))*100
  | extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
  | project MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()
  | where MaturityLevel <> ''
  | where MaturityLevel == "Advanced Event Logging (EL3)"
  | where PassedControls < 70 
  //Adjust Either FailedRatePercentage or PasedRatePercentage Thresholds within   Organizational Needs
  | sort by PassedControls desc
  | extend URLCustomEntity = RemediationLink
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityRecommendation

Stage 2: where

| where RecommendationDisplayName <> ""

Stage 3: extend

| extend MaturityLevel=iff(RecommendationDisplayName has_any("agent","extension","retention","logs encryption","collect","retained","log profile","CloudTrail","metric","AWS","GCP","DNS","Auditing","Flow","logging","usage"), "Event Logging (EL0)",
iff(RecommendationDisplayName has_any("container registries","logic apps","Enhanced monitoring","IoT Hub","Event Hub","App Service", "Kubernetes","updates","email", "automation", "adaptive"), "Advanced Event Logging (EL3)",
iff(RecommendationDisplayName has_any("signatures","CMK","CMEK","double encryption","managed key","KMS","container","Watcher"), "Intermediate Event Logging (EL2)",
iff(RecommendationDisplayName has_any("Exploit Guard","endpoint protection","Antimalware","health","VPC","Defender","Vulnerabilities","vulnerability","diagnostic","Key","activity log alert",""), "Basic Event Logging (EL1)","Other"))))
MaturityLevel =
if(RecommendationDisplayName has "agent" or RecommendationDisplayName has "extension" or RecommendationDisplayName has "retention" or RecommendationDisplayName has "logs encryption" or RecommendationDisplayName has "collect" or RecommendationDisplayName has "retained" or RecommendationDisplayName has "log profile" or RecommendationDisplayName has "CloudTrail" or RecommendationDisplayName has "metric" or RecommendationDisplayName has "AWS" or RecommendationDisplayName has "GCP" or RecommendationDisplayName has "DNS" or RecommendationDisplayName has "Auditing" or RecommendationDisplayName has "Flow" or RecommendationDisplayName has "logging" or RecommendationDisplayName has "usage")"Event Logging (EL0)"
elseiff((RecommendationDisplayName has_any ("container registries", "logic apps", "Enhanced monitoring", "IoT Hub", "Event Hub", "App Service", "Kubernetes", "updates", "email", "automation", "adaptive")), "Advanced Event Logging (EL3)", iff((RecommendationDisplayName has_any ("signatures", "CMK", "CMEK", "double encryption", "managed key", "KMS", "container", "Watcher")), "Intermediate Event Logging (EL2)", iff((RecommendationDisplayName has_any ("Exploit Guard", "endpoint protection", "Antimalware", "health", "VPC", "Defender", "Vulnerabilities", "vulnerability", "diagnostic", "Key", "activity log alert", "")), "Basic Event Logging (EL1)", "Other")))

Stage 4: summarize

| summarize arg_max(TimeGenerated, *) by RecommendationDisplayName, AssessedResourceId, MaturityLevel

Stage 5: summarize

| summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by MaturityLevel

Stage 6: extend

| extend PassedControls = (Passed/todouble(Total))*100

Stage 7: extend

| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')

Stage 8: project

| project MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()

Stage 9: where

| where MaturityLevel <> ''

Stage 10: where

| where MaturityLevel == "Advanced Event Logging (EL3)"

Stage 11: where

| where PassedControls < 70

Stage 12: sort

| sort by PassedControls desc

Stage 13: extend

| extend URLCustomEntity = RemediationLink

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Failedproject
LastObservedproject
MaturityLevelproject
Passedproject
PassedControlsproject
RemediationLinkproject
Totalproject
URLCustomEntityextend

NRT New access credential added to Application or Service Principal

#
Status
available
Severity
medium
Source
github.com/Azure/Azure-Sentinel

This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where a verify KeyCredential was already present for the app. If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential. Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: e42e889a-caaf-4dbb-aec6-371b37d64298
name: NRT New access credential added to Application or Service Principal
description: |
  'This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where a verify KeyCredential was already present for the app.
  If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential.
  Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1550
tags:
  - Solorigate
  - NOBELIUM
query: |
  AuditLogs
  | where OperationName has_any ("Add service principal", "Certificates and secrets management") // captures "Add service principal", "Add service principal credentials", and "Update application - Certificates and secrets management" events
  | where Result =~ "success"
  | where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "Application"
        | extend targetDisplayName = tostring(TargetResource.displayName),
                 targetId = tostring(TargetResource.id),
                 targetType = tostring(TargetResource.type),
                 keyEvents = TargetResource.modifiedProperties
    )
  | mv-apply Property = keyEvents on 
    (
        where Property.displayName =~ "KeyDescription"
        | extend new_value_set = parse_json(tostring(Property.newValue)),
                 old_value_set = parse_json(tostring(Property.oldValue))
    )
  | where old_value_set != "[]"
  | extend diff = set_difference(new_value_set, old_value_set)
  | where diff != "[]"
  | parse diff with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
  | where keyUsage =~ "Verify"
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend UserAgent = tostring(AdditionalDetail.value)
    )
  | 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))
  // The below line is currently commented out but Microsoft Sentinel users can modify this query to show only Application or only Service Principal events in their environment
  //| where targetType =~ "Application" // or targetType =~ "ServicePrincipal"
  | project-away diff, new_value_set, old_value_set
  | project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatedByName
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.5
kind: NRT

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has_any ("Add service principal", "Certificates and secrets management")

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "Application"
      | extend targetDisplayName = tostring(TargetResource.displayName),
               targetId = tostring(TargetResource.id),
               targetType = tostring(TargetResource.type),
               keyEvents = TargetResource.modifiedProperties
  )

Stage 6: kusto:mv-apply

| mv-apply Property = keyEvents on 
  (
      where Property.displayName =~ "KeyDescription"
      | extend new_value_set = parse_json(tostring(Property.newValue)),
               old_value_set = parse_json(tostring(Property.oldValue))
  )

Stage 7: where

| where old_value_set != "[]"

Stage 8: extend

| extend diff = set_difference(new_value_set, old_value_set)

Stage 9: where

| where diff != "[]"

Stage 10: parse

| parse diff with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *

Stage 11: where

| where keyUsage =~ "Verify"

Stage 12: kusto:mv-apply

| mv-apply AdditionalDetail = AdditionalDetails on 
  (
      where AdditionalDetail.key =~ "User-Agent"
      | extend UserAgent = tostring(AdditionalDetail.value)
  )

Stage 13: extend (5 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))

Stage 14: project-away

| project-away diff, new_value_set, old_value_set

Stage 15: project-reorder

| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId

Stage 16: extend

| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AdditionalDetail.keyeq
  • User-Agent transforms: kusto_dynamic_path
field:"AdditionalDetail.key" kind:eq value:"User-Agent"
InitiatedBy.app.displayNamematch
  • @ transforms: tostring, term, kusto_dynamic_path corpus 4 (kusto 4)
field:"InitiatedBy.app.displayName" kind:match value:"@"
InitiatedBy.user.userPrincipalNamematch
  • @ transforms: tostring, term, kusto_dynamic_path corpus 4 (kusto 4)
field:"InitiatedBy.user.userPrincipalName" kind:match value:"@"
OperationNamematch
  • Add service principal transforms: term corpus 4 (kusto 4)
  • Certificates and secrets management transforms: term corpus 5 (kusto 5)
field:"OperationName" kind:match
Property.displayNameeq
  • KeyDescription transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"KeyDescription"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"
TargetResource.typeeq
  • Application transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"Application"
diffne
  • []
field:"diff" kind:ne value:"[]"
keyUsageeq
  • Verify
field:"keyUsage" kind:eq value:"Verify"
old_value_setne
  • []
field:"old_value_set" kind:ne value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

NRT User added to Microsoft Entra ID Privileged Groups

#
Status
available
Severity
medium
Source
github.com/Azure/Azure-Sentinel

This will alert when a user is added to any of the Privileged Groups. For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities. For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 70fc7201-f28e-4ba7-b9ea-c04b96701f13
name: NRT User added to Microsoft Entra ID Privileged Groups
description: |
  'This will alert when a user is added to any of the Privileged Groups.
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
  For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T1078
tags:
  - DEV-0537
query: |
  let OperationList = dynamic(["Add member to role","Add eligible member to role"]);
  let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins","PrivilegedAuthenticationAdmins"]);
  AuditLogs
  | where Category =~ "RoleManagement"
  | where OperationName in~ (OperationList)
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName),
                 modProps = TargetResource.modifiedProperties
    )
  | mv-apply Property = modProps on 
    (
        where Property.displayName =~ "Role.WellKnownObjectName"
        | extend DisplayName = trim('"',tostring(Property.displayName)),
                 GroupName = trim('"',tostring(Property.newValue))
    )
  | extend InitiatingAppId = tostring(InitiatedBy.app.appId)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.servicePrincipalName)
  | 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 InitiatingUserRoles = InitiatedBy.user.roles
  | where GroupName in~ (PrivilegedGroups)
  // If you don't want to alert for operations from PIM, remove below filtering for MS-PIM.
  //| where InitiatingAppName != "MS-PIM" and InitiatingAppName != "MS-PIM-Fairfax"
  | project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppId, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserRoles, DisplayName, GroupName, TargetUserPrincipalName
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
  | extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
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.7
kind: NRT

Stages and Predicates

Parameters

let OperationList = dynamic(["Add member to role","Add eligible member to role"]);
let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins","PrivilegedAuthenticationAdmins"]);

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "RoleManagement"

Stage 3: where

| where OperationName in~ (OperationList)

Stage 4: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName),
               modProps = TargetResource.modifiedProperties
  )

Stage 5: kusto:mv-apply

| mv-apply Property = modProps on 
  (
      where Property.displayName =~ "Role.WellKnownObjectName"
      | extend DisplayName = trim('"',tostring(Property.displayName)),
               GroupName = trim('"',tostring(Property.newValue))
  )

Stage 6: extend (8 consecutive steps)

| extend InitiatingAppId = tostring(InitiatedBy.app.appId)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.servicePrincipalName)
| 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 InitiatingUserRoles = InitiatedBy.user.roles

Stage 7: where

| where GroupName in~ (PrivilegedGroups)

Stage 8: project

| project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppId, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserRoles, DisplayName, GroupName, TargetUserPrincipalName

Stage 9: extend

| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Stage 10: extend

| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
GroupNamein
  • PrivilegedAuthenticationAdmins
  • PrivilegedRoleAdmins
  • TenantAdmins
  • UserAccountAdmins
field:"GroupName" kind:in
OperationNamein
  • Add eligible member to role corpus 2 (kusto 2)
  • Add member to role corpus 3 (kusto 3)
field:"OperationName" kind:in
Property.displayNameeq
  • Role.WellKnownObjectName transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"Role.WellKnownObjectName"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADOperationTypeproject
AADTenantIdproject
Categoryproject
DisplayNameproject
GroupNameproject
InitiatingAadUserIdproject
InitiatingAppIdproject
InitiatingAppNameproject
InitiatingAppServicePrincipalIdproject
InitiatingIpAddressproject
InitiatingUserPrincipalNameproject
InitiatingUserRolesproject
OperationNameproject
TargetUserPrincipalNameproject
TimeGeneratedproject
InitiatedByNameextend
InitiatedByUPNSuffixextend
TargetNameextend
TargetUPNSuffixextend

Pathlock TDnR - Authorization Profile Changes

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

Detects changes to SAP authorization profiles, forwarded by Pathlock Threat Detection and Response. Unauthorized profile modifications may grant excessive privileges, create privilege escalation paths, or be used to establish persistent privileged access in SAP systems.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c41
name: Pathlock TDnR - Authorization Profile Changes
kind: Scheduled
description: >-
  Detects changes to SAP authorization profiles, forwarded by Pathlock Threat Detection and Response.
  Unauthorized profile modifications may grant excessive privileges, create privilege escalation paths,
  or be used to establish persistent privileged access in SAP systems.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1548
  - T1098
query: |
  Pathlock_TDnR_CL
  | where DataSource == "PROFILE_CHANGES"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "PROFILE_CHANGES"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Pathlock TDnR - User-Role Assignment Changes

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

Detects changes to user-to-role assignments in SAP (SU01 roles tab), forwarded by Pathlock Threat Detection and Response. Unauthorized role assignments are the primary mechanism for granting and revoking SAP access, and unexpected changes may indicate privilege escalation, account backdooring, or unauthorized access provisioning.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c76
name: Pathlock TDnR - User-Role Assignment Changes
kind: Scheduled
description: >-
  Detects changes to user-to-role assignments in SAP (SU01 roles tab), forwarded by Pathlock
  Threat Detection and Response. Unauthorized role assignments are the primary mechanism for
  granting and revoking SAP access, and unexpected changes may indicate privilege escalation,
  account backdooring, or unauthorized access provisioning.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1548
  - T1098
query: |
  Pathlock_TDnR_CL
  | where DataSource == "USER_ROLES"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "USER_ROLES"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Potentially Relayed NTLM Authentication - Microsoft Sentinel

#
Group by
$left.ServiceName, $right.Computer, IpAddress, TargetUserName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

The below query detects Kerberos logons of computer accounts where there isn't any ticket request in the last 12h (10h is the default ticket expiration) coming from the same IpAddress with the same TargetUserName. The query can be enriched further if needed.

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique

Telemetry Coverage

Rule Body

// Author       : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/detecting-kerberos-relaying-e6be66fa647c
//
// Description: This query detects Kerberos logons of computer accounts where there isn't any ticket request in the last 12h (10h is the default ticket expiration) coming from the same IpAddress with the same TargetUserName. The query can be enriched further if needed. 
//
// Query parameters:
//
let Ticket_Requests = materialize ( 
SecurityEvent
| where TimeGenerated > ago(12h)
| where EventID == 4769
| where EventData has '<Data Name="Status">0x0</Data>'
| where EventData !has'<Data Name="IpAddress">::1</Data>'
| parse EventData with * 'TargetUserName">' TargetUserName '</Data' * 'TargetDomainName">' TargetDomainName '</Data' * 'ServiceName">' ServiceName '<' * 'IpAddress">::ffff:' IpAddress '<' * 'Status">' Status '<' *
| where TargetUserName !has ServiceName
| where TargetUserName contains "$"
| where ServiceName has "$"
| project TimeGenerated, TargetUserName=tolower(TargetUserName), TargetDomainName, ServiceName=tolower(replace_string(ServiceName, '$', '')), IpAddress, Status
)
;
let Suspicious_Logons = 
    Ticket_Requests
    | join kind=rightanti (
        SecurityEvent
        | where TimeGenerated > ago(1h)
        | where EventID == 4624
        | where AuthenticationPackageName == "Kerberos"
        | where IpAddress !in ('-', '::1', '127.0.0.1')
        | where IpAddress !startswith "169.254."
        | where Account endswith_cs "$"
        | project TimeGenerated, Computer = tolower(replace_regex(Computer, @'(\w+)\..*', @'\1')), Account, TargetUserName=tolower(TargetUserName), IpAddress
        | where TargetUserName !has Computer
        ) on IpAddress, $left.ServiceName==$right.Computer
        ;
Suspicious_Logons
| join kind=leftouter  (
    Ticket_Requests
    | extend TargetUserName = replace_regex(TargetUserName, @'(\w+\$)@.*', @'\1')
    ) on IpAddress, TargetUserName
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), count(), dcount(ServiceName) by TargetUserName, IpAddress
// Filter results
// we don't expect a successful ticket request coming from the rogue(attacker) device befor the relaying attack.
// If there is at least one ticket request coming from the suspicious IP with the same TargetUserName, assume it's a legitimate activity.
| where isempty(dcount_ServiceName)

Stages and Predicates

let Ticket_Requests and let Suspicious_Logons are inlined into the numbered stages below.

Stage 0: let

let Ticket_Requests = materialize(<inlined as stages below>);
let Suspicious_Logons = Ticket_Requests <inlined as stages below>;

Stages 1 to 13 define let Suspicious_Logons as the rule's main pipeline source. Stages 14 to 16 use it.

Stage 1: source

let Ticket_Requests

Stage 2: source

let Suspicious_Logons

Stage 3: source

SecurityEvent

Stage 4: where

where TimeGenerated > ago(43200s)

Stage 5: where

where EventID == 4769

Stage 6: where

where EventData has "<Data Name=\"Status\">0x0</Data>"

Stage 7: where

where not (EventData has "<Data Name=\"IpAddress\">::1</Data>")

Stage 8: parse

parse

Stage 9: where

where TargetUserName =~ ServiceName

Stage 10: where

where TargetUserName contains "$"

Stage 11: where

where ServiceName has "$"

Stage 12: project

project IpAddress, ServiceName, Status, TargetDomainName, TargetUserName, TimeGenerated

Stage 13: join

| join kind=rightanti (
        SecurityEvent
        | where TimeGenerated > ago(1h)
        | where EventID == 4624
        | where AuthenticationPackageName == "Kerberos"
        | where IpAddress !in ('-', '::1', '127.0.0.1')
        | where IpAddress !startswith "169.254."
        | where Account endswith_cs "$"
        | project TimeGenerated, Computer = tolower(replace_regex(Computer, @'(\w+)\..*', @'\1')), Account, TargetUserName=tolower(TargetUserName), IpAddress
        | where TargetUserName !has Computer
        ) on IpAddress, $left.ServiceName==$right.Computer

Stage 14: join

Suspicious_Logons
| join kind=leftouter  (
    Ticket_Requests
    | extend TargetUserName = replace_regex(TargetUserName, @'(\w+\$)@.*', @'\1')
    ) on IpAddress, TargetUserName

Stage 15: summarize

summarize FirstSeen, LastSeen by TargetUserName, IpAddress

Stage 16: where

where isempty(dcount_ServiceName)

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EventDatamatch<Data Name="IpAddress">::1</Data>excludes:EventData
IpAddressin-, 127.0.0.1, ::1excludes:IpAddress field:"IpAddress" value:"-" field:"IpAddress" value:"127.0.0.1" field:"IpAddress" value:"::1"
IpAddressstarts_with169.254.excludes:IpAddress field:"IpAddress" value:"169.254."

Output Fields

These fields are emitted when the rule matches.

FieldSource
FirstSeensummarize
IpAddresssummarize
LastSeensummarize
TargetUserNamesummarize

Power Apps - Multiple users access a malicious link after launching new app

#

Red Sift - New email with URL from previously unseen sender

#
Status
available
Severity
medium
Time window
14d
Group by
EmailFrom
Source
github.com/Azure/Azure-Sentinel

'Detects email forensics events that contain one or more URLs where the sender in the from field has not been seen in the previous 14 days, which may indicate phishing activity or a newly observed sender.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 6e0b70d4-0ab8-480e-9707-8ad45fc21a65
name: Red Sift - New email with URL from previously unseen sender
description: |
  'Detects email forensics events that contain one or more URLs where the sender in the from field has not been seen in the previous 14 days, which may indicate phishing activity or a newly observed sender.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RedSiftPush
    dataTypes:
      - RedSiftEmailForensics_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let lookback = 14d;
  let recentWindow = 1h;
  let historicalSenders = RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", ""))
  | where TimeGenerated between (ago(lookback) .. ago(recentWindow))
  | where isnotempty(EmailFrom)
  | summarize by EmailFrom;
  RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", "")),
      EmailSubject = tostring(column_ifexists("EmailSubject", "")),
      EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
      EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
      SrcIp = tostring(column_ifexists("SrcIp", "")),
      DstHostname = tostring(column_ifexists("DstHostname", "")),
      Severity = tostring(column_ifexists("Severity", "")),
      Message = tostring(column_ifexists("Message", "")),
      CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
      EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))
  | where TimeGenerated >= ago(recentWindow)
  | where isnotempty(EmailFrom)
  | extend UrlCount = array_length(EmailUrls)
  | where UrlCount > 0
  | mv-apply Url = EmailUrls on (
      summarize UrlSet = make_set(tostring(Url.url_string), 50)
    )
  | extend UrlList = strcat_array(UrlSet, ", ")
  | join kind=leftanti (historicalSenders) on EmailFrom
  | project
      TimeGenerated,
      EmailFrom,
      EmailSubject,
      EmailReturnPath,
      EmailMessageUid,
      SrcIp,
      DstHostname,
      UrlCount,
      UrlList,
      Severity,
      Message,
      CorrelationUid
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EmailFrom
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DstHostname
customDetails:
  EmailSubject: EmailSubject
  ReturnPath: EmailReturnPath
  UrlCount: UrlCount
  UrlList: UrlList
  CorrelationUid: CorrelationUid
alertDetailsOverride:
  alertDisplayNameFormat: "RedSift - New URL-bearing sender {{EmailFrom}}"
  alertDescriptionFormat: "Email from previously unseen sender {{EmailFrom}} contains {{UrlCount}} URL(s)."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
    groupByCustomDetails:
      - EmailSubject
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 14d;
let recentWindow = 1h;

Let Binding: historicalSenders used in Stage 9

let historicalSenders = RedSiftEmailForensics_CL
| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailFrom)
| summarize by EmailFrom;

Stage 1: source

RedSiftEmailForensics_CL

Stage 2: extend

| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", "")),
    EmailSubject = tostring(column_ifexists("EmailSubject", "")),
    EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
    EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
    SrcIp = tostring(column_ifexists("SrcIp", "")),
    DstHostname = tostring(column_ifexists("DstHostname", "")),
    Severity = tostring(column_ifexists("Severity", "")),
    Message = tostring(column_ifexists("Message", "")),
    CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
    EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))

Stage 3: where

| where TimeGenerated >= ago(recentWindow)

Stage 4: where

| where isnotempty(EmailFrom)

Stage 5: extend

| extend UrlCount = array_length(EmailUrls)

Stage 6: where

| where UrlCount > 0

Stage 7: kusto:mv-apply

| mv-apply Url = EmailUrls on (
    summarize UrlSet = make_set(tostring(Url.url_string), 50)
  )

Stage 8: extend

| extend UrlList = strcat_array(UrlSet, ", ")

Stage 9: join (negated)

join kind=leftanti (historicalSenders) on EmailFrom

Stage 10: project

project CorrelationUid, DstHostname, EmailFrom, EmailMessageUid, EmailReturnPath, EmailSubject, Message, Severity, SrcIp, TimeGenerated, UrlCount, UrlList

Stage 11: summarize aggregation inside the mv-apply branch

summarize

Stage 12: summarize aggregation inside the join branch

summarize by EmailFrom

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EmailFromis_not_null
  • (no value, null check)
field:"EmailFrom" kind:is_not_null
UrlCountgt
  • 0 transforms: number
field:"UrlCount" kind:gt value:"0"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EmailFromis_not_null(no value, null check)excludes:EmailFrom

Output Fields

These fields are emitted when the rule matches.

FieldSource
EmailFromsummarize

Removable storage ONLINE event from secRMM

#
Severity
high
Source
github.com/Azure/Azure-Sentinel

Detect when a removable storage device is plugged in by the end-user.

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: A22B2ECF-1478-4400-877E-07A32E53A897
name: Removable storage ONLINE event from secRMM
description: Detect when a removable storage device is plugged in by the end-user.
severity: High
kind: NRT
apiVersion: 2025-09-01
displayName: Removable Storage ONLINE
enabled: true
query: |+
    secRMM_CL | where Event_s == "ONLINE" | 
    extend AccountCustomEntity = User_s |
    extend HostCustomEntity = Computer
suppressionDuration: PT5H
suppressionEnabled: false
tactics:
- Collection
relevantTechniques:
- T1025
subTechniques: []
alertRuleTemplateName: 
incidentConfiguration:
createIncident: true
groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride: 
customDetails: 
entityMappings:
- entityType: Account
  fieldMappings:
    - identifier: AadUserId
      columnName: User_s
- entityType: Host
  fieldMappings:
    - identifier: HostName
      columnName: Computer

Stages and Predicates

Stage 1: source

secRMM_CL

Stage 2: where

| where Event_s == "ONLINE"

Stage 3: extend

| extend AccountCustomEntity = User_s

Stage 4: extend

| extend HostCustomEntity = Computer

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
HostCustomEntityextend

Sentinel One - Same custom rule triggered on different hosts

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

'Detects when same custom rule was triggered on different hosts.'

MITRE ATT&CK Coverage

Rule Body

id: 5586d378-1bce-4d9b-9ac8-e7271c9d5a9a
name: Sentinel One - Same custom rule triggered on different hosts
description: |
  'Detects when same custom rule was triggered on different hosts.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SentinelOne
    dataTypes:
      - SentinelOne
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - LateralMovement
relevantTechniques:
  - T1190
  - T1210
query: | 
  SentinelOne
  | where ActivityType == 3608
  | extend RuleName = extract(@'Custom Rule:\s(.*?)\sin Group', 1, EventOriginalMessage)
  | extend DstHostname = extract(@'detected on\s(\S+)\.', 1, EventOriginalMessage)
  | summarize hosts = makeset(DstHostname) by RuleName, bin(TimeGenerated, 15m)
  | where array_length(hosts) > 1
  | extend HostCustomEntity = hosts
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

SentinelOne

Stage 2: where

| where ActivityType == 3608

Stage 3: extend

| extend RuleName = extract(@'Custom Rule:\s(.*?)\sin Group', 1, EventOriginalMessage)

Stage 4: extend

| extend DstHostname = extract(@'detected on\s(\S+)\.', 1, EventOriginalMessage)

Stage 5: summarize

| summarize hosts = makeset(DstHostname) by RuleName, bin(TimeGenerated, 15m)

Stage 6: where

| where array_length(hosts) > 1

Stage 7: extend

| extend HostCustomEntity = hosts

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityTypeeq
  • 3608 corpus 3 (kusto 3)
field:"ActivityType" kind:eq value:"3608"

Output Fields

These fields are emitted when the rule matches.

FieldSource
RuleNamesummarize
hostssummarize
HostCustomEntityextend

Trend Micro CAS - Threat detected and not blocked

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

'Detects when threat was not blocked by CAS solution.'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: c8e2ad52-bd5f-4f74-a2f7-6c3ab8ba687a
name: Trend Micro CAS - Threat detected and not blocked
description: |
  'Detects when threat was not blocked by CAS solution.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  TrendMicroCAS
  | where isnotempty(SecurityRiskName)
  | where EventOriginalResultDetails !has 'Blocked' or EventOriginalResultDetails !has 'Block' or EventOriginalResultDetails !has 'Quarantine' or TriggeredPolicyName has 'Monitor Only'
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

TrendMicroCAS

Stage 2: where

| where isnotempty(SecurityRiskName)

Stage 3: where

| where EventOriginalResultDetails !has 'Blocked' or EventOriginalResultDetails !has 'Block' or EventOriginalResultDetails !has 'Quarantine' or TriggeredPolicyName has 'Monitor Only'

Stage 4: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

UniFi Site Manager: ISP Downtime

#
Status
available
Severity
high
Time window
30m
Group by
SiteId, ispAsn, ispName, metricTime
Source
github.com/Azure/Azure-Sentinel

Identifies when the ISP connection experiences downtime, impacting business operations and requiring documentation for SLA purposes.

MITRE ATT&CK Coverage

Rule Body

id: b54123ef-cfa7-769e-a959-f437404a1192
name: "UniFi Site Manager: ISP Downtime"
description: |
  Identifies when the ISP connection experiences downtime, impacting business operations and requiring documentation for SLA purposes.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 15m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1489
  - T1499
subTechniques:
  - T1499.002
query: |
  // UniFi ISP Downtime Detection
  let MinDowntimeSeconds = 60;
  Unifi_SiteManager_ISPMetrics_CL
  | where TimeGenerated > ago(30m)
  | mv-expand period = Periods
  | extend
      metricTime = todatetime(period.metricTime),
      downtime = toint(period.data.wan.downtime),
      uptime = todouble(period.data.wan.uptime),
      ispName = tostring(period.data.wan.ispName),
      ispAsn = tostring(period.data.wan.ispAsn)
  // De-duplicate Periods: each poll returns the same hour buckets, so collapse to latest value per metricTime
  | summarize arg_max(TimeGenerated, downtime, uptime, ispAsn) by tostring(SiteId), ispName, metricTime
  | where metricTime > ago(30m)
  | where downtime > 0
  | summarize
      TotalDowntimeSeconds = sum(downtime),
      EventCount = count(),
      AvgUptime = round(avg(uptime), 2),
      FirstSeen = min(metricTime),
      LastSeen = max(metricTime)
      by SiteId, ispName, ispAsn
  | where TotalDowntimeSeconds >= MinDowntimeSeconds
  | extend
      TimeGenerated = now(),
      DowntimeMinutes = round(TotalDowntimeSeconds / 60.0, 2)
  | project
      TimeGenerated,
      SiteId = SiteId,
      ISPName = ispName,
      ISPAsn = ispAsn,
      TotalDowntimeSeconds,
      DowntimeMinutes,
      AvgUptimePct = AvgUptime,
      EventCount,
      FirstSeen,
      LastSeen
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ISPName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: PT4H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Parameters

let MinDowntimeSeconds = 60;

Stage 1: source

Unifi_SiteManager_ISPMetrics_CL

Stage 2: where

| where TimeGenerated > ago(30m)

Stage 3: mv-expand

| mv-expand period = Periods

Stage 4: extend

| extend
    metricTime = todatetime(period.metricTime),
    downtime = toint(period.data.wan.downtime),
    uptime = todouble(period.data.wan.uptime),
    ispName = tostring(period.data.wan.ispName),
    ispAsn = tostring(period.data.wan.ispAsn)

Stage 5: summarize

| summarize arg_max(TimeGenerated, downtime, uptime, ispAsn) by tostring(SiteId), ispName, metricTime

Stage 6: where

| where metricTime > ago(30m)

Stage 7: where

| where downtime > 0

Stage 8: summarize

| summarize
    TotalDowntimeSeconds = sum(downtime),
    EventCount = count(),
    AvgUptime = round(avg(uptime), 2),
    FirstSeen = min(metricTime),
    LastSeen = max(metricTime)
    by SiteId, ispName, ispAsn

Stage 9: where

| where TotalDowntimeSeconds >= MinDowntimeSeconds

Stage 10: extend

| extend
    TimeGenerated = now(),
    DowntimeMinutes = round(TotalDowntimeSeconds / 60.0, 2)

Stage 11: project

| project
    TimeGenerated,
    SiteId = SiteId,
    ISPName = ispName,
    ISPAsn = ispAsn,
    TotalDowntimeSeconds,
    DowntimeMinutes,
    AvgUptimePct = AvgUptime,
    EventCount,
    FirstSeen,
    LastSeen

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvgUptimePctproject
DowntimeMinutesproject
EventCountproject
FirstSeenproject
ISPAsnproject
ISPNameproject
LastSeenproject
SiteIdproject
TimeGeneratedproject
TotalDowntimeSecondsproject

User Accounts - Sign in Failure due to CA Spikes

#
Status
available
Severity
medium
Time window
14d
Group by
$left.AnomalyHour, $right.DateHour, IPAddress, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies spike in failed sign-ins from user accounts due to conditional access policied. Spike is determined based on Time series anomaly which will look at historical baseline values. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 3a9d5ede-2b9d-43a2-acc4-d272321ff77c
name: User Accounts - Sign in Failure due to CA Spikes
description: |
  ' Identifies spike in failed sign-ins from user accounts due to conditional access policied.
  Spike is determined based on Time series anomaly which will look at historical baseline values.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
  This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - BehaviorAnalytics
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let riskScoreCutoff = 20; //Adjust this based on volume of results
  let starttime = 14d;
  let timeframe = 1d;
  let scorethreshold = 3;
  let baselinethreshold = 50;
  let aadFunc = (tableName:string){
    // Failed Signins attempts with reasoning related to conditional access policies.
    table(tableName)
    | where TimeGenerated between (startofday(ago(starttime))..startofday(now()))
    | where ResultDescription has_any ("conditional access", "CA") or ResultType in (50005, 50131, 53000, 53001, 53002, 52003, 70044)
    | extend UserPrincipalName = tolower(UserPrincipalName)
    | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  let allSignins = union isfuzzy=true aadSignin, aadNonInt;
  let TimeSeriesAlerts = 
  allSignins
  | make-series DailyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step 1d by UserPrincipalName
  | extend (anomalies, score, baseline) = series_decompose_anomalies(DailyCount, scorethreshold, -1, 'linefit')
  | mv-expand DailyCount to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
  // Filtering low count events per baselinethreshold
  | where anomalies > 0 and baseline > baselinethreshold
  | extend AnomalyHour = TimeGenerated
  | project UserPrincipalName, AnomalyHour, TimeGenerated, DailyCount, baseline, anomalies, score;
  // Filter the alerts for specified timeframe
  TimeSeriesAlerts
  | where TimeGenerated > startofday(ago(timeframe))
  | join kind=inner ( 
    allSignins
    | where TimeGenerated > startofday(ago(timeframe))
    // create a new column and round to hour
    | extend DateHour = bin(TimeGenerated, 1h)
    | summarize PartialFailedSignins = count(), LatestAnomalyTime = arg_max(TimeGenerated, *) by bin(TimeGenerated, 1h), OperationName, Category, ResultType, ResultDescription, UserPrincipalName, UserDisplayName, AppDisplayName, ClientAppUsed, IPAddress, ResourceDisplayName
  ) on UserPrincipalName, $left.AnomalyHour == $right.DateHour
  | project LatestAnomalyTime, OperationName, Category, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, AppDisplayName, ClientAppUsed, UserAgent, IPAddress, Location, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, PartialFailedSignins, TotalFailedSignins = DailyCount, baseline, anomalies, score
  | extend timestamp = LatestAnomalyTime, Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
  | extend UserPrincipalName = tolower(UserPrincipalName)
  | join kind=leftouter (
      IdentityInfo
      | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
      | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
      | summarize
          Tags = make_set(Tags, 1000),
          GroupMembership = make_set(GroupMembership, 1000),
          AssignedRoles = make_set(AssignedRoles, 1000),
          UserType = make_set(UserType, 1000),
          UserAccountControl = make_set(UserType, 1000)
      by AccountUPN
      | extend UserPrincipalName=tolower(AccountUPN)
  ) on UserPrincipalName
  | join kind=leftouter (
      BehaviorAnalytics
      | where ActivityType in ("FailedLogOn", "LogOn")
      | where isnotempty(SourceIPAddress)
      | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress
      | project-rename IPAddress = SourceIPAddress
      | summarize
          UsersInsights = make_set(UsersInsights, 1000),
          DevicesInsights = make_set(DevicesInsights, 1000),
          IPInvestigationPriority = sum(InvestigationPriority)
      by IPAddress)
  on IPAddress
  | extend UEBARiskScore = IPInvestigationPriority
  | where UEBARiskScore > riskScoreCutoff
  | sort by UEBARiskScore desc 
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 2.0.5
kind: Scheduled

Stages and Predicates

Parameters

let riskScoreCutoff = 20;
let starttime = 14d;
let timeframe = 1d;
let scorethreshold = 3;
let baselinethreshold = 50;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

let allSignins and let TimeSeriesAlerts are inlined into the numbered stages below.

Let Binding: aadFunc used in Stages 1, 11

let aadFunc = (tableName:string){
  table(tableName)
  | where TimeGenerated between (startofday(ago(starttime))..startofday(now()))
  | where ResultDescription has_any ("conditional access", "CA") or ResultType in (50005, 50131, 53000, 53001, 53002, 52003, 70044)
  | extend UserPrincipalName = tolower(UserPrincipalName)
  | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
};

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

Stage 1: union

union of 2 branches

Stage 2: source

aadSignin

Stage 3: source

aadNonInt

Stage 4: summarize

summarize DailyCount by UserPrincipalName

Stage 5: extend

extend anomalies, baseline, score

Stage 6: mv-expand

mv-expand DailyCount

Stage 7: where

where anomalies > 0 and baseline > 50

Stage 8: extend

extend AnomalyHour

Stage 9: project

project AnomalyHour, DailyCount, TimeGenerated, UserPrincipalName, anomalies, baseline, score

Stage 10: where

where TimeGenerated > ago(86400s)

Stage 11: join

join kind=inner (allSignins) on UserPrincipalName, $left.AnomalyHour, $right.DateHour

Stage 12: project

project AppDisplayName, AuthenticationRequirement, Category, ClientAppUsed, ConditionalAccessStatus, IPAddress, LatestAnomalyTime, Location, OperationName, PartialFailedSignins, ResourceDisplayName, ResultDescription, ResultType, TotalFailedSignins, UserAgent, UserDisplayName, UserPrincipalName, anomalies, baseline, score

Stage 13: extend

extend Name, UPNSuffix, timestamp

Stage 14: extend

extend UserPrincipalName

Stage 15: join

| join kind=leftouter (
    IdentityInfo
    | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
    | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
    | summarize
        Tags = make_set(Tags, 1000),
        GroupMembership = make_set(GroupMembership, 1000),
        AssignedRoles = make_set(AssignedRoles, 1000),
        UserType = make_set(UserType, 1000),
        UserAccountControl = make_set(UserType, 1000)
    by AccountUPN
    | extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName

Stage 16: join

| join kind=leftouter (
    BehaviorAnalytics
    | where ActivityType in ("FailedLogOn", "LogOn")
    | where isnotempty(SourceIPAddress)
    | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress
    | project-rename IPAddress = SourceIPAddress
    | summarize
        UsersInsights = make_set(UsersInsights, 1000),
        DevicesInsights = make_set(DevicesInsights, 1000),
        IPInvestigationPriority = sum(InvestigationPriority)
    by IPAddress)
on IPAddress

Stage 17: extend

extend UEBARiskScore

Stage 18: where

where UEBARiskScore > 20

Stage 19: sort

sort by UEBARiskScore

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
AuthenticationRequirementproject
Categoryproject
ClientAppUsedproject
ConditionalAccessStatusproject
IPAddressproject
LatestAnomalyTimeproject
Locationproject
OperationNameproject
PartialFailedSigninsproject
ResourceDisplayNameproject
ResultDescriptionproject
ResultTypeproject
TotalFailedSigninsproject
UserAgentproject
UserDisplayNameproject
UserPrincipalNameextend
anomaliesproject
baselineproject
scoreproject
Nameextend
UPNSuffixextend
timestampextend
UEBARiskScoreextend

VMware ESXi - Root impersonation

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

'Detects when root impersonation occurs.'

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: 23a3cf72-9497-408e-8144-87958a60d31a
name: VMware ESXi - Root impersonation
description: |
  'Detects when root impersonation occurs.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
  VMwareESXi
  | where SyslogMessage has_all ('ImpersonateUser', 'VcIntegrity', 'root')
  | extend user = 'root'
  | extend AccountCustomEntity = user
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

VMwareESXi

Stage 2: where

| where SyslogMessage has_all ('ImpersonateUser', 'VcIntegrity', 'root')

Stage 3: extend

| extend user = 'root'

Stage 4: extend

| extend AccountCustomEntity = user

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • ImpersonateUser transforms: term
  • VcIntegrity transforms: term
  • root transforms: term
field:"SyslogMessage" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
userextend
AccountCustomEntityextend

Windows Binaries Lolbins Renamed

#
Status
available
Severity
medium
Time window
1h
Group by
CommandLine, Computer, CurrentDirectory, Description, EventID, Hashes, Image, OriginalFileName, ParentCommandLine, ParentImage, ParentProcessGuid, ParentUser, ProcessGuid, User
Source
github.com/Azure/Azure-Sentinel

This query detects the execution of renamed Windows binaries (Lolbins). This is a common technique used by adversaries to evade detection. Ref: https://docs.paloaltonetworks.com/cortex/cortex-xdr/cortex-xdr-analytics-alert-reference/cortex-xdr-analytics-alert-reference/execution-of-renamed-lolbin.html

MITRE ATT&CK Coverage

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 1: Process creation

Rule Body

id: cbf6ad48-fa5c-4bf7-b205-28dbadb91255
name: Windows Binaries Lolbins Renamed
description: | 
  'This query detects the execution of renamed Windows binaries (Lolbins). This is a common technique used by adversaries to evade detection. 
  Ref: https://docs.paloaltonetworks.com/cortex/cortex-xdr/cortex-xdr-analytics-alert-reference/cortex-xdr-analytics-alert-reference/execution-of-renamed-lolbin.html'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  let procList = externaldata(Process:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Microsoft_Lolbas_Execution_Binaries.csv"] with (format="csv", ignoreFirstRecord=True);
  Event
  | where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1
  | parse EventData with * 'Image">' Image "<" * 'OriginalFileName">' OriginalFileName "<" *
  | where OriginalFileName has_any (procList) and not (Image has_any (procList))
  | parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Description">' Description "<" * 'CommandLine">' CommandLine "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'Hashes">' Hashes "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description, OriginalFileName, CurrentDirectory, Hashes
  | extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: CommandLine
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings: 
      - identifier: Name
        columnName: User
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: procList used in Stage 4

let procList = externaldata(Process:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Microsoft_Lolbas_Execution_Binaries.csv"] with (format="csv", ignoreFirstRecord=True);

Stage 1: source

Event

Stage 2: where

| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1

Stage 3: parse

| parse EventData with * 'Image">' Image "<" * 'OriginalFileName">' OriginalFileName "<" *

Stage 4: where

| where OriginalFileName has_any (procList) and not (Image has_any (procList))

Stage 5: parse

| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Description">' Description "<" * 'CommandLine">' CommandLine "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'Hashes">' Hashes "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description, OriginalFileName, CurrentDirectory, Hashes

Stage 7: extend

| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 1 corpus 12 (kusto 11, elastic 1)
field:"EventID" kind:eq value:"1"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 7 (kusto 7)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
OriginalFileNamematch
  • procList transforms: term
field:"OriginalFileName" kind:match value:"procList"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ImagematchprocListexcludes:Image field:"Image" value:"procList"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
Computersummarize
CurrentDirectorysummarize
Descriptionsummarize
EndTimesummarize
EventIDsummarize
Hashessummarize
Imagesummarize
OriginalFileNamesummarize
ParentCommandLinesummarize
ParentImagesummarize
ParentProcessGuidsummarize
ParentUsersummarize
ProcessGuidsummarize
StartTimesummarize
Usersummarize
DnsDomainextend
HostNameextend

Back to all Kusto rules