Detection rules › Kusto

Kusto rules: 3b

BloodHound Attack Path Finding - Add Member Privileges on Tier Zero Security Groups

#

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

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

BloodHound Attack Path Finding - Add Member Privileges on Tier Zero Security Groups

Rule Body

id: 17c3d3b1-42f1-4bbd-bcbd-6ab7621448c9
name: BloodHound Attack Path Finding - Add Member Privileges on Tier Zero Security
  Groups
description: BloodHound Attack Path Finding - Add Member Privileges on Tier Zero Security
  Groups
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0AddMember"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0AddMember"

Indicators

These rows show field, operator, and value matches.

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

BloodHound Attack Path Finding - Tier Zero Computer Vulnerable to Coercion-Based NTLM Relay to ADCS (ESC8) Attack

#

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

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

BloodHound Attack Path Finding - Tier Zero Computer Vulnerable to Coercion-Based NTLM Relay to ADCS (ESC8) Attack

Rule Body

id: 28b86df8-daa1-4b94-939b-903c24d8196d
name: BloodHound Attack Path Finding - Tier Zero Computer Vulnerable to Coercion-Based
  NTLM Relay to ADCS (ESC8) Attack
description: BloodHound Attack Path Finding - Tier Zero Computer Vulnerable to Coercion-Based
  NTLM Relay to ADCS (ESC8) Attack
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0CoerceAndRelayNTLMToADCS"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0CoerceAndRelayNTLMToADCS"

Indicators

These rows show field, operator, and value matches.

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

Cisco Duo - AD sync failed

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

'Detects when AD syncronization failed.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 398dd1cd-3251-49d8-b927-5b93bae4a094
name: Cisco Duo - AD sync failed
description: |
  'Detects when AD syncronization failed.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoDuoActivityConnectorDefinition
    dataTypes:
      - DuoActivity_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1489
query: |
  DuoActivity_CL
  | where ActionName =~ "ad_sync_failed"
  | extend IPCustomEntity = AccessDeviceIp
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

DuoActivity_CL

Stage 2: where

| where ActionName =~ "ad_sync_failed"

Stage 3: extend

| extend IPCustomEntity = AccessDeviceIp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

CTE - License Expiring Soon (30 Days)

#
Severity
medium
Time window
2d
Source
github.com/Azure/Azure-Sentinel

A paid Cybereinforce license is expiring within 30 days - a reminder to plan renewal before it lapses. Emitted once per license term by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); re-arms automatically on renewal. Trial licenses do not get this 30-day reminder, only the 7-day critical one below.

Rule Body

id: ec305a18-2520-443d-9f54-18dea3637a15
name: CTE - License Expiring Soon (30 Days)
description: |
  A paid Cybereinforce license is expiring within 30 days - a reminder to plan renewal before it lapses. Emitted once per license term by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); re-arms automatically on renewal. Trial licenses do not get this 30-day reminder, only the 7-day critical one below.
severity: Medium
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 24h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  []
relevantTechniques: []
query: |
  CybereinforceCTE_CL
  | where EventType == "license.expiring_soon"
  | extend d = parse_json(Details)
  | extend DaysRemaining = toint(d.daysRemaining), EndAt = tostring(d.endAt), Plan = tostring(d.plan), AppName = "Cybereinforce"
  | project TimeGenerated, TenantId, DaysRemaining, EndAt, Plan, AppName, Severity, Category, Source
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AppName
      - identifier: InstanceName
        columnName: TenantId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "license.expiring_soon"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend DaysRemaining = toint(d.daysRemaining), EndAt = tostring(d.endAt), Plan = tostring(d.plan), AppName = "Cybereinforce"

Stage 5: project

| project TimeGenerated, TenantId, DaysRemaining, EndAt, Plan, AppName, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • license.expiring_soon
field:"EventType" kind:eq value:"license.expiring_soon"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppNameproject
Categoryproject
DaysRemainingproject
EndAtproject
Planproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject

Detect Suspicious ncrypt.dll usage with RDP connections to unmanaged or non TPM protected device

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

This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and devices performing RDP connection to unmanaged or non-TPM devices. More information on the attack scenario this is detection is applicable for can be found in the references.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let cli_tools = dynamic(["powershell", "python"]);
// Get suspicious ncrypt.dll usage via WDAC audit policy
let time_lookback = 1h;
let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    // Exclude MDI RDP Connections (known for NNR)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    // Search for RDP connections to non-tpm devices
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = Timestamp
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where Timestamp > ago(time_lookback)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where Timestamp between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project Timestamp, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 
    InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
    NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
    RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported

let time_lookback = 1h;
let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where TimeGenerated > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(TimeGenerated, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where TimeGenerated > ago(time_lookback)
    // Exclude MDI RDP Connections (known for NNR)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    // Search for RDP connections to non-tpm devices
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
    DeviceNetworkEvents
    | where TimeGenerated > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = TimeGenerated
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where TimeGenerated > ago(time_lookback)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where TimeGenerated between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project TimeGenerated, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 
    InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
    NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
    RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported

Stages and Predicates

Parameters

let cli_tools = dynamic(["powershell", "python"]);
let time_lookback = 1h;

Let Binding: no_tpm_devices used in Stage 1

let no_tpm_devices = (
    ExposureGraphNodes
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);

Let Binding: no_tpm_device_info used in Stage 2

let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);

Let Binding: dangerous_rdp_sessions used in Stages 3, 10

let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);

Let Binding: nonce_requests used in Stages 4, 8

let nonce_requests = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = Timestamp
);

Stage 1: source

let no_tpm_devices

Stage 2: source

let no_tpm_device_info

Stage 3: source

let dangerous_rdp_sessions

Stage 4: source

let nonce_requests

Stage 5: source

DeviceEvents

Stage 6: where

where Timestamp > ago(3600s)

Stage 7: where

where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"

Stage 8: join

join kind=inner (nonce_requests) on InitiatingProcessId, DeviceId

Stage 9: where

where /* macro: (Timestamp between (todatetime((NonceRequestTimestamp - 10m)) .. todatetime((NonceRequestTimestamp + 10m)))) */

Stage 10: join

join kind=inner (dangerous_rdp_sessions) on DeviceId

Stage 11: where

where not (InitiatingProcessFileName in ("backgroundtaskhost.exe", "svchost.exe"))

Stage 12: extend

extend WdacPolicyName

Stage 13: project

project ActionType, DeviceName, FileName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, InitiatingProcessSHA1, NonceRequestTimestamp, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteDeviceOnboardingStatus, RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported, RdpRemoteMacAddress, RdpTimeGenerated, Timestamp, WdacPolicyName

Stage 14: summarize aggregation inside the join branch

summarize by DeviceId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize

DomainTools Newly Observed Domains Feeds Ingested

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

Creates an incident whenever DomainTools Newly Observed Domains new Feeds (domains) are ingested in the last 1 hour.

Rule Body

id: f8c223c1-1a28-4b28-8e9f-5e35b610916b
name: DomainTools Newly Observed Domains Feeds Ingested
description: Creates an incident whenever DomainTools Newly Observed Domains new Feeds (domains) are ingested in the last 1 hour.
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: DomainTools
    dataTypes:
      - DomainToolsThreatIntelDomains_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "DomainToolsThreatIntelDomains(1h, \"nod\")"
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainCustomEntity
eventGroupingSettings:
  aggregationKind: SingleAlert
version: 1.0.0

Stages and Predicates

Stage 1: source

DomainToolsThreatIntelDomains(1h, "nod")

Insider Risk_Microsoft Purview Insider Risk Management Alert Observed

#

This is a third-party alert feed, not a detection over modeled telemetry. Another security product raised the finding; this rule forwards or reshapes 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
6h
Group by
SystemAlertId, UPN
Source
github.com/Azure/Azure-Sentinel

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

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

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

Stages and Predicates

Let Binding: AlertLinks used in Stage 7

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

Stage 1: source

SecurityAlert

Stage 2: where

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

Stage 3: summarize

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

Stage 4: mv-expand

| mv-expand todynamic(Entities)

Stage 5: where

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

Stage 6: extend (3 consecutive steps)

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

Stage 7: join

| join kind=inner (AlertLinks) on UPN

Stage 8: extend

extend UserPrincipalName

Stage 9: extend

extend PreviousAlertsLinks

Stage 10: project

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

Stage 11: sort

sort by TimeGenerated

Stage 12: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Entities["Type"]eq
  • account transforms: kusto_dynamic_path
ProductNameeq
  • Microsoft 365 Insider Risk Management
field:"ProductName" kind:eq value:"Microsoft 365 Insider Risk Management"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertLinkproject
AlertNameproject
PreviousAlertsLinksproject
ProductNameproject
Statusproject
Tacticsproject
TimeGeneratedproject
UserPrincipalNameproject
AccountNameextend
AccountUPNSuffixextend

Job No Longer Used as Second Destination

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

Detects when a job used as a secondary destination is removed.

Rule Body

id: 61c576ae-f81a-4151-8524-b08c86f206a8
name: Job No Longer Used as Second Destination
description: Detects when a job used as a secondary destination is removed.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 23420\n| extend JobName = \
  \ extract(\"JobName=\\\"(?<JobName>[^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n\
  \    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource\
  \ = original_host,\n    EventId = instanceId,\n    UserName = user,\n    [\"Job\
  \ Name\"] = JobName,\n    MessageDetails = Description,\n    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 23420

Stage 3: extend

| extend JobName =  extract("JobName=\"(?<JobName>[^\"]*)\"", 1, SyslogMessage)

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = user,
    ["Job Name"] = JobName,
    MessageDetails = Description,
    Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
Job Nameproject
MessageDetailsproject
Severityproject
UserNameproject

New direct access policy was granted against organizational policy

#

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

Status
available
Severity
low
Time window
30m
Source
github.com/Azure/Azure-Sentinel

This policy detects when access was granted directly (not via groups). This policy is defined by default by Authomize to track AWS only. It is possible to edit the existing policy or create more versions to track other apps.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Privilege Escalation

Rule Body

id: d7ee7bb5-d712-4d44-b201-b13379924934
name: New direct access policy was granted against organizational policy
kind: Scheduled
description: This policy detects when access was granted directly (not via groups). This policy is defined by default by Authomize to track AWS only. It is possible to edit the existing policy or create more versions to track other apps.
severity: Low
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1078
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "New direct access policy was granted against organizational policy"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - New direct access policy was granted against organizational policy
  alertDescriptionFormat: |
    New direct access policy was granted against organizational policy. This policy detects when access was granted directly (not via groups). This policy is defined by default by Authomize to track AWS only. It is possible to edit the existing policy or create more versions to track other apps.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "New direct access policy was granted against organizational policy"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • New direct access policy was granted against organizational policy transforms: term
field:"policy_name_s" kind:match value:"New direct access policy was granted against organizational policy"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Palo Alto - potential beaconing detected

#
Status
available
Severity
low
Time window
1h
Group by
FirewallName_s, Message, TimeDeltainSeconds
Source
github.com/Azure/Azure-Sentinel

'Identifies beaconing patterns from Palo Alto Network traffic logs based on recurrent timedelta patterns. The query leverages various KQL functions to calculate time deltas and then compares it with total events observed in a day to find percentage of beaconing. This outbound beaconing pattern to untrusted public networks should be investigated for any malware callbacks or data exfiltration attempts. Reference Blog: http://www.austintaylor.io/detect/beaconing/intrusion/detection/system/command/control/flare/elastic/stack/2017/06/10/detect-beaconing-with-flare-elasticsearch-and-intrusion-detection-systems/ https://techcommunity.microsoft.com/t5/microsoft-sentinel-blog/detect-network-beaconing-via-intra-request-time-delta-patterns/ba-p/779586'

MITRE ATT&CK Coverage

Rule Body

id: f0be259a-34ac-4946-aa15-ca2b115d5feb
name: Palo Alto - potential beaconing detected
description: |
  'Identifies beaconing patterns from Palo Alto Network traffic logs based on recurrent timedelta patterns.
  The query leverages various KQL functions to calculate time deltas and then compares it with total events observed in a day to find percentage of beaconing.
  This outbound beaconing pattern to untrusted public networks should be investigated for any malware callbacks or data exfiltration attempts.
  Reference Blog:
  http://www.austintaylor.io/detect/beaconing/intrusion/detection/system/command/control/flare/elastic/stack/2017/06/10/detect-beaconing-with-flare-elasticsearch-and-intrusion-detection-systems/
  https://techcommunity.microsoft.com/t5/microsoft-sentinel-blog/detect-network-beaconing-via-intra-request-time-delta-patterns/ba-p/779586'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureCloudNGFWByPaloAltoNetworks
    dataTypes:
      - fluentbit_CL
queryFrequency: 1d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
  - T1571
query: |
  let starttime = 2d;
  let endtime = 1d;
  let TimeDeltaThreshold = 25;
  let TotalEventsThreshold = 30;
  let MostFrequentTimeDeltaThreshold = 25;
  let PercentBeaconThreshold = 80;
  fluentbit_CL
  | where ident_s == "TRAFFIC"
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where ipv4_is_private(tostring(parse_json(Message).dst_ip)) == false
  | project TimeGenerated, FirewallName_s, parse_json(Message).src_ip, parse_json(Message).sport, parse_json(Message).dst_ip, parse_json(Message).dport,Message
  | sort by tostring(parse_json(Message).src_ip) asc, TimeGenerated asc, tostring(parse_json(Message).dst_ip) asc, tostring(parse_json(Message).dport) asc
  | extend src_ip=tostring(parse_json(Message).src_ip)
  | serialize
  | extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(src_ip, 1)
  | extend TimeDeltainSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
  | where parse_json(Message).src_ip == nextSourceIP
  | where TimeDeltainSeconds > TimeDeltaThreshold
  | summarize count() by TimeDeltainSeconds, bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message
  | summarize (MostFrequentTimeDeltaCount, MostFrequentTimeDeltainSeconds) = arg_max(count_, TimeDeltainSeconds), TotalEvents=sum(count_)
  by bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message
  | where TotalEvents > TotalEventsThreshold and MostFrequentTimeDeltaCount > MostFrequentTimeDeltaThreshold
  | extend BeaconPercent = MostFrequentTimeDeltaCount/toreal(TotalEvents) * 100
  | where BeaconPercent > PercentBeaconThreshold
  | extend IPAddress = tostring(parse_json(Message).dst_ip)
  | extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: FirewallName_s
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 2d;
let endtime = 1d;
let TimeDeltaThreshold = 25;
let TotalEventsThreshold = 30;
let MostFrequentTimeDeltaThreshold = 25;
let PercentBeaconThreshold = 80;

Stage 1: source

fluentbit_CL

Stage 2: where

| where ident_s == "TRAFFIC"

Stage 3: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 4: where

| where ipv4_is_private(tostring(parse_json(Message).dst_ip)) == false

Stage 5: project

| project TimeGenerated, FirewallName_s, parse_json(Message).src_ip, parse_json(Message).sport, parse_json(Message).dst_ip, parse_json(Message).dport,Message

Stage 6: sort

| sort by tostring(parse_json(Message).src_ip) asc, TimeGenerated asc, tostring(parse_json(Message).dst_ip) asc, tostring(parse_json(Message).dport) asc

Stage 7: extend

| extend src_ip=tostring(parse_json(Message).src_ip)

Stage 8: kusto:serialize

| serialize

Stage 9: extend

| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(src_ip, 1)

Stage 10: extend

| extend TimeDeltainSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)

Stage 11: where

| where parse_json(Message).src_ip == nextSourceIP

Stage 12: where

| where TimeDeltainSeconds > TimeDeltaThreshold

Stage 13: summarize

| summarize count() by TimeDeltainSeconds, bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message

Stage 14: summarize

| summarize (MostFrequentTimeDeltaCount, MostFrequentTimeDeltainSeconds) = arg_max(count_, TimeDeltainSeconds), TotalEvents=sum(count_)
by bin(TimeGenerated, 1h), FirewallName_s, tostring(parse_json(Message).src_ip), tostring(parse_json(Message).dst_ip), tostring(parse_json(Message).dport),Message

Stage 15: where

| where TotalEvents > TotalEventsThreshold and MostFrequentTimeDeltaCount > MostFrequentTimeDeltaThreshold

Stage 16: extend

| extend BeaconPercent = MostFrequentTimeDeltaCount/toreal(TotalEvents) * 100

Stage 17: where

| where BeaconPercent > PercentBeaconThreshold

Stage 18: extend (3 consecutive steps)

| extend IPAddress = tostring(parse_json(Message).dst_ip)
| extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
BeaconPercentgt
  • 80 transforms: number
field:"BeaconPercent" kind:gt value:"80"
Message.src_ipeq
  • nextSourceIP transforms: kusto_accessor_expr:{"expression":"parse_json(Message).src_ip","field":"Message.src_ip"}, kusto_dynamic_path
field:"Message.src_ip" kind:eq value:"nextSourceIP"
MostFrequentTimeDeltaCountgt
  • 25 transforms: number
field:"MostFrequentTimeDeltaCount" kind:gt value:"25"
TimeDeltainSecondsgt
  • 25 transforms: number
field:"TimeDeltainSeconds" kind:gt value:"25"
TotalEventsgt
  • 30 transforms: number
field:"TotalEvents" kind:gt value:"30"
ident_seq
  • TRAFFIC
field:"ident_s" kind:eq value:"TRAFFIC"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Message.dst_ipcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:Message.dst_ip

Output Fields

These fields are emitted when the rule matches.

FieldSource
FirewallName_ssummarize
Messagesummarize
MostFrequentTimeDeltaCountsummarize
MostFrequentTimeDeltainSecondssummarize
TotalEventssummarize
BeaconPercentextend
IPAddressextend
DomainIndexextend
HostNameextend
HostNameDomainextend

Pathlock TDnR - SAP Read Access Logging Data

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

Detects SAP Read Access Logging (RAL) data events capturing actual sensitive data access, forwarded by Pathlock Threat Detection and Response. RAL data events record when users access sensitive fields (e.g. salary data, personal information) and may indicate data harvesting or insider threat activity.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c44
name: Pathlock TDnR - SAP Read Access Logging Data
kind: Scheduled
description: >-
  Detects SAP Read Access Logging (RAL) data events capturing actual sensitive data access,
  forwarded by Pathlock Threat Detection and Response. RAL data events record when users access
  sensitive fields (e.g. salary data, personal information) and may indicate data harvesting
  or insider threat activity.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Exfiltration
relevantTechniques:
  - T1213
  - T1048
query: |
  Pathlock_TDnR_CL
  | where DataSource == "RAL_DATA"
  | 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 == "RAL_DATA"

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

PROD (TM007.1) - OU - Created or Deleted at Tier Level

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

Creation or Deletion of an OU within the Tier Model

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 4e027b5f-fc4a-4de6-be24-3eb9324e2fc8
name: 'PROD (TM007.1) - OU - Created or Deleted at Tier Level'
description: |
  Creation or Deletion of an OU within the Tier Model
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1222.001
query: |
  SecurityEvent 
  | where (EventID == 5137
      or EventID == 5141)
      and (EventData matches regex @"(?i)OU=Domain Controllers"
          or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
          or EventData matches regex @"(?i)OU=Tier Model Administration"
          or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
      and EventData has '"ObjectClass">organizationalUnit'
  | extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),(?i)DC=", 1, EventData)
  | extend ObjectTier = case(
      ObjectName has "Tier 0", "Tier 0",
      ObjectName has "Tier 1", "Tier 1",
      ObjectName has "Tier 2", "Tier 2",
      ObjectName has "Tier Model Administration", "Tier 0",
      ObjectName has "Tier Model Computer Quarantine", "Tier 0",
      ObjectName has "Domain Controllers", "Tier 0",
      "Unknown")
  | extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
  | extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
  | extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
  | extend SubjectUserName = extract("<Data Name=\"SubjectUserName\">([^<]+)</Data>", 1, EventData)
  | extend SubjectDomainName = extract("<Data Name=\"SubjectDomainName\">([^<]+)</Data>", 1, EventData)
  | extend Account = strcat(SubjectDomainName, "\\", SubjectUserName)
  | project TimeGenerated
     , ObjectName
     , ObjectTier
     , ObjectClass
     , ActivityType
     , Account
     , Domain
     , Computer
     , Channel
     , EventID
     , EventData
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ObjectName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
customDetails:
  ObjectName: ObjectName
  ObjectTier: ObjectTier
  ObjectClass: ObjectClass
  ActivityType: ActivityType
  Account: Account
  Domain: Domain
  Computer: Computer
  Channel: Channel
  EventID: EventID
  EventData: EventData
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: '(TM007.1) A {{ObjectTier}} OU was {{ActivityType}}'
  alertDescriptionFormat: 'A OU was deleted or created within the Tier Model.'
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where (EventID == 5137
    or EventID == 5141)
    and (EventData matches regex @"(?i)OU=Domain Controllers"
        or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
        or EventData matches regex @"(?i)OU=Tier Model Administration"
        or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
    and EventData has '"ObjectClass">organizationalUnit'

Stage 3: extend (8 consecutive steps)

| extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),(?i)DC=", 1, EventData)
| extend ObjectTier = case(
    ObjectName has "Tier 0", "Tier 0",
    ObjectName has "Tier 1", "Tier 1",
    ObjectName has "Tier 2", "Tier 2",
    ObjectName has "Tier Model Administration", "Tier 0",
    ObjectName has "Tier Model Computer Quarantine", "Tier 0",
    ObjectName has "Domain Controllers", "Tier 0",
    "Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend SubjectUserName = extract("<Data Name=\"SubjectUserName\">([^<]+)</Data>", 1, EventData)
| extend SubjectDomainName = extract("<Data Name=\"SubjectDomainName\">([^<]+)</Data>", 1, EventData)
| extend Account = strcat(SubjectDomainName, "\\", SubjectUserName)

Stage 4: project

| project TimeGenerated
   , ObjectName
   , ObjectTier
   , ObjectClass
   , ActivityType
   , Account
   , Domain
   , Computer
   , Channel
   , EventID
   , EventData

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDatamatch
  • "ObjectClass">organizationalUnit transforms: term corpus 5 (kusto 5)
field:"EventData" kind:match
EventDataregex_match
  • (?i)OU=Domain Controllers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 1 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 2 End-User Accounts corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Devices corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Groups corpus 3 (kusto 3)
  • (?i)OU=Tier Model Administration corpus 3 (kusto 3)
  • (?i)OU=Tier Model Computer Quarantine corpus 3 (kusto 3)
field:"EventData" kind:regex_match
EventIDeq
  • 5137 corpus 8 (kusto 5, elastic 3)
  • 5141 corpus 2 (kusto 2)
field:"EventID" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
ObjectClassproject
ObjectNameproject
ObjectTierproject
TimeGeneratedproject

ProofpointPOD - Binary file in attachment

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

'Detects when email received with binary file as attachment.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: eb68b129-5f17-4f56-bf6d-dde48d5e615a
name: ProofpointPOD - Binary file in attachment
description: |
  'Detects when email received with binary file as attachment.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ProofpointPOD
    dataTypes:
      - ProofpointPOD_message_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let lbtime = 10m;
  let binaryTypes = dynamic(['zip', 'octet-stream', 'java-archive', 'rar', 'tar', 'x-7z-compressed', 'x-msdownload', 'portable-executable']);
  ProofpointPOD
  | where TimeGenerated > ago(lbtime)
  | where EventType == 'message'
  | where NetworkDirection == 'inbound'
  | where FilterDisposition !in ('reject', 'discard')
  | extend attachedMimeType = tostring(todynamic(MsgParts)[0]['detectedMime'])
  | where attachedMimeType has_any (binaryTypes)
  | project SrcUserUpn, AccountCustomEntity = tostring(parse_json(DstUserUpn)[0]), attachedMimeType, MsgHeaderSubject
  | extend Name = tostring(split(AccountCustomEntity, "@")[0]), UPNSuffix = tostring(split(AccountCustomEntity, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let lbtime = 10m;
let binaryTypes = dynamic(['zip', 'octet-stream', 'java-archive', 'rar', 'tar', 'x-7z-compressed', 'x-msdownload', 'portable-executable']);

Stage 1: source

ProofpointPOD

Stage 2: where

| where TimeGenerated > ago(lbtime)

Stage 3: where

| where EventType == 'message'

Stage 4: where

| where NetworkDirection == 'inbound'

Stage 5: where

| where FilterDisposition !in ('reject', 'discard')

Stage 6: extend

| extend attachedMimeType = tostring(todynamic(MsgParts)[0]['detectedMime'])

Stage 7: where

| where attachedMimeType has_any (binaryTypes)

Stage 8: project

| project SrcUserUpn, AccountCustomEntity = tostring(parse_json(DstUserUpn)[0]), attachedMimeType, MsgHeaderSubject

Stage 9: extend

| extend Name = tostring(split(AccountCustomEntity, "@")[0]), UPNSuffix = tostring(split(AccountCustomEntity, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • message
field:"EventType" kind:eq value:"message"
NetworkDirectioneq
  • inbound
field:"NetworkDirection" kind:eq value:"inbound"
attachedMimeTypematch
  • java-archive transforms: term
  • octet-stream transforms: term
  • portable-executable transforms: term
  • rar transforms: term
  • tar transforms: term
  • x-7z-compressed transforms: term
  • x-msdownload transforms: term
  • zip transforms: term
field:"attachedMimeType" kind:match

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityproject
MsgHeaderSubjectproject
SrcUserUpnproject
attachedMimeTypeproject
Nameextend
UPNSuffixextend

Ransomware Client Blocked

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

Identifies malicious clients blocked by the Ransomware Protection service running on a Nasuni Edge Appliance.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 0c96a5a2-d60d-427d-8399-8df7fe8e6536
name: Ransomware Client Blocked
description: 'Identifies malicious clients blocked by the Ransomware Protection service running on a Nasuni Edge Appliance.'
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Impact
relevantTechniques:
- T1486
query: |-
  Syslog
  | project TimeGenerated, Computer, SyslogMessage
  | where SyslogMessage has "The Filer has enforced the mitigation policy on volume"
  | extend SrcIpAddr = substring(SyslogMessage, indexof(SyslogMessage, "(")+1, indexof(SyslogMessage, ")") - indexof(SyslogMessage,"(")-1)
  | extend volume_name = substring(SyslogMessage, indexof(SyslogMessage, "volume")+7, indexof(SyslogMessage,"and") - (indexof(SyslogMessage, "volume")+7))
  | sort by TimeGenerated desc
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertnameFormat: 'Nasuni: Ransomware Client Blocked'
  alertDescriptionFormat: Nasuni has blocked a client involved in a ransomware attack from accessing a Nasuni Edge Appliance at {{TimeGenerated}}
customDetails:
  VolumeName: volume_name
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.3

Stages and Predicates

Stage 1: source

Syslog

Stage 2: project

| project TimeGenerated, Computer, SyslogMessage

Stage 3: where

| where SyslogMessage has "The Filer has enforced the mitigation policy on volume"

Stage 4: extend

| extend SrcIpAddr = substring(SyslogMessage, indexof(SyslogMessage, "(")+1, indexof(SyslogMessage, ")") - indexof(SyslogMessage,"(")-1)

Stage 5: extend

| extend volume_name = substring(SyslogMessage, indexof(SyslogMessage, "volume")+7, indexof(SyslogMessage,"and") - (indexof(SyslogMessage, "volume")+7))

Stage 6: sort

| sort by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • The Filer has enforced the mitigation policy on volume transforms: term
field:"SyslogMessage" kind:match value:"The Filer has enforced the mitigation policy on volume"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
SyslogMessageproject
TimeGeneratedproject
SrcIpAddrextend
volume_nameextend

SSH Credentials Changed

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

Detects when SSH credentials are updated.

Rule Body

id: 40b8e739-1077-42d8-9501-b68736c2c4d2
name: SSH Credentials Changed
description: Detects when SSH credentials are updated.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 31900\n| extend RelatedCredsName\
  \ = extract(\"RelatedCredsName=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n\
  \    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource\
  \ = original_host,\n    EventId = instanceId,\n    [\"User Name\"] = user,\n   \
  \ [\"Credential Record\"] = RelatedCredsName,\n    MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 31900

Stage 3: extend

| extend RelatedCredsName = extract("RelatedCredsName=\"([^\"]*)\"", 1, SyslogMessage)

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
    ["Credential Record"] = RelatedCredsName,
    MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Credential Recordproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Suspicious application consent similar to O365 Attack Toolkit

#
Status
available
Severity
high
Time window
14d
Group by
AppClientId, CorrelationId
Source
github.com/Azure/Azure-Sentinel

This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the MDSec O365 Attack Toolkit (https://github.com/mdsecactivebreach/o365-attack-toolkit). The default permissions/scope for the MDSec O365 Attack toolkit change sometimes but often include contacts.read, user.read, mail.read, notes.read.all, mailboxsettings.readwrite, files.readwrite.all, mail.send, files.read, and files.read.all. Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: f948a32f-226c-4116-bddd-d95e91d97eb9
name: Suspicious application consent similar to O365 Attack Toolkit
description: |
  'This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the MDSec O365 Attack Toolkit (https://github.com/mdsecactivebreach/o365-attack-toolkit).
  The default permissions/scope for the MDSec O365 Attack toolkit change sometimes but often include contacts.read, user.read, mail.read, notes.read.all, mailboxsettings.readwrite, files.readwrite.all, mail.send, files.read, and files.read.all.
  Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1528
  - T1550
query: |
  let detectionTime = 1d;
  let joinLookback = 14d;
  let threshold = 5;
  let o365_attack_regex = "contacts.read|user.read|mail.read|notes.read.all|mailboxsettings.readwrite|Files.ReadWrite.All|mail.send|files.read|files.read.all";
  let o365_attack = dynamic(["contacts.read", "user.read", "mail.read", "notes.read.all", "mailboxsettings.readwrite", "Files.ReadWrite.All", "mail.send", "files.read", "files.read.all"]);
  AuditLogs
  | where TimeGenerated > ago(detectionTime)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Consent to application"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal"
        | extend AppDisplayName = tostring(TargetResource.displayName),
                 AppClientId = tostring(TargetResource.id),
                 props = TargetResource.modifiedProperties
    )
  | where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv"))) // NOTE: a MATCH from this list will cause the alert to NOT fire - please modify for your environment!
  | mv-apply ConsentFull = props on 
    (
        where ConsentFull.displayName =~ "ConsentAction.Permissions"
    )
  | parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 ", CreatedDateTime" * "]" *
  | where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
  | where ConsentFull has_any (o365_attack)  
  | extend GrantScopeCount = countof(tolower(GrantScope1), o365_attack_regex, 'regex')
  | where GrantScopeCount > threshold
  | extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
  | extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
  | extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
  | extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend GrantUserAgent = AdditionalDetail.value
    )
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
  | join kind = leftouter (AuditLogs
    | where TimeGenerated > ago(joinLookback)
    | where LoggedByService =~ "Core Directory"
    | where Category =~ "ApplicationManagement"
    | where OperationName =~ "Add service principal"
    | mv-apply TargetResource = TargetResources on 
        (
            where TargetResource.type =~ "ServicePrincipal"
            | extend props = TargetResource.modifiedProperties,
                    AppClientId = tostring(TargetResource.id)
        )
    | mv-apply Property = props on 
        (
            where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
            | extend AppReplyURLs = trim('"',tostring(Property.newValue))
        )
    | distinct AppClientId, tostring(AppReplyURLs)
  ) on AppClientId
  | join kind = innerunique (AuditLogs
        | where TimeGenerated > ago(joinLookback)
        | where LoggedByService =~ "Core Directory"
        | where Category =~ "ApplicationManagement"
        | where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
            | mv-apply TargetResource = TargetResources on 
            (
                where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
                | extend GrantAuthentication = tostring(TargetResource.displayName)
            )
        | extend GrantOperation = OperationName
        | project GrantAuthentication, GrantOperation, CorrelationId
    ) on CorrelationId
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
  | extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: GrantInitiatedByUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: GrantIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AppDisplayName
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let detectionTime = 1d;
let joinLookback = 14d;
let threshold = 5;

Let Binding: o365_attack_regex used in Stage 12

let o365_attack_regex = "contacts.read|user.read|mail.read|notes.read.all|mailboxsettings.readwrite|Files.ReadWrite.All|mail.send|files.read|files.read.all";

Let Binding: o365_attack used in Stage 11

let o365_attack = dynamic(["contacts.read", "user.read", "mail.read", "notes.read.all", "mailboxsettings.readwrite", "Files.ReadWrite.All", "mail.send", "files.read", "files.read.all"]);

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(detectionTime)

Stage 3: where

| where LoggedByService =~ "Core Directory"

Stage 4: where

| where Category =~ "ApplicationManagement"

Stage 5: where

| where OperationName =~ "Consent to application"

Stage 6: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal"
      | extend AppDisplayName = tostring(TargetResource.displayName),
               AppClientId = tostring(TargetResource.id),
               props = TargetResource.modifiedProperties
  )

Stage 7: where

| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))

Stage 8: kusto:mv-apply

| mv-apply ConsentFull = props on 
  (
      where ConsentFull.displayName =~ "ConsentAction.Permissions"
  )

Stage 9: parse

| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 ", CreatedDateTime" * "]" *

Stage 10: where

| where GrantConsentType != "AllPrincipals"

Stage 11: where

| where ConsentFull has_any (o365_attack)

Stage 12: extend

| extend GrantScopeCount = countof(tolower(GrantScope1), o365_attack_regex, 'regex')

Stage 13: where

| where GrantScopeCount > threshold

Stage 14: extend (6 consecutive steps)

| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)

