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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| 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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0CoerceAndRelayNTLMToADCS" |
Cisco Duo - AD sync failed
#'Detects when AD syncronization failed.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionName | eq |
| field:"ActionName" kind:eq value:"ad_sync_failed" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
CTE - License Expiring Soon (30 Days)
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"license.expiring_soon" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppName | project |
Category | project |
DaysRemaining | project |
EndAt | project |
Plan | project |
Severity | project |
Source | project |
TenantId | project |
TimeGenerated | project |
Detect Suspicious ncrypt.dll usage with RDP connections to unmanaged or non TPM protected device
#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
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Lateral Movement |
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
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
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
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"ConnectionSuccess" |
ActionType | starts_with |
| field:"ActionType" kind:starts_with value:"AppControl" |
EntityIds.type | eq |
| field:"EntityIds.type" kind:eq value:"DeviceInventoryId" |
FileName | eq |
| field:"FileName" kind:eq value:"ncrypt.dll" |
InitiatingProcessFileName | ne |
| field:"InitiatingProcessFileName" kind:ne value:"microsoft.tri.sensor.exe" |
NodeLabel | eq |
| field:"NodeLabel" kind:eq value:"device" |
OnboardingStatus | ne |
| field:"OnboardingStatus" kind:ne value:"Onboarded" |
RemotePort | eq |
| field:"RemotePort" kind:eq value:"3389" |
RemoteUrl | eq |
| field:"RemoteUrl" kind:eq value:"login.microsoftonline.com" |
TpmActivated | ne |
| field:"TpmActivated" kind:ne value:"true" |
TpmEnabled | ne |
| field:"TpmEnabled" kind:ne value:"true" |
TpmSupported | ne |
| field:"TpmSupported" kind:ne value:"true" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
InitiatingProcessFileName | in | backgroundtaskhost.exe, svchost.exe | excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"backgroundtaskhost.exe" field:"InitiatingProcessFileName" value:"svchost.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
DomainTools Newly Observed Domains Feeds Ingested
#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.
'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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Entities["Type"] | eq |
| |
ProductName | eq |
| field:"ProductName" kind:eq value:"Microsoft 365 Insider Risk Management" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertName | project |
PreviousAlertsLinks | project |
ProductName | project |
Status | project |
Tactics | project |
TimeGenerated | project |
UserPrincipalName | project |
AccountName | extend |
AccountUPNSuffix | extend |
Job No Longer Used as Second Destination
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"23420" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
Job Name | project |
MessageDetails | project |
Severity | project |
UserName | project |
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.
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| 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.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
Palo Alto - potential beaconing detected
#'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
| Tactic | Techniques |
|---|---|
| Command & Control |
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.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Message.dst_ip | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:Message.dst_ip |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FirewallName_s | summarize |
Message | summarize |
MostFrequentTimeDeltaCount | summarize |
MostFrequentTimeDeltainSeconds | summarize |
TotalEvents | summarize |
BeaconPercent | extend |
IPAddress | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
Pathlock TDnR - SAP Read Access Logging Data
#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
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"RAL_DATA" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
PROD (TM007.1) - OU - Created or Deleted at Tier Level
#Creation or Deletion of an OU within the Tier Model
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | match |
| field:"EventData" kind:match |
EventData | regex_match |
| field:"EventData" kind:regex_match |
EventID | eq |
| field:"EventID" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
ObjectClass | project |
ObjectName | project |
ObjectTier | project |
TimeGenerated | project |
ProofpointPOD - Binary file in attachment
#'Detects when email received with binary file as attachment.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"message" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"inbound" |
attachedMimeType | match |
| field:"attachedMimeType" kind:match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
FilterDisposition | in | discard, reject | excludes:FilterDisposition field:"FilterDisposition" value:"discard" field:"FilterDisposition" value:"reject" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | project |
MsgHeaderSubject | project |
SrcUserUpn | project |
attachedMimeType | project |
Name | extend |
UPNSuffix | extend |
Ransomware Client Blocked
#Identifies malicious clients blocked by the Ransomware Protection service running on a Nasuni Edge Appliance.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"The Filer has enforced the mitigation policy on volume" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | project |
SyslogMessage | project |
TimeGenerated | project |
SrcIpAddr | extend |
volume_name | extend |
SSH Credentials Changed
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"31900" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Credential Record | project |
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Suspicious application consent similar to O365 Attack Toolkit
#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
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Lateral Movement |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
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
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
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.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppClientId | project |
AppDisplayName | project |
AppReplyURLs | project |
ConsentFull | project |
CorrelationId | project |
GrantAuthentication | project |
GrantConsentType | project |
GrantInitiatedBy | project |
GrantInitiatedByAadUserId | project |
GrantInitiatedByAppName | project |
GrantInitiatedByAppServicePrincipalId | project |
GrantInitiatedByUserPrincipalName | project |
GrantIpAddress | project |
GrantOperation | project |
GrantScope1 | project |
GrantUserAgent | project |
OperationName | project |
TimeGenerated | project |
Name | extend |
UPNSuffix | extend |
Tailscale Premium: Subnet router throughput anomaly
#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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
HasSubnetTraffic | eq |
| field:"HasSubnetTraffic" kind:eq value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
BaselineHourlyMB | project |
Multiplier | project |
NodeId | project |
RecentMB | project |
SrcNodeName | project |
SrcOs | project |
SrcTags | project |
SrcUser | project |
Tailscale: Device Tailscale SSH newly enabled
#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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
SshEnabled | eq |
| field:"SshEnabled" kind:eq value:"true" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SshEnabled | eq | true | excludes:SshEnabled field:"SshEnabled" value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientVersion | project |
DeviceName | project |
Hostname | project |
LastSeen | project |
Os | project |
TimeGenerated | project |
User | project |
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.
"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
| Tactic | Techniques |
|---|---|
| Collection |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
customProps_RuleId_s | eq |
| field:"customProps_RuleId_s" kind:eq value:"TRIS0032" |
priority_s | eq |
| field:"priority_s" kind:eq |