Detection rules › Kusto
Kusto rules: 2m
BloodHound Attack Path Finding - Large Default Groups With Ownership Privileges
#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 - Large Default Groups With Ownership Privileges
Rule Body
id: 222e7627-fc25-4ffd-a312-5a27513800ea
name: BloodHound Attack Path Finding - Large Default Groups With Ownership Privileges
description: BloodHound Attack Path Finding - Large Default Groups With Ownership
Privileges
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 == "LargeDefaultGroupsOwns"
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 == "LargeDefaultGroupsOwns"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"LargeDefaultGroupsOwns" |
Credential Record Deleted
#Detects when a credential record is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: 1edb50b5-fee1-4826-86f9-abaf0e4ce5a5
name: Credential Record Deleted
description: Detects when a credential record is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
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 == 25500\n| extend AccountName\
\ = extract(\"AccountName=\\\"(?<AccountName>[^\\\"]*)\\\"\", 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 [\"Credential\
\ Record\"] = AccountName,\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 == 25500
Stage 3: extend
| extend AccountName = extract("AccountName=\"(?<AccountName>[^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
["Credential Record"] = AccountName,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"25500" |
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 |
UserName | project |
Cyble Vision Alerts Product Vulnerability Detected
#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.
'Detects product vulnerability updates (CVE) for monitored products. Triggers SOC triage when a product vulnerability is reported or updated for a monitored product/version.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Initial Access |
Rule Body
id: c360341e-6ba6-472a-ae00-7be85967e240
name: Cyble Vision Alerts Product Vulnerability Detected
description: |
'Detects product vulnerability updates (CVE) for monitored products. Triggers SOC triage when a product vulnerability is reported or updated for a monitored product/version.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- InitialAccess
- ResourceDevelopment
relevantTechniques:
- T1190
- T1588
query: |
Alerts_product_vulnerability
| where Service == "product_vulnerability"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: Product Vulnerability {{PV_Product}} {{PV_Version}} {{PV_CVE_ID}}
alertDescriptionFormat: |
A product vulnerability has been detected.
Title {{PV_Title}}
Exploitability {{PV_New_Exploitability}}
Review the CVE details, assess exposure in your environment, and apply mitigations as required.
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
PV_CVE_ID: PV_CVE_ID
PV_Product: PV_Product
PV_Version: PV_Version
PV_Title: PV_Title
New_Exploitability: PV_New_Exploitability
PV_New_EPS_Score: PV_New_EPS_Score
New_EPS_Percentile: PV_New_EPS_Percentile
PV_New_Price_0day: PV_New_Price_0day
PV_New_Price_Today: PV_New_Price_Today
PV_Old_EPS_Score: PV_Old_EPS_Score
PV_SW_Company: PV_SW_Company
PV_SW_Software: PV_SW_Software
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: SD_Subdomain
- entityType: Account
fieldMappings:
- identifier: Name
columnName: KeywordName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SD_Subdomain
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_product_vulnerability
Stage 2: where
| where Service == "product_vulnerability"
Stage 3: extend
| extend MappedSeverity = Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"product_vulnerability" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
License Expired
#Detects when a Veeam license is expired. This could impact backup operations and data protection.
Rule Body
id: 94724029-6ec3-4bf7-a0e9-c4e4cba0479f
name: License Expired
description: Detects when a Veeam license is expired. This could impact backup operations
and data protection.
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: "let license_editions_lookup = union isfuzzy=true (datatable(Edition:string,\
\ EditionDescription:string)[]), (_GetWatchlist(\"license_editions_lookup\")); \n\
let license_types_lookup = union isfuzzy=true (datatable(Type:string, TypeDescription:string)[]),\
\ (_GetWatchlist(\"license_types_lookup\")); \nVeeam_GetSecurityEvents\n| where\
\ instanceId == 24030\n| extend Edition = extract(\"Edition=\\\"([^\\\"]*)\\\"\"\
, 1, SyslogMessage)\n| lookup kind=leftouter (license_editions_lookup) \n on\
\ $left.Edition == $right.Edition\n| extend Type = extract(\"Type=\\\"([^\\\"]*)\\\
\"\", 1, SyslogMessage)\n| lookup kind=leftouter (license_types_lookup) \n on\
\ $left.Type == $right.Type\n| extend TenantID = extract(\"TenantID=\\\"([^\\\"\
]*)\\\"\", 1, SyslogMessage)\n| extend DaysLeft = extract(\"DaysLeft=\\\"([^\\\"\
]*)\\\"\", 1, SyslogMessage)\n| extend SupportLeft = extract(\"SupportLeft=\\\"\
([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date = format_datetime(TimeGenerated,\
\ 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n EventId = instanceId,\n\
\ [\"License Edition\"] = EditionDescription,\n [\"License Type\"] = TypeDescription,\n\
\ [\"Days Left\"] = DaysLeft,\n [\"Days of Support Left\"] = SupportLeft,\n\
\ MessageDetails = Description,\n Severity = SeverityDescription\n"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Parameters
let license_editions_lookup = union isfuzzy=true (datatable(Edition:string, EditionDescription:string)[]), (_GetWatchlist("license_editions_lookup"));
let license_types_lookup = union isfuzzy=true (datatable(Type:string, TypeDescription:string)[]), (_GetWatchlist("license_types_lookup"));
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 24030
Stage 3: extend
| extend Edition = extract("Edition=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: kusto:lookup
lookup kind=leftouter (license_editions_lookup) on $left.Edition, $right.Edition
Stage 5: extend
extend Type
Stage 6: kusto:lookup
lookup kind=leftouter (license_types_lookup) on $left.Type, $right.Type
Stage 7: extend (3 consecutive steps)
extend DaysLeft, SupportLeft, TenantID
Stage 8: project
project DataSource, Date, Days Left, Days of Support Left, EventId, License Edition, License Type, MessageDetails, Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"24030" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
Days Left | project |
Days of Support Left | project |
EventId | project |
License Edition | project |
License Type | project |
MessageDetails | project |
Severity | project |
Multiple RDP connections from Single System
#Identifies when an RDP connection is made to multiple systems and above the normal connection count for the previous 7 days. Connections from the same system with the same account within the same day. RDP connections are indicated by the EventID 4624 with LogonType = 10
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4624: An account was successfully logged on. |
Rule Body
id: 78422ef2-62bf-48ca-9bab-72c69818a425
name: Multiple RDP connections from Single System
description: |
'Identifies when an RDP connection is made to multiple systems and above the normal connection count for the previous 7 days.
Connections from the same system with the same account within the same day.
RDP connections are indicated by the EventID 4624 with LogonType = 10'
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1021
query: |
let endtime = 1d;
let starttime = 8d;
let threshold = 2.0;
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName),
(WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10
| extend ProcessName = tostring(EventData.ProcessName)
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(Account endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| extend Activity="4624 - An account was successfully logged on."
| extend LogonTypeName="10 - RemoteInteractive"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName)
)
| join kind=inner (
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress
),
( WindowsEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress)
)
) on Account, IpAddress
| extend Ratio = iff(isempty(ComputerCountPrev7Days), toreal(ComputerCountToday), ComputerCountToday / (ComputerCountPrev7Days * 1.0))
// Where the ratio of today to previous 7 days is more than double.
| where Ratio > threshold
| project StartTime, EndTime, Account, IpAddress, ComputerSet, ComputerCountToday, ComputerCountPrev7Days, Ratio, AccountType, Activity, LogonTypeName, ProcessSet
| extend AccountName = tostring(split(Account, @"\")[1]), AccountNTDomain = tostring(split(Account, @"\")[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.2.6
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Parameters
let endtime = 1d;
let starttime = 8d;
let threshold = 2.0;
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent
Leg 1: SecurityEvent
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName
Leg 2: WindowsEvent
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10
| extend ProcessName = tostring(EventData.ProcessName)
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(Account endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| extend Activity="4624 - An account was successfully logged on."
| extend LogonTypeName="10 - RemoteInteractive"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName
Applied to the Combined Result
| join kind=inner (
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress
),
( WindowsEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress)
)
) on Account, IpAddress
| extend Ratio = iff(isempty(ComputerCountPrev7Days), toreal(ComputerCountToday), ComputerCountToday / (ComputerCountPrev7Days * 1.0))
| where Ratio > threshold
| project StartTime, EndTime, Account, IpAddress, ComputerSet, ComputerCountToday, ComputerCountPrev7Days, Ratio, AccountType, Activity, LogonTypeName, ProcessSet
| extend AccountName = tostring(split(Account, @"\")[1]), AccountNTDomain = tostring(split(Account, @"\")[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | match |
| field:"EventData" kind:match value:"10" |
EventID | eq |
| field:"EventID" kind:eq value:"4624" |
LogonType | eq |
| field:"LogonType" kind:eq value:"10" |
Ratio | gt |
| field:"Ratio" kind:gt value:"2.0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
AccountType | project |
Activity | project |
ComputerCountPrev7Days | project |
ComputerCountToday | project |
ComputerSet | project |
EndTime | project |
IpAddress | project |
LogonTypeName | project |
ProcessSet | project |
Ratio | project |
StartTime | project |
AccountNTDomain | extend |
AccountName | extend |
Netskope - New Risky App Access vs 7-Day Baseline
#Compares today's accessed applications against a 7-day baseline and triggers alerts when users access new risky applications not seen before.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Discovery |
Rule Body
id: ba66b81c-2cf7-4c53-9db0-e8b6f537704a
name: Netskope - New Risky App Access vs 7-Day Baseline
description: |
Compares today's accessed applications against a 7-day baseline and triggers alerts when users access new risky applications not seen before.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: NetskopeWebTxConnector
dataTypes:
- NetskopeWebTransactions_CL
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Discovery
relevantTechniques:
- T1199
- T1526
query: |
let lookbackPeriod = 7d;
let currentPeriod = 1d;
let baseline = NetskopeWebTransactions_CL
| where TimeGenerated between (ago(lookbackPeriod) .. ago(currentPeriod))
| where isnotempty(CsUsername) and isnotempty(XCsApp)
| summarize BaselineApps = make_set(XCsApp) by CsUsername;
let current = NetskopeWebTransactions_CL
| where TimeGenerated > ago(currentPeriod)
| where isnotempty(CsUsername) and isnotempty(XCsApp)
| where XCsAppCcl =~ 'poor' or XCsAppCcl =~ 'low' or XCsAppCcl =~ 'medium' or XCsAppCci < 70
| summarize
CurrentApps = make_set(XCsApp),
arg_max(TimeGenerated, XCsAppCcl, XCsAppCci, XCsAppCategory)
by CsUsername, XCsApp;
current
| join kind=leftouter baseline on CsUsername
| extend BaselineApps = coalesce(BaselineApps, dynamic([]))
| where not(set_has_element(BaselineApps, XCsApp))
| where isnotempty(XCsApp)
| where XCsAppCci < 70 or XCsAppCcl =~ 'poor' or XCsAppCcl =~ 'low'
| project
TimeGenerated,
User = CsUsername,
NewRiskyApp = XCsApp,
AppCCL = XCsAppCcl,
AppCCI = XCsAppCci,
AppCategory = XCsAppCategory,
BaselineAppCount = array_length(BaselineApps)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: User
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: NewRiskyApp
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookbackPeriod = 7d;
let currentPeriod = 1d;
let current is inlined into the numbered stages below.
Let Binding: baseline
let baseline = NetskopeWebTransactions_CL
| where TimeGenerated between (ago(lookbackPeriod) .. ago(currentPeriod))
| where isnotempty(CsUsername) and isnotempty(XCsApp)
| summarize BaselineApps = make_set(XCsApp) by CsUsername;
Stages 1 to 5 define let current as the rule's main pipeline source. Stages 6 to 11 use it.
Stage 1: source
NetskopeWebTransactions_CL
Stage 2: where
| where TimeGenerated > ago(currentPeriod)
Stage 3: where
| where isnotempty(CsUsername) and isnotempty(XCsApp)
Stage 4: where
| where XCsAppCcl =~ 'poor' or XCsAppCcl =~ 'low' or XCsAppCcl =~ 'medium' or XCsAppCci < 70
Stage 5: summarize
| summarize
CurrentApps = make_set(XCsApp),
arg_max(TimeGenerated, XCsAppCcl, XCsAppCci, XCsAppCategory)
by CsUsername, XCsApp
Stage 6: join
current
| join kind=leftouter baseline on CsUsername
Stage 7: extend
extend BaselineApps
Stage 8: where
where not (XCsApp =~ "BaselineApps")
Stage 9: where
where isnotempty(XCsApp)
Stage 10: where
where (XCsAppCci < 70 or XCsAppCcl =~ "low" or XCsAppCcl =~ "poor")
Stage 11: project
project AppCCI, AppCCL, AppCategory, BaselineAppCount, NewRiskyApp, TimeGenerated, User
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CsUsername | is_not_null | field:"CsUsername" kind:is_not_null | |
XCsApp | is_not_null | field:"XCsApp" kind:is_not_null | |
XCsAppCci | lt |
| field:"XCsAppCci" kind:lt value:"70" |
XCsAppCcl | eq |
| field:"XCsAppCcl" kind:eq |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
XCsApp | eq | BaselineApps | excludes:XCsApp field:"XCsApp" value:"BaselineApps" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppCCI | project |
AppCCL | project |
AppCategory | project |
BaselineAppCount | project |
NewRiskyApp | project |
TimeGenerated | project |
User | project |
New onmicrosoft domain added to tenant
#This detection looks for new onmicrosoft domains being added to a tenant. An attacker who compromises a tenant may register a new onmicrosoft domain in order to masquerade as a service provider for launching phishing campaigns. Domain additions are not a common occurrence and users should validate that the domain was added by a legitimate user, with a legitimate purpose.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add unverified domain |
| Azure | Add verified domain |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Domain Federation Trust Settings Modified (Kusto)
- Azure AD New Custom Domain Added (Splunk)
- Azure AD New Federated Domain Added (Splunk)
- Azure Domain Federation Settings Modified (Sigma)
- Entra ID Custom Domain Added or Verified (Elastic)
- Entra ID Domain Federation Configuration Change (Elastic)
- Modified domain federation trust settings (Kusto)
- New Root Certificate Authority Added (Sigma)
Rule Body
id: 4f42b94f-b210-42d1-a023-7fa1c51d969f
name: New onmicrosoft domain added to tenant
description: |
'This detection looks for new onmicrosoft domains being added to a tenant.
An attacker who compromises a tenant may register a new onmicrosoft domain in order to masquerade as a service provider for launching phishing campaigns.
Domain additions are not a common occurrence and users should validate that the domain was added by a legitimate user, with a legitimate purpose.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- ResourceDevelopment
relevantTechniques:
- T1585.003
query: |
AuditLogs
| where AADOperationType == "Add"
| where Result == "success"
| where OperationName in ("Add verified domain", "Add unverified domain")
| extend InitiatedBy = parse_json(InitiatedBy)
| extend InitiatingUser = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIp = tostring(InitiatedBy.user.ipAddress)
| extend InitiatingApp = tostring(InitiatedBy.app.displayName)
| extend InitiatingSPID = tostring(InitiatedBy.app.servicePrincipalId)
| extend DomainAdded = tostring(TargetResources[0].displayName)
| where DomainAdded has "onmicrosoft"
| extend ActionInitiatedBy = case(isnotempty(InitiatingUser), InitiatingUser, strcat(InitiatingApp, " - ", InitiatingSPID))
| extend UserName = split(InitiatingUser, "@")[0]
| extend UPNSuffix = split(InitiatingUser, "@")[1]
| project-reorder TimeGenerated, OperationName, DomainAdded, ActionInitiatedBy, InitiatingIp
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUser
- identifier: Name
columnName: UserName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingSPID
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIp
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DomainAdded
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: '{{DomainAdded}} added to tenant by {{ActionInitiatedBy}}'
alertDescriptionFormat: 'This detection looks for new onmicrosoft domains being added to a tenant. An attacker who compromises a tenant may register a new onmicrosoft domain in order to masquerade as a service provider for launching phishing accounts. Domain additions are not a common occurrence and users should validate that {{ActionInitiatedBy}} added {{DomainAdded}} with a legitimate purpose.'
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where AADOperationType == "Add"
Stage 3: where
| where Result == "success"
Stage 4: where
| where OperationName in ("Add verified domain", "Add unverified domain")
Stage 5: extend (7 consecutive steps)
| extend InitiatedBy = parse_json(InitiatedBy)
| extend InitiatingUser = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIp = tostring(InitiatedBy.user.ipAddress)
| extend InitiatingApp = tostring(InitiatedBy.app.displayName)
| extend InitiatingSPID = tostring(InitiatedBy.app.servicePrincipalId)
| extend DomainAdded = tostring(TargetResources[0].displayName)
Stage 6: where
| where DomainAdded has "onmicrosoft"
Stage 7: extend (3 consecutive steps)
| extend ActionInitiatedBy = case(isnotempty(InitiatingUser), InitiatingUser, strcat(InitiatingApp, " - ", InitiatingSPID))
| extend UserName = split(InitiatingUser, "@")[0]
| extend UPNSuffix = split(InitiatingUser, "@")[1]
ActionInitiatedBy =isnotempty(InitiatingUser)InitiatingUserstrcat(InitiatingApp, " - ", InitiatingSPID)Stage 8: project-reorder
| project-reorder TimeGenerated, OperationName, DomainAdded, ActionInitiatedBy, InitiatingIp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | eq |
| field:"AADOperationType" kind:eq value:"Add" |
DomainAdded | match |
| field:"DomainAdded" kind:match value:"onmicrosoft" |
OperationName | in |
| field:"OperationName" kind:in |
Result | eq |
| field:"Result" kind:eq value:"success" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatedBy | extend |
InitiatingUser | extend |
InitiatingAadUserId | extend |
InitiatingIp | extend |
InitiatingApp | extend |
InitiatingSPID | extend |
DomainAdded | extend |
ActionInitiatedBy | extend |
UserName | extend |
UPNSuffix | extend |
PLC unsecure key state (Microsoft Defender for IoT)
#'This alert leverages Defender for IoT to detect PLC operating mode changes indicating the PLC is potentially insecure. If the PLC is compromised, devices that interact with it may be impacted. This may affect overall system security and safety.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Evasion |
Rule Body
id: f9df500a-e2a4-4104-a517-dc1d85bb654f
name: PLC unsecure key state (Microsoft Defender for IoT)
description: |
'This alert leverages Defender for IoT to detect PLC operating mode changes indicating the PLC is potentially insecure. If the PLC is compromised, devices that interact with it may be impacted. This may affect overall system security and safety.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: IoT
dataTypes:
- SecurityAlert (ASC for IoT)
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
relevantTechniques:
- T0858
query: |
SecurityAlert
| where ProviderName == "IoTSecurity"
| where AlertName has "PLC Operating Mode Changed"
| extend ExtendedProperties = parse_json(ExtendedProperties)
| where tostring(ExtendedProperties.isNew) == "True"
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
entityMappings:
sentinelEntitiesMappings:
- columnName: Entities
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
Sensor: DeviceId
Protocol: Protocol
VendorOriginalId: VendorOriginalId
AlertManagementUri: AlertManagementUri
alertDetailsOverride:
alertDisplayNameFormat: (MDIoT) {{AlertName}}
alertDescriptionFormat: (MDIoT) {{Description}}
alertTacticsColumnName: Tactics
alertSeverityColumnName: AlertSeverity
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: RemediationSteps
value: RemediationSteps
- alertProperty: Techniques
value: Techniques
- alertProperty: ProductComponentName
value: ProductComponentName
- alertProperty: AlertLink
value: AlertLink
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where ProviderName == "IoTSecurity"
Stage 3: where
| where AlertName has "PLC Operating Mode Changed"
Stage 4: extend
| extend ExtendedProperties = parse_json(ExtendedProperties)
Stage 5: where
| where tostring(ExtendedProperties.isNew) == "True"
Stage 6: extend
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
Stage 7: project
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | match |
| field:"AlertName" kind:match value:"PLC Operating Mode Changed" |
ExtendedProperties.isNew | eq |
| field:"ExtendedProperties.isNew" kind:eq value:"True" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IoTSecurity" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertManagementUri | project |
AlertName | project |
AlertSeverity | project |
Description | project |
DestDeviceAddress | project |
DeviceId | project |
Entities | project |
ProductComponentName | project |
ProductName | project |
Protocol | project |
RemediationSteps | project |
SourceDeviceAddress | project |
Tactics | project |
Techniques | project |
TimeGenerated | project |
VendorOriginalId | project |
Red Sift - Login from previously unseen IP address
#'Detects a successful login from an IP address that has not been seen in the previous 14 days for that user, which may indicate compromised credentials or unauthorized access.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: c3d4e5f6-a7b8-9012-cdef-123456789012
name: Red Sift - Login from previously unseen IP address
description: |
'Detects a successful login from an IP address that has not been seen in the previous 14 days for that user, which may indicate compromised credentials or unauthorized access.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: RedSiftPush
dataTypes:
- RedSiftAuth_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let lookback = 14d;
let recentWindow = 1h;
let historicalLogins = RedSiftAuth_CL
| extend
ActivityName = tostring(column_ifexists("ActivityName", "")),
UserEmail = tostring(column_ifexists("UserEmail", "")),
SrcIp = tostring(column_ifexists("SrcIp", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where ActivityName has "Logon"
| where isnotempty(UserEmail) and isnotempty(SrcIp)
| summarize by UserEmail, SrcIp;
RedSiftAuth_CL
| extend
ActivityName = tostring(column_ifexists("ActivityName", "")),
UserEmail = tostring(column_ifexists("UserEmail", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
HttpUserAgent = tostring(column_ifexists("HttpUserAgent", "")),
ServiceName = tostring(column_ifexists("ServiceName", "")),
Severity = tostring(column_ifexists("Severity", ""))
| where TimeGenerated >= ago(recentWindow)
| where ActivityName has "Logon"
| where isnotempty(UserEmail) and isnotempty(SrcIp)
| join kind=leftanti (historicalLogins) on UserEmail, SrcIp
| project
TimeGenerated,
UserEmail,
SrcIp,
HttpUserAgent,
ServiceName,
ActivityName,
Severity
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserEmail
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
customDetails:
UserAgent: HttpUserAgent
ServiceName: ServiceName
alertDetailsOverride:
alertDisplayNameFormat: "RedSift - New IP Login for {{UserEmail}}"
alertDescriptionFormat: "User {{UserEmail}} logged in from a previously unseen IP address {{SrcIp}}."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByEntities:
- Account
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let recentWindow = 1h;
Let Binding: historicalLogins
let historicalLogins = RedSiftAuth_CL
| extend
ActivityName = tostring(column_ifexists("ActivityName", "")),
UserEmail = tostring(column_ifexists("UserEmail", "")),
SrcIp = tostring(column_ifexists("SrcIp", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where ActivityName has "Logon"
| where isnotempty(UserEmail) and isnotempty(SrcIp)
| summarize by UserEmail, SrcIp;
Stage 1: source
RedSiftAuth_CL
Stage 2: extend
| extend
ActivityName = tostring(column_ifexists("ActivityName", "")),
UserEmail = tostring(column_ifexists("UserEmail", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
HttpUserAgent = tostring(column_ifexists("HttpUserAgent", "")),
ServiceName = tostring(column_ifexists("ServiceName", "")),
Severity = tostring(column_ifexists("Severity", ""))
Stage 3: where
| where TimeGenerated >= ago(recentWindow)
Stage 4: where
| where ActivityName has "Logon"
Stage 5: where
| where isnotempty(UserEmail) and isnotempty(SrcIp)
Stage 6: join (negated)
join kind=leftanti (historicalLogins) on UserEmail, SrcIp
Stage 7: project
project ActivityName, HttpUserAgent, ServiceName, Severity, SrcIp, TimeGenerated, UserEmail
Stage 8: summarize aggregation inside the join branch
summarize by UserEmail, SrcIp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityName | match |
| field:"ActivityName" kind:match value:"Logon" |
SrcIp | is_not_null | field:"SrcIp" kind:is_not_null | |
UserEmail | is_not_null | field:"UserEmail" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ActivityName | match | Logon | excludes:ActivityName field:"ActivityName" value:"Logon" |
SrcIp | is_not_null | excludes:SrcIp | |
UserEmail | is_not_null | excludes:UserEmail |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIp | summarize |
UserEmail | summarize |
Sites Alerts for Prancer
#'High severity sites alerts found by Prancer.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: bbeb2f26-cb99-4e4b-900f-24ce9809142d
name: Sites Alerts for Prancer
description: |
'High severity sites alerts found by Prancer.'
severity: High
requiredDataConnectors:
- connectorId: PrancerLogData
dataTypes:
- prancer_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: SingleAlert
status: Available
tactics:
- Reconnaissance
relevantTechniques:
- T1595
query: |
union prancer_CL
| where deviceProduct_s == 'azure'
| where parse_json(data_data_snapshots_s)[0].type == 'Microsoft.Web/sites'
| where data_data_severity_s == 'High' and data_data_result_s == 'failed'
| extend snapshot = parse_json(data_data_snapshots_s)
| mv-expand snapshot
| extend
id = tostring(snapshot.id),
structure = tostring(snapshot.structure),
reference = tostring(snapshot.reference),
source = tostring(snapshot.source),
collection = tostring(snapshot.collection),
type = tostring(snapshot.type),
region = tostring(snapshot.region),
resourceTypes = tostring(snapshot.resourceTypes),
path = tostring(snapshot.path)
customDetails:
entityMappings:
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: path
alertDetailsOverride:
alertDisplayNameFormat: "{{data_data_message_s}}"
alertDescriptionFormat: "{{data_data_description_s}}"
alertSeverityColumnName: "{{data_data_severity_s}}"
alertDynamicProperties:
- alertProperty: RemediationSteps
value: data_data_remediation_description_s
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: union
union of 1 branches
Stage 2: source
prancer_CL
Stage 3: where
where deviceProduct_s == "azure"
Stage 4: where
where parse_json(data_data_snapshots_s)[0].type == "Microsoft.Web/sites"
Stage 5: where
where data_data_result_s == "failed" and data_data_severity_s == "High"
Stage 6: extend
extend snapshot
Stage 7: mv-expand
mv-expand snapshot
Stage 8: extend
extend collection, id, path, reference, region, resourceTypes, source, structure, type
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
snapshot | extend |
collection | extend |
id | extend |
path | extend |
reference | extend |
region | extend |
resourceTypes | extend |
source | extend |
structure | extend |
type | extend |
Suspicious Driver Load
#Below query detects suspicious(unusual/rare) driver loads. Further checks are required on detected files to confirm malicious activity.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | No specific technique |
Telemetry Coverage
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to blog post:
// https://posts.bluraven.io/detecting-edr-bypass-malicious-drivers-kernel-callbacks-f5e6bf8f7481
//
// Query parameters:
// Query at least 7d of data to reduce false positives(if you have many.
DeviceEvents
| where ActionType == "DriverLoad"
| distinct SHA1 // get SHA1 of the drivers
// get certificate information of the drivers
| join kind=inner
(
DeviceFileCertificateInfo
// get only the files having certificate older than "7/30/2015"
| where CertificateCreationTime < todatetime("7/30/2015") or CertificateExpirationTime < todatetime("7/30/2015")
) on SHA1
// use prevalence. assuming malicious driver has been installed on max 5 machines.
| summarize dcount(DeviceId) by SHA1
| where dcount_DeviceId <= 5
// get file profile
| invoke FileProfile(SHA1,1000)
// filter out the files having GlobalPrevalence > 500 (FP reduction)
| where GlobalPrevalence <= 500
// get certificate details back
| join DeviceFileCertificateInfo on SHA1
Stages and Predicates
Stage 1: source
DeviceEvents
Stage 2: where
| where ActionType == "DriverLoad"
Stage 3: distinct
| distinct SHA1
Stage 4: join
| join kind=inner
(
DeviceFileCertificateInfo
| where CertificateCreationTime < todatetime("7/30/2015") or CertificateExpirationTime < todatetime("7/30/2015")
) on SHA1
Stage 5: summarize
summarize by SHA1
Stage 6: where
where dcount_DeviceId <= 5
Stage 7: invoke
invoke
Stage 8: where
where GlobalPrevalence <= 500
Stage 9: join
| join DeviceFileCertificateInfo on SHA1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"DriverLoad" |
GlobalPrevalence | le |
| field:"GlobalPrevalence" kind:le value:"500" |
dcount_DeviceId | le |
| field:"dcount_DeviceId" kind:le value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SHA1 | summarize |
Tenable.ad Active Directory attacks pathways
#'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 4639bb0a-ca12-4a57-8e53-f61c2c6034d6
name: Tenable.ad Active Directory attacks pathways
description: |
'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'
severity: Low
requiredDataConnectors:
- connectorId: Tenable.ad
dataTypes:
- Tenable_ad_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
// For the query to work properly, make sure you have imported the afad_parser.kql parser into the workspace
// Retrieve the parser here: https://raw.githubusercontent.com/tenable/Azure-Sentinel/Tenable.ad-connector/Solutions/TenableAD/Parsers/afad_parser.kql
// Then, create the Kusto Function with alias afad_parser
let SeverityTable=datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];
afad_parser
| where MessageType == 0 and Codename in~ (codeNameList)
| lookup kind=leftouter SeverityTable on Severity
| order by Level
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: SeverityTable
let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
Let Binding: codeNameList
let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 0 and Codename in~ (codeNameList)
Stage 3: kusto:lookup
lookup kind=leftouter (SeverityTable) on Severity
Stage 4: sort
sort by Level
Stage 5: extend
extend DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | in |
| field:"Codename" kind:in value:"codeNameList" |
MessageType | eq |
| field:"MessageType" kind:eq value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
TI Map IP Entity to AzureActivity
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in AzureActivity.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 2441bce9-02e4-407b-8cc7-7d597f38b8b0
name: TI Map IP Entity to AzureActivity
description: |
This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in AzureActivity.
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h; // Look back 1 hour for AzureActivity logs
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelligenceIndicator
// Filter out indicators without relevant IP address fields
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
| where TimeGenerated >= ago(ioc_lookBack)
// Select the IP entity based on availability of different IP fields
| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
// Exclude local addresses using the ipv4_is_private operator and filtering out specific address prefixes
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now();
// Perform a join between IP indicators and AzureActivity logs to identify potential malicious activity
IP_Indicators
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (
AzureActivity | where TimeGenerated >= ago(dt_lookBack)
// renaming time column so it is clear the log this came from
| extend AzureActivity_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CallerIpAddress
| where AzureActivity_TimeGenerated < ExpirationDateTime
| summarize AzureActivity_TimeGenerated = arg_max(AzureActivity_TimeGenerated, *) by IndicatorId, CallerIpAddress
| project AzureActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, TI_ipEntity, CallerIpAddress,
Caller, OperationNameValue, ActivityStatusValue, CategoryValue, ResourceId, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
| extend timestamp = AzureActivity_TimeGenerated
| extend Name = iif(Caller has '@', tostring(split(Caller,'@',0)[0]), "")
| extend UPNSuffix = iif(Caller has '@', tostring(split(Caller,'@',1)[0]), "")
| extend AadUserId = iif(Caller !has '@', tostring(Caller), "")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: AadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: ResourceId
version: 1.4.3
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Stages 1 to 7 define let IP_Indicators as the rule's main pipeline source. Stages 8 to 12 use it.
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
Stage 3: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 4: extend (3 consecutive steps)
| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =isnotempty(NetworkIP)NetworkIPNetworkDestinationIPStage 5: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 6: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 7: where
| where Active == true and ExpirationDateTime > now()
Stage 8: join
IP_Indicators
| join kind=innerunique (
AzureActivity | where TimeGenerated >= ago(dt_lookBack)
| extend AzureActivity_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CallerIpAddress
Stage 9: where
where AzureActivity_TimeGenerated < ExpirationDateTime
Stage 10: summarize
summarize AzureActivity_TimeGenerated by IndicatorId, CallerIpAddress
Stage 11: project
project ActivityGroupNames, ActivityStatusValue, AzureActivity_TimeGenerated, Caller, CallerIpAddress, CategoryValue, ConfidenceScore, Description, EmailSourceIpAddress, ExpirationDateTime, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, OperationNameValue, ResourceId, TI_ipEntity, ThreatType, Type, Url
Stage 12: extend (4 consecutive steps)
extend AadUserId, Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
EmailSourceIpAddress | is_not_null | field:"EmailSourceIpAddress" kind:is_not_null | |
NetworkDestinationIP | is_not_null | field:"NetworkDestinationIP" kind:is_not_null | |
NetworkIP | is_not_null | field:"NetworkIP" kind:is_not_null | |
NetworkSourceIP | is_not_null | field:"NetworkSourceIP" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | 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:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ActivityStatusValue | project |
AzureActivity_TimeGenerated | project |
Caller | project |
CallerIpAddress | project |
CategoryValue | project |
ConfidenceScore | project |
Description | project |
EmailSourceIpAddress | project |
ExpirationDateTime | project |
IndicatorId | project |
NetworkDestinationIP | project |
NetworkIP | project |
NetworkSourceIP | project |
OperationNameValue | project |
ResourceId | project |
TI_ipEntity | project |
ThreatType | project |
Type | project |
Url | project |
timestamp | extend |
Name | extend |
UPNSuffix | extend |
AadUserId | extend |