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.

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

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.

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

Credential Record Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Credential Recordproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

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.

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

'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

TacticTechniques
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.

FieldKindValuesSearch
Serviceeq
  • product_vulnerability
field:"Service" kind:eq value:"product_vulnerability"

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

License Expired

#
Status
available
Severity
high
Time window
5m
Group by
$left.Edition, $left.Type, $right.Edition, $right.Type
Source
github.com/Azure/Azure-Sentinel

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
Days Leftproject
Days of Support Leftproject
EventIdproject
License Editionproject
License Typeproject
MessageDetailsproject
Severityproject

Multiple RDP connections from Single System

#
Severity
low
Time window
8d
Group by
Account, AccountType, Activity, IpAddress, LogonTypeName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Lateral Movement

Telemetry Coverage

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.

FieldKindValuesSearch
EventDatamatch
  • 10 transforms: term
field:"EventData" kind:match value:"10"
EventIDeq
  • 4624 corpus 8 (chronicle 4, kusto 3, elastic 1)
field:"EventID" kind:eq value:"4624"
LogonTypeeq
  • 10 corpus 3 (kusto 3)
field:"LogonType" kind:eq value:"10"
Ratiogt
  • 2.0 transforms: number
field:"Ratio" kind:gt value:"2.0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
AccountTypeproject
Activityproject
ComputerCountPrev7Daysproject
ComputerCountTodayproject
ComputerSetproject
EndTimeproject
IpAddressproject
LogonTypeNameproject
ProcessSetproject
Ratioproject
StartTimeproject
AccountNTDomainextend
AccountNameextend

Netskope - New Risky App Access vs 7-Day Baseline

#
Status
available
Severity
medium
Time window
8d
Group by
CsUsername, XCsApp
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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 used in Stage 6

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.

FieldKindValuesSearch
CsUsernameis_not_null
  • (no value, null check)
field:"CsUsername" kind:is_not_null
XCsAppis_not_null
  • (no value, null check)
field:"XCsApp" kind:is_not_null
XCsAppCcilt
  • 70 transforms: number
field:"XCsAppCci" kind:lt value:"70"
XCsAppCcleq
  • low
  • medium
  • poor
field:"XCsAppCcl" kind:eq

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
XCsAppeqBaselineAppsexcludes:XCsApp field:"XCsApp" value:"BaselineApps"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppCCIproject
AppCCLproject
AppCategoryproject
BaselineAppCountproject
NewRiskyAppproject
TimeGeneratedproject
Userproject

New onmicrosoft domain added to tenant

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

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

TacticTechniques
Resource Development

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 =
ifisnotempty(InitiatingUser)InitiatingUser
elsestrcat(InitiatingApp, " - ", InitiatingSPID)

Stage 8: project-reorder

| project-reorder TimeGenerated, OperationName, DomainAdded, ActionInitiatedBy, InitiatingIp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatedByextend
InitiatingUserextend
InitiatingAadUserIdextend
InitiatingIpextend
InitiatingAppextend
InitiatingSPIDextend
DomainAddedextend
ActionInitiatedByextend
UserNameextend
UPNSuffixextend

PLC unsecure key state (Microsoft Defender for IoT)

#
Status
available
Severity
low
Time window
3h
Source
github.com/Azure/Azure-Sentinel

'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

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.

FieldKindValuesSearch
AlertNamematch
  • PLC Operating Mode Changed transforms: term
field:"AlertName" kind:match value:"PLC Operating Mode Changed"
ExtendedProperties.isNeweq
  • True transforms: tostring, kusto_dynamic_path corpus 15 (kusto 15)
field:"ExtendedProperties.isNew" kind:eq value:"True"
ProviderNameeq
  • IoTSecurity corpus 15 (kusto 15)
field:"ProviderName" kind:eq value:"IoTSecurity"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertLinkproject
AlertManagementUriproject
AlertNameproject
AlertSeverityproject
Descriptionproject
DestDeviceAddressproject
DeviceIdproject
Entitiesproject
ProductComponentNameproject
ProductNameproject
Protocolproject
RemediationStepsproject
SourceDeviceAddressproject
Tacticsproject
Techniquesproject
TimeGeneratedproject
VendorOriginalIdproject