Stage 15: kusto:mv-apply

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

Stage 16: project

| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId

Stage 17: join

| join kind = leftouter (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add service principal"
  | mv-apply TargetResource = TargetResources on 
      (
          where TargetResource.type =~ "ServicePrincipal"
          | extend props = TargetResource.modifiedProperties,
                  AppClientId = tostring(TargetResource.id)
      )
  | mv-apply Property = props on 
      (
          where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
          | extend AppReplyURLs = trim('"',tostring(Property.newValue))
      )
  | distinct AppClientId, tostring(AppReplyURLs)
) on AppClientId

Stage 18: join

| join kind = innerunique (AuditLogs
      | where TimeGenerated > ago(joinLookback)
      | where LoggedByService =~ "Core Directory"
      | where Category =~ "ApplicationManagement"
      | where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
          | mv-apply TargetResource = TargetResources on 
          (
              where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
              | extend GrantAuthentication = tostring(TargetResource.displayName)
          )
      | extend GrantOperation = OperationName
      | project GrantAuthentication, GrantOperation, CorrelationId
  ) on CorrelationId

Stage 19: project

project AppClientId, AppDisplayName, AppReplyURLs, ConsentFull, CorrelationId, GrantAuthentication, GrantConsentType, GrantInitiatedBy, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantInitiatedByUserPrincipalName, GrantIpAddress, GrantOperation, GrantScope1, GrantUserAgent, OperationName, TimeGenerated

Stage 20: extend

extend Name, UPNSuffix

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"
Categoryeq
  • ApplicationManagement corpus 13 (kusto 13)
field:"Category" kind:eq value:"ApplicationManagement"
ConsentFullmatch
  • Files.ReadWrite.All transforms: term
  • contacts.read transforms: term
  • files.read transforms: term
  • files.read.all transforms: term
  • mail.read transforms: term
  • mail.send transforms: term
  • mailboxsettings.readwrite transforms: term
  • notes.read.all transforms: term
  • user.read transforms: term
field:"ConsentFull" kind:match
ConsentFull.displayNameeq
  • ConsentAction.Permissions transforms: kusto_dynamic_path
field:"ConsentFull.displayName" kind:eq value:"ConsentAction.Permissions"
GrantConsentTypene
  • AllPrincipals
field:"GrantConsentType" kind:ne value:"AllPrincipals"
GrantScopeCountgt
  • 5 transforms: number
field:"GrantScopeCount" kind:gt value:"5"
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
  • Core Directory
field:"azure_ad::logged_by_service" kind:eq
OperationNameeq
  • Add OAuth2PermissionGrant
  • Add delegated permission grant
  • Add service principal
  • Consent to application corpus 5 (kusto 5)
field:"OperationName" kind:eq
Property.displayNameeq
  • AppAddress transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"AppAddress"
Property.newValuematch
  • AddressType transforms: term, kusto_dynamic_path
field:"Property.newValue" kind:match value:"AddressType"
TargetResource.displayNameis_not_null
  • (no value, null check)
field:"TargetResource.displayName" kind:is_not_null
TargetResource.typeeq
  • ServicePrincipal transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"ServicePrincipal"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppClientIdproject
AppDisplayNameproject
AppReplyURLsproject
ConsentFullproject
CorrelationIdproject
GrantAuthenticationproject
GrantConsentTypeproject
GrantInitiatedByproject
GrantInitiatedByAadUserIdproject
GrantInitiatedByAppNameproject
GrantInitiatedByAppServicePrincipalIdproject
GrantInitiatedByUserPrincipalNameproject
GrantIpAddressproject
GrantOperationproject
GrantScope1project
GrantUserAgentproject
OperationNameproject
TimeGeneratedproject
Nameextend
UPNSuffixextend

Tailscale Premium: Subnet router throughput anomaly

#
Status
available
Severity
low
Time window
8d
Group by
NodeId, SrcNodeName, SrcOs, SrcTags, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control
Exfiltration

Rule Body

id: a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d
name: "Tailscale Premium: Subnet router throughput anomaly"
description: |
  Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
relevantTechniques:
  - T1572
  - T1041
query: |
  let baselineDays = 7d;
  let recent = 1h;
  let multiplier = 3.0;
  let recentTraffic =
      Tailscale_Network_CL
      | where TimeGenerated > ago(recent)
      | where HasSubnetTraffic
      | mv-expand t = SubnetTraffic
      | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags);
  let baseline =
      Tailscale_Network_CL
      | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent))
      | where HasSubnetTraffic
      | mv-expand t = SubnetTraffic
      | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize TotalBaselineBytes = sum(Bytes) by NodeId
      | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0;
  recentTraffic
  | join kind=inner baseline on NodeId
  | where RecentBytes > BaselineHourlyBytes * multiplier
  | extend Multiplier = round(RecentBytes / BaselineHourlyBytes, 1), RecentMB = round(RecentBytes / 1024.0 / 1024.0, 2), BaselineHourlyMB = round(BaselineHourlyBytes / 1024.0 / 1024.0, 2)
  | project NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags, RecentMB, BaselineHourlyMB, Multiplier
  | order by Multiplier desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let baselineDays = 7d;
let recent = 1h;
let multiplier = 3.0;

let recentTraffic is inlined into the numbered stages below.

Let Binding: baseline used in Stage 7

let baseline = Tailscale_Network_CL
    | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent))
    | where HasSubnetTraffic
    | mv-expand t = SubnetTraffic
    | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
    | summarize TotalBaselineBytes = sum(Bytes) by NodeId
    | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0;

Stages 1 to 6 define let recentTraffic as the rule's main pipeline source. Stages 7 to 11 use it.

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(recent)

Stage 3: where

| where HasSubnetTraffic

Stage 4: mv-expand

| mv-expand t = SubnetTraffic

Stage 5: extend

| extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)

Stage 6: summarize

| summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags)

Stage 7: join

recentTraffic
| join kind=inner baseline on NodeId

Stage 8: where

where RecentBytes > (BaselineHourlyBytes * 3.0)

Stage 9: extend

extend BaselineHourlyMB, Multiplier, RecentMB

Stage 10: project

project BaselineHourlyMB, Multiplier, NodeId, RecentMB, SrcNodeName, SrcOs, SrcTags, SrcUser

Stage 11: sort

sort by Multiplier

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
HasSubnetTrafficeq
  • true transforms: boolean
field:"HasSubnetTraffic" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
BaselineHourlyMBproject
Multiplierproject
NodeIdproject
RecentMBproject
SrcNodeNameproject
SrcOsproject
SrcTagsproject
SrcUserproject

Tailscale: Device Tailscale SSH newly enabled

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

Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Lateral Movement

Rule Body

id: f0a1b2c3-4567-8901-23de-f12345670041
name: "Tailscale: Device Tailscale SSH newly enabled"
description: |
  Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - LateralMovement
relevantTechniques:
  - T1021
  - T1098
query: |
  let recent =
      Tailscale_Devices_CL
      | where TimeGenerated > ago(1h)
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where SshEnabled == true
      | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen;
  let prior =
      Tailscale_Devices_CL
      | where TimeGenerated between (ago(2d) .. ago(1h))
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where SshEnabled == true
      | distinct DeviceId;
  recent
  | join kind=leftanti prior on DeviceId
  | project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, LastSeen
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

let recent is inlined into the numbered stages below.

Let Binding: prior used in Stage 6

let prior = Tailscale_Devices_CL
    | where TimeGenerated between (ago(2d) .. ago(1h))
    | summarize arg_max(TimeGenerated, *) by DeviceId
    | where SshEnabled == true
    | distinct DeviceId;

Stages 1 to 5 define let recent as the rule's main pipeline source. Stages 6 to 7 use it.

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where SshEnabled == true

Stage 5: project

| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen

Stage 6: join (negated)

join kind=leftanti (prior) on DeviceId

Stage 7: project

project ClientVersion, DeviceName, Hostname, LastSeen, Os, TimeGenerated, User

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SshEnabledeqtrueexcludes:SshEnabled field:"SshEnabled" value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientVersionproject
DeviceNameproject
Hostnameproject
LastSeenproject
Osproject
TimeGeneratedproject
Userproject

Theom - Least priv large value shadow DB

#

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

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

"Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0032 (Theom has observed shadow (or clone) databases/tables that have a large financial value. Additionally, it has observed roles that are overprovisioned for these data stores. As per this requirement, use this information to apply data access control lists or access permissions and enforce data retention policies)"

MITRE ATT&CK Coverage

Rule Body

id: 67b9ff50-5393-49d5-b66f-05b33e2f35d2
name: Theom - Least priv large value shadow DB
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0032 (Theom has observed shadow (or clone) databases/tables that have a large financial value. Additionally, it has observed roles that are overprovisioned for these data stores. As per this requirement, use this information to apply data access control lists or access permissions and enforce data retention policies)"
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1560
  - T1530
query: |
   TheomAlerts_CL
     | where customProps_RuleId_s == "TRIS0032" and (priority_s == "P1" or priority_s == "P2")
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

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

Indicators

These rows show field, operator, and value matches.

Back to all Kusto rules