Red Sift - Login from previously unseen IP address

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

'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

TacticTechniques
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 used in Stage 6

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.

FieldKindValuesSearch
ActivityNamematch
  • Logon transforms: term
field:"ActivityName" kind:match value:"Logon"
SrcIpis_not_null
  • (no value, null check)
field:"SrcIp" kind:is_not_null
UserEmailis_not_null
  • (no value, null check)
field:"UserEmail" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ActivityNamematchLogonexcludes:ActivityName field:"ActivityName" value:"Logon"
SrcIpis_not_null(no value, null check)excludes:SrcIp
UserEmailis_not_null(no value, null check)excludes:UserEmail

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpsummarize
UserEmailsummarize

Sites Alerts for Prancer

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

'High severity sites alerts found by Prancer.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
data_data_result_seq
  • failed
field:"data_data_result_s" kind:eq value:"failed"
data_data_severity_seq
  • High
field:"data_data_severity_s" kind:eq value:"High"
data_data_snapshots_s[0].typeeq
  • Microsoft.Web/sites transforms: kusto_accessor_expr:{"expression":"parse_json(data_data_snapshots_s)[0].type","field":"data_data_snapshots_s[0].type"}, kusto_dynamic_path
field:"data_data_snapshots_s[0].type" kind:eq value:"Microsoft.Web/sites"
deviceProduct_seq
  • azure
field:"deviceProduct_s" kind:eq value:"azure"

Output Fields

These fields are emitted when the rule matches.

FieldSource
snapshotextend
collectionextend
idextend
pathextend
referenceextend
regionextend
resourceTypesextend
sourceextend
structureextend
typeextend

Suspicious Driver Load

#
Group by
SHA1
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query detects suspicious(unusual/rare) driver loads. Further checks are required on detected files to confirm malicious activity.

MITRE ATT&CK Coverage

TacticTechniques
StealthNo 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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SHA1summarize

Tenable.ad Active Directory attacks pathways

#
Severity
low
Time window
2h
Group by
Severity
Source
github.com/Azure/Azure-Sentinel

'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

TI Map IP Entity to AzureActivity

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.CallerIpAddress, CallerIpAddress, IndicatorId
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in AzureActivity.

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ActivityStatusValueproject
AzureActivity_TimeGeneratedproject
Callerproject
CallerIpAddressproject
CategoryValueproject
ConfidenceScoreproject
Descriptionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
OperationNameValueproject
ResourceIdproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
timestampextend
Nameextend
UPNSuffixextend
AadUserIdextend

Trend Micro CAS - Unexpected file on file share

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

'Detects unexpected files on file share.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: de54f817-f338-46bf-989b-4e016ea6b71b
name: Trend Micro CAS - Unexpected file on file share
description: |
  'Detects unexpected files on file share.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);
  TrendMicroCAS
  | where EventCategoryType in~ ('sharepoint', 'onedrive', 'dropbox', 'box', 'googledrive')
  | where isnotempty(SrcFileName)
  | extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)
  | where file_type in~ (f_types) or SrcFileName !contains @'.'
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);

Stage 1: source

TrendMicroCAS

Stage 2: where

| where EventCategoryType in~ ('sharepoint', 'onedrive', 'dropbox', 'box', 'googledrive')

Stage 3: where

| where isnotempty(SrcFileName)

Stage 4: extend

| extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)

Stage 5: where

| where file_type in~ (f_types) or SrcFileName !contains @'.'

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventCategoryTypein
  • box
  • dropbox
  • googledrive
  • onedrive
  • sharepoint
field:"EventCategoryType" kind:in
SrcFileNameis_not_null
  • (no value, null check)
field:"SrcFileName" kind:is_not_null
file_typein
  • bat
  • exe
  • js
  • lnk
  • ps1
  • scr
  • sh
field:"file_type" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
file_typeextend
AccountCustomEntityextend

Back to all Kusto rules