Detection rules › Kusto

Kusto rules: 0g

Cisco SE - Malware outbreak

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

'Detects possible malware outbreak.'

MITRE ATT&CK Coverage

Rule Body

id: 225053c7-085b-4fca-a18f-c367f9228bf3
name: Cisco SE - Malware outbreak
description: |
  'Detects possible malware outbreak.'
severity: High
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 2;
  CiscoSecureEndpoint
  | where isnotempty(ThreatName)
  | summarize infected = makeset(DstHostname) by ThreatName, bin(TimeGenerated, 10m)
  | where array_length(infected) >= threshold
  | extend MalwareCustomEntity = ThreatName
entityMappings:
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 2;

Stage 1: source

CiscoSecureEndpoint

Stage 2: where

| where isnotempty(ThreatName)

Stage 3: summarize

| summarize infected = makeset(DstHostname) by ThreatName, bin(TimeGenerated, 10m)

Stage 4: where

| where array_length(infected) >= threshold

Stage 5: extend

| extend MalwareCustomEntity = ThreatName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ThreatNameis_not_null
  • (no value, null check)
field:"ThreatName" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
ThreatNamesummarize
infectedsummarize
MalwareCustomEntityextend

Cloudflare - Multiple error requests from single source

#
Status
available
Severity
low
Time window
5m
Group by
SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

'Detects multiple failure requests from single source in short timeframe.'

MITRE ATT&CK Coverage

Rule Body

id: ef877d68-755f-4cf1-ac1d-f336e395667d
name: Cloudflare - Multiple error requests from single source
description: |
  'Detects multiple failure requests from single source in short timeframe.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 100;
  Cloudflare
  | where HttpRequestMethod =~ 'GET'
  | summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
  | where err_cnt > threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

Cloudflare

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: summarize

| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 4: where

| where err_cnt > threshold

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
err_cntsummarize

Configuration Backup Failed

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

Detects failed configuration backup operations. This might indicate system or storage issues, or a potential sabotage of the backup infrastructure.

Rule Body

id: 259ef474-836d-4662-86ef-70cb7a38e765
name: Configuration Backup Failed
description: Detects failed configuration backup operations. This might indicate
  system or storage issues, or a potential sabotage of the backup infrastructure.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: VeeamCustomTablesDataConnector
    dataTypes:
      - VeeamSessions_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query:
  VeeamSessions_CL | where SessionType == "ConfigurationBackup" and ResultStatus == "Failed" | project
  TimeGenerated,
  VbrHostName,
  SessionId = Id,
  SessionType,
  Name,
  Result = ResultStatus
version: 1.0.2
kind: Scheduled
customDetails:
  VbrHostName: VbrHostName
  SessionId: SessionId
  SessionType: SessionType
  Name: Name
  Result: Result

Stages and Predicates

Stage 1: source

VeeamSessions_CL

Stage 2: where

| where SessionType == "ConfigurationBackup" and ResultStatus == "Failed"

Stage 3: project

| project TimeGenerated, VbrHostName, SessionId = Id, SessionType, Name, Result = ResultStatus

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Nameproject
Resultproject
SessionIdproject
SessionTypeproject
TimeGeneratedproject
VbrHostNameproject

CTE - CTI Blocked URL Attempted

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

Cybereinforce blocked a URL/domain access attempt that matched a Cybereinforce Threat Intelligence (CTI) indicator - a higher-confidence signal than a generic policy block.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Command & Control

Rule Body

id: 49da5494-b6b9-42c0-9b9b-b7b219dc8173
name: CTE - CTI Blocked URL Attempted
description: |
  Cybereinforce blocked a URL/domain access attempt that matched a Cybereinforce Threat Intelligence (CTI) indicator - a higher-confidence signal than a generic policy block.
severity: Medium
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1189
  - T1071
query: |
  CybereinforceCTE_CL
  | where EventType == "block.url"
  | extend d = parse_json(Details)
  | extend
      DeviceId = tostring(d.deviceId),
      HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
      DeviceUser = tostring(d.deviceUser),
      DeviceOs = tostring(d.deviceOs),
      DeviceVersion = tostring(d.deviceVersion),
      RuleId = tostring(d.ruleId),
      RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
      Pattern = tostring(d.pattern),
      Url = tostring(d.url),
      Action = tostring(d.action),
      IsIoc = tobool(d.ioc),
      ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
      ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
      UserAgent = tostring(d.userAgent)
  | where RuleName == "Cybereinforce Threat Intelligence" or IsIoc == true
  | project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, DeviceVersion, RuleId, RuleName, Pattern, Url, Action, IsIoc, ClientIp, ClientPort, UserAgent, Severity, Category, Source
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "block.url"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend
    DeviceId = tostring(d.deviceId),
    HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
    DeviceUser = tostring(d.deviceUser),
    DeviceOs = tostring(d.deviceOs),
    DeviceVersion = tostring(d.deviceVersion),
    RuleId = tostring(d.ruleId),
    RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
    Pattern = tostring(d.pattern),
    Url = tostring(d.url),
    Action = tostring(d.action),
    IsIoc = tobool(d.ioc),
    ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
    ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
    UserAgent = tostring(d.userAgent)
HostName =
ifisnotempty(tostring(d.hostname))tostring(d.hostname)
elsestrcat("cte-device-", substring(EntityId, 0, 8))
RuleName =
ifisnotempty(tostring(d.ruleName))tostring(d.ruleName)
else"Pattern Match"

Stage 5: where

| where RuleName == "Cybereinforce Threat Intelligence" or IsIoc == true

Stage 6: project

| project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, DeviceVersion, RuleId, RuleName, Pattern, Url, Action, IsIoc, ClientIp, ClientPort, UserAgent, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
Categoryproject
ClientIpproject
ClientPortproject
DeviceIdproject
DeviceOsproject
DeviceUserproject
DeviceVersionproject
HostNameproject
IsIocproject
Patternproject
RuleIdproject
RuleNameproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject
Urlproject
UserAgentproject

Darktrace Incident Event

#

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

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

This query searches for Darktrace incident events and creates a Microsoft Sentinel incident for each matching result.

MITRE ATT&CK Coverage

Rule Body

id: 05de0eaf-01bc-4615-99fc-2ec769864b34
name: Darktrace Incident Event
kind: NRT
description: This query searches for Darktrace incident events and creates a Microsoft Sentinel incident for each matching result.
severity: High
requiredDataConnectors:
  - connectorId: DarktraceActiveAISecurityPlatform
    dataTypes:
      - DarktraceIncidents_CL
tactics:
  - InitialAccess
  - Execution
  - LateralMovement
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1059
  - T1021
  - T1071
query: |
  DarktraceIncidents_CL
  | where TimeGenerated >= ago(5m)
  | extend SentinelSeverity = case(groupCategory == "suspicious", "Medium",
  groupCategory == "critical", "High", "Informational")
  | extend ProviderName = "Darktrace"
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: accountName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: deviceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: deviceHostname
customDetails:
  Asset: bestAssetName
  IncidentEventScore: aiaScore
  IncidentScore: groupScore
  CurrentGroup: currentGroup
  PreviousGroups: groupPreviousGroups
  StartTime: startTime
  EndTime: endTime
  CustomLabel: customLabel
  AssetDetails: devices
  ModelBreaches: modelBreaches
alertDetailsOverride:
  alertDisplayNameFormat: 'Darktrace Incident Event: {{incidentEventTitle}} '
  alertDescriptionFormat: '{{summary}}'
  alertSeverityColumnName: SentinelSeverity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: url
    - alertProperty: ProductName
      value: darktraceProduct
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5M
    matchingMethod: AllEntities
version: 1.1.0

Stages and Predicates

Stage 1: source

DarktraceIncidents_CL

Stage 2: where

| where TimeGenerated >= ago(5m)

Stage 3: extend

| extend SentinelSeverity = case(groupCategory == "suspicious", "Medium",
groupCategory == "critical", "High", "Informational")
SentinelSeverity =
ifgroupCategory == "suspicious""Medium"
elifgroupCategory == "critical""High"
else"Informational"

Stage 4: extend

| extend ProviderName = "Darktrace"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SentinelSeverityextend
ProviderNameextend

Detect Direct Send phishing emails

#
Group by
$left.SenderObjectId, $right.UserId, UserId, UserPrincipalName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

In May 2025, a campaign started using the Microsoft Exchange Direct Send feature to send phishing mails to organizations. This feature is designed for use by printers, scanners, cloud services and other devices that need to send messages on behalf of the company. This detection rule tries to detect the malicious mails being send via Direct Send using a couple of indicators: - Sender email is the same as Recipient email - SPF and DMARC both failed - Mail is not comming in via an Exchange connector - The sender IP address country is not a country that the user is known to login from FYI, if you remove the country check (last line of the query), you can use the query to identify if there are legitimate mails as well. If not, you can disable the Direct Send feature in Exchange Online.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

References

Telemetry Coverage

Rule Body

let country_land_codes = (
    externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string) 
    ['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true)
    | distinct Country, Alpha2
);
let login_locations = (
    SigninLogs
    | where TimeGenerated > ago(7d)
    | where ResultSignature == 0
    | distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion)
    | where isnotempty(LandCode)
    | join kind=inner country_land_codes on $left.LandCode == $right.Alpha2
    | summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName
);
EmailEvents
| where TimeGenerated > ago(30d)
// Find Direct Send mails
| where SenderMailFromAddress == RecipientEmailAddress
| extend AuthenticationDetails = parse_json(AuthenticationDetails)
| where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail"
// Only flag mails not comming in via an exchange connector (these are legit mails)
| where isempty(Connectors)
// Exclude if detected as phish (since it is already remediated then)
| where DeliveryAction !in ("Junked", "Blocked")
| extend Location = parse_json(geo_info_from_ip_address(SenderIPv4))
| extend MailFromCountry = tostring(Location.country)
// Find the usage countries of the users
| join kind=leftouter login_locations on $left.SenderObjectId == $right.UserId
// Flag when sender country is not a usage country of the user
| where tostring(UsageCountries) !contains MailFromCountry

Stages and Predicates

Let Binding: country_land_codes

let country_land_codes = (
    externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string) 
    ['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true)
    | distinct Country, Alpha2
);

Let Binding: login_locations used in Stage 10

let login_locations = (
    SigninLogs
    | where TimeGenerated > ago(7d)
    | where ResultSignature == 0
    | distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion)
    | where isnotempty(LandCode)
    | join kind=inner country_land_codes on $left.LandCode == $right.Alpha2
    | summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName
);

Stage 1: source

EmailEvents

Stage 2: where

| where TimeGenerated > ago(30d)

Stage 3: where

| where SenderMailFromAddress == RecipientEmailAddress

Stage 4: extend

| extend AuthenticationDetails = parse_json(AuthenticationDetails)

Stage 5: where

| where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail"

Stage 6: where

| where isempty(Connectors)

Stage 7: where

| where DeliveryAction !in ("Junked", "Blocked")

Stage 8: extend

| extend Location = parse_json(geo_info_from_ip_address(SenderIPv4))

Stage 9: extend

| extend MailFromCountry = tostring(Location.country)

Stage 10: join

join kind=leftouter (login_locations) on $left.SenderObjectId, $right.UserId

Stage 11: where

where <unknown:tostring(, UsageCountries)> !contains MailFromCountry

Stage 12: summarize aggregation inside the join branch

summarize by UserId, UserPrincipalName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdsummarize
UserPrincipalNamesummarize

Excessive Windows Logon Failures

#
Status
available
Severity
low
Time window
8d
Group by
Account, AccountType, Computer, EventID, IpAddress, LogonTypeName, Process, Reason, SubStatus, WorkstationName
Source
github.com/Azure/Azure-Sentinel

This query identifies user accounts which has over 50 Windows logon failures today and at least 33% of the count of logon failures over the previous 7 days.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: 2391ce61-8c8d-41ac-9723-d945b2e90720
name: Excessive Windows Logon Failures
description: |
  'This query identifies user accounts which has over 50 Windows logon failures today and at least 33% of the count of logon failures over the previous 7 days.'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let starttime = 8d;
  let endtime = 1d;
  let threshold = 0.333;
  let countlimit = 50;
  SecurityEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4625 and AccountType =~ "User"
  | where IpAddress !in ("127.0.0.1", "::1")
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), CountToday = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress, Process
  | join kind=leftouter (
      SecurityEvent
      | where TimeGenerated between (ago(starttime) .. ago(endtime))
      | where EventID == 4625 and AccountType =~ "User"
      | where IpAddress !in ("127.0.0.1", "::1")
      | summarize CountPrev7day = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
  ) on EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
  | where CountToday >= coalesce(CountPrev7day,0)*threshold and CountToday >= countlimit
  //SubStatus Codes are detailed here - https://docs.microsoft.com/windows/security/threat-protection/auditing/event-4625
  | extend Reason = case(
  SubStatus =~ '0xC000005E', 'There are currently no logon servers available to service the logon request.',
  SubStatus =~ '0xC0000064', 'User logon with misspelled or bad user account',
  SubStatus =~ '0xC000006A', 'User logon with misspelled or bad password',
  SubStatus =~ '0xC000006D', 'Bad user name or password',
  SubStatus =~ '0xC000006E', 'Unknown user name or bad password',
  SubStatus =~ '0xC000006F', 'User logon outside authorized hours',
  SubStatus =~ '0xC0000070', 'User logon from unauthorized workstation',
  SubStatus =~ '0xC0000071', 'User logon with expired password',
  SubStatus =~ '0xC0000072', 'User logon to account disabled by administrator',
  SubStatus =~ '0xC00000DC', 'Indicates the Sam Server was in the wrong state to perform the desired operation',
  SubStatus =~ '0xC0000133', 'Clocks between DC and other computer too far out of sync',
  SubStatus =~ '0xC000015B', 'The user has not been granted the requested logon type (aka logon right) at this machine',
  SubStatus =~ '0xC000018C', 'The logon request failed because the trust relationship between the primary domain and the trusted domain failed',
  SubStatus =~ '0xC0000192', 'An attempt was made to logon, but the Netlogon service was not started',
  SubStatus =~ '0xC0000193', 'User logon with expired account',
  SubStatus =~ '0xC0000224', 'User is required to change password at next logon',
  SubStatus =~ '0xC0000225', 'Evidently a bug in Windows and not a risk',
  SubStatus =~ '0xC0000234', 'User logon with account locked',
  SubStatus =~ '0xC00002EE', 'Failure Reason: An Error occurred during Logon',
  SubStatus =~ '0xC0000413', 'Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine',
  strcat('Unknown reason substatus: ', SubStatus))
  | extend WorkstationName = iff(WorkstationName == "-" or isempty(WorkstationName), Computer , WorkstationName)
  | project StartTime, EndTime, EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, Computer, WorkstationName, IpAddress, CountToday, CountPrev7day, Avg7Day = round(CountPrev7day*1.00/7,2), Process
  | summarize StartTime = min(StartTime), EndTime = max(EndTime), Computer = make_set(Computer,128), IpAddressList = make_set(IpAddress,128), sum(CountToday), sum(CountPrev7day), avg(Avg7Day)
  by EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, WorkstationName, Process
  | order by sum_CountToday desc nulls last
  | extend timestamp = StartTime, NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0]), HostName = tostring(split(WorkstationName, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(WorkstationName, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: WorkstationName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: Process
version: 2.0.3
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 8d;
let endtime = 1d;
let threshold = 0.333;
let countlimit = 50;

Stage 1: source

SecurityEvent

Stage 2: where

| where TimeGenerated >= ago(endtime)

Stage 3: where

| where EventID == 4625 and AccountType =~ "User"

Stage 4: where

| where IpAddress !in ("127.0.0.1", "::1")

Stage 5: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), CountToday = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress, Process

Stage 6: join

| join kind=leftouter (
    SecurityEvent
    | where TimeGenerated between (ago(starttime) .. ago(endtime))
    | where EventID == 4625 and AccountType =~ "User"
    | where IpAddress !in ("127.0.0.1", "::1")
    | summarize CountPrev7day = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
) on EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress

Stage 7: where

where CountToday >= coalesce((CountPrev7day * 0.333), 0) and CountToday >= 50

Stage 8: extend

extend Reason
Reason =
ifSubStatus == 0xC000005E'There are currently no logon servers available to service the logon request.'
elifSubStatus == 0xC0000064'User logon with misspelled or bad user account'
elifSubStatus == 0xC000006A'User logon with misspelled or bad password'
elifSubStatus == 0xC000006D'Bad user name or password'
elifSubStatus == 0xC000006E'Unknown user name or bad password'
elifSubStatus == 0xC000006F'User logon outside authorized hours'
elifSubStatus == 0xC0000070'User logon from unauthorized workstation'
elifSubStatus == 0xC0000071'User logon with expired password'
elifSubStatus == 0xC0000072'User logon to account disabled by administrator'
elifSubStatus == 0xC00000DC'Indicates the Sam Server was in the wrong state to perform the desired operation'
elifSubStatus == 0xC0000133'Clocks between DC and other computer too far out of sync'
elifSubStatus == 0xC000015B'The user has not been granted the requested logon type (aka logon right) at this machine'
elifSubStatus == 0xC000018C'The logon request failed because the trust relationship between the primary domain and the trusted domain failed'
elifSubStatus == 0xC0000192'An attempt was made to logon, but the Netlogon service was not started'
elifSubStatus == 0xC0000193'User logon with expired account'
elifSubStatus == 0xC0000224'User is required to change password at next logon'
elifSubStatus == 0xC0000225'Evidently a bug in Windows and not a risk'
elifSubStatus == 0xC0000234'User logon with account locked'
elifSubStatus == 0xC00002EE'Failure Reason: An Error occurred during Logon'
elifSubStatus == 0xC0000413'Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine'
elsestrcat('Unknown reason substatus: ', SubStatus)

Stage 9: extend

extend WorkstationName
WorkstationName =
if(WorkstationName == "-" or isempty(WorkstationName))Computer
elseWorkstationName

Stage 10: project

project Account, AccountType, Avg7Day, Computer, CountPrev7day, CountToday, EndTime, EventID, IpAddress, LogonTypeName, Process, Reason, StartTime, SubStatus, WorkstationName

Stage 11: summarize

summarize Computer, EndTime, IpAddressList, StartTime by EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, WorkstationName, Process

Stage 12: sort

sort by sum_CountToday

Stage 13: extend

extend DnsDomain, HostName, NTDomain, Name, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountTypeeq
  • User corpus 4 (kusto 4)
field:"AccountType" kind:eq value:"User"
CountTodayge
  • 50 transforms: number
field:"CountToday" kind:ge value:"50"
EventIDeq
  • 4625 corpus 4 (chronicle 2, kusto 2)
field:"EventID" kind:eq value:"4625"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpAddressin127.0.0.1, ::1excludes:IpAddress field:"IpAddress" value:"127.0.0.1" field:"IpAddress" value:"::1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
AccountTypesummarize
Computersummarize
EndTimesummarize
EventIDsummarize
IpAddressListsummarize
LogonTypeNamesummarize
Processsummarize
Reasonsummarize
StartTimesummarize
SubStatussummarize
WorkstationNamesummarize
DnsDomainextend
HostNameextend
NTDomainextend
Nameextend
timestampextend

High bandwidth in the network (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 an unusually high bandwidth which may be an indication of a new service/process or malicious activity on the network. An example scenario is a cyber threat attempting to manipulate the SCADA network.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: caa4665f-21fa-462d-bb31-92226e746c68
name: High bandwidth in the network (Microsoft Defender for IoT)
description: |
  'This alert leverages Defender for IoT to detect an unusually high bandwidth which may be an indication of a new service/process or malicious activity on the network. An example scenario is a cyber threat attempting to manipulate the SCADA network.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T0842
query: |
  SecurityAlert
  | where ProviderName == "IoTSecurity"
  | where AlertName in ("Abnormal Traffic Bandwidth", "Abnormal Traffic Bandwidth Between Devices", "ARP Spoofing", "ICMP Flooding")
  | 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 in ("Abnormal Traffic Bandwidth", "Abnormal Traffic Bandwidth Between Devices", "ARP Spoofing", "ICMP Flooding")

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
AlertNamein
  • ARP Spoofing
  • Abnormal Traffic Bandwidth
  • Abnormal Traffic Bandwidth Between Devices
  • ICMP Flooding
field:"AlertName" kind:in
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

Insider Risk_High User Security Incidents Correlation

#
Severity
high
Time window
7d
Group by
$left.AlertId, $right.SystemAlertId, IncidentName, IncidentNumber, IncidentUrl, ProductName, Severity, SystemAlertId, UPN
Source
github.com/Azure/Azure-Sentinel

'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see Investigate incidents with Microsoft Sentinel.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: 28a75d10-9b75-4192-9863-e452c3ad24db
name: Insider Risk_High User Security Incidents Correlation
description: |
  'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see [Investigate incidents with Microsoft Sentinel]( https://docs.microsoft.com/azure/sentinel/investigate-cases).'
severity: High
requiredDataConnectors:
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert (MDATP)
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert (ASC)
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert (ASC for IoT)
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (MCAS)
  - connectorId: OfficeATP
    dataTypes:
      - SecurityAlert (Office 365)
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  let PreviousIncidents =
  SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
  | mv-expand todynamic(Entities)
  | where Entities["Type"] =~ "account"
  | extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
            Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
  | extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
  | extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
  | where UPN <> ""
  | summarize PreviousIncidents = make_set(IncidentNumber) by UPN;
  let LastTimeObserved =
  SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
  | mv-expand todynamic(Entities)
  | where Entities["Type"] =~ "account"
  | extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
            Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
  | extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
  | extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
  | where UPN <> ""
  | summarize arg_max(TimeGenerated, IncidentName) by UPN;
    SecurityIncident
  | summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
  | mv-expand AlertIds
  | extend AlertId = tostring(AlertIds)
  | join kind= innerunique ( 
            SecurityAlert 
            )
            on $left.AlertId == $right.SystemAlertId
  | summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
  | mv-expand todynamic(Entities)
  | where Entities["Type"] =~ "account"
  | extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
            Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
  | extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
  | extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
  | where UPN <> ""
  | summarize count() by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName
  | extend SecurityIncidents = count_
  | where SecurityIncidents > 5 //Adjust & Tune within Organzational Requirements
  | join (LastTimeObserved) on UPN
  | project-rename LastObserved = TimeGenerated, LastIncident = IncidentNumber
  | project-away IncidentName, count_, UPN1, IncidentName1, Severity, IncidentUrl
  | join kind=inner (PreviousIncidents) on UPN
  // | lookup kind=inner _GetWatchlist('<Your Watchlist Name>') on $left.UPN == $right.SearchKey
  | project UPN, SecurityIncidents, LastIncident, ProductName, LastObserved, PreviousIncidents
  | sort by SecurityIncidents desc
  | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1])
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 3d
    matchingMethod: Selected
    groupByEntities:
      - Account
version: 1.1.2
kind: Scheduled

Stages and Predicates

Let Binding: PreviousIncidents used in Stages 17, 18

let PreviousIncidents = SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
          Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize PreviousIncidents = make_set(IncidentNumber) by UPN;

Let Binding: LastTimeObserved used in Stage 14

let LastTimeObserved = SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]), 
          Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize arg_max(TimeGenerated, IncidentName) by UPN;

Stage 1: source

SecurityIncident

Stage 2: summarize

| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber

Stage 3: mv-expand

| mv-expand AlertIds

Stage 4: extend

| extend AlertId = tostring(AlertIds)

Stage 5: join

| join kind= innerunique ( 
          SecurityAlert 
          )
          on $left.AlertId == $right.SystemAlertId

Stage 6: summarize

summarize NumberOfUpdates by SystemAlertId

Stage 7: mv-expand

mv-expand

Stage 8: where

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

Stage 9: extend (3 consecutive steps)

extend AadTenantId, AadUserId, Host, Href_, IsDomainJoined, NTDomain, Name, Sid, UPN, UPNSuffix

Stage 10: where

where UPN != ""

Stage 11: summarize

summarize by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName

Stage 12: extend

extend SecurityIncidents

Stage 13: where

where SecurityIncidents > 5

Stage 14: join

join (LastTimeObserved) on UPN

Stage 15: project-rename

project-rename

Stage 16: project-away

project-away IncidentName, IncidentName1, IncidentUrl, Severity, UPN1, count_

Stage 17: join

join kind=inner (PreviousIncidents) on UPN

Stage 18: project

project LastIncident, LastObserved, PreviousIncidents, ProductName, SecurityIncidents, UPN

Stage 19: sort

sort by SecurityIncidents

Stage 20: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Entities["Type"]eq
  • account transforms: kusto_dynamic_path
SecurityIncidentsgt
  • 5 transforms: number
field:"SecurityIncidents" kind:gt value:"5"

Output Fields

These fields are emitted when the rule matches.

FieldSource
LastIncidentproject
LastObservedproject
PreviousIncidentsproject
ProductNameproject
SecurityIncidentsproject
UPNproject
AccountNameextend
AccountUPNSuffixextend

Keeper Security - Password Changed

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

'Creates an informational incident based on Keeper Security Password Changed data in Microsoft Sentinel'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: f031fbbc-37d8-4667-b795-d386bf2b5ab2
name: Keeper Security - Password Changed
description: |
  'Creates an informational incident based on Keeper Security Password Changed data in Microsoft Sentinel'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: KeeperSecurityPush2
    dataTypes: 
      - KeeperSecurityEventNewLogs_CL
suppressionDuration: PT5H
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
tactics:
- Persistence
relevantTechniques:
- T1556
query: |
  KeeperSecurityEventNewLogs_CL
  | where AuditEvent == "change_master_password"
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: "{{AuditEvent}} on {{RemoteAddress}}"
  alertDescriptionFormat: "{{AuditEvent}} has been captured in the Keeper Security Event Logs"
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: Username
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: RemoteAddress
version: 1.0.3
kind: NRT

Stages and Predicates

Stage 1: source

KeeperSecurityEventNewLogs_CL

Stage 2: where

| where AuditEvent == "change_master_password"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AuditEventeq
  • change_master_password
field:"AuditEvent" kind:eq value:"change_master_password"

Ransomware Attack Detected

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

Identifies ransomware attacks detected by the Ransomware Protection service running on a Nasuni Edge Appliance.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 6c8770fb-c854-403e-a64d-0293ba344d5f
name: Ransomware Attack Detected
description: 'Identifies ransomware attacks detected by the Ransomware Protection service running on a Nasuni Edge Appliance.'
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Impact
relevantTechniques:
  - T1486
query: |-
  Syslog
  | project TimeGenerated, Computer, SyslogMessage
  | where SyslogMessage has "The Filer has detected a new ransomware attack"
  | extend pattern = substring(SyslogMessage, indexof(SyslogMessage, "(")+1, indexof(SyslogMessage, ")") - indexof(SyslogMessage,"(")-1)
  | extend volume_name = substring(SyslogMessage, indexof(SyslogMessage, "volume")+7, indexof(SyslogMessage,". Visit") - (indexof(SyslogMessage, "volume")+7))
  | sort by TimeGenerated desc
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertnameFormat: 'Nasuni: Ransomware Attack Detected'
  alertDescriptionFormat: Ransomware attack detected by Nasuni at {{TimeGenerated}}.
  alertDynamicProperties:
  - alertProperty: RemediationSteps
    value: SyslogMessage
customDetails:
  VolumeName: volume_name
entityMappings:
- entityType: Malware
  fieldMappings:
  - identifier: Name
    columnName: pattern
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.3

Stages and Predicates

Stage 1: source

Syslog

Stage 2: project

| project TimeGenerated, Computer, SyslogMessage

Stage 3: where

| where SyslogMessage has "The Filer has detected a new ransomware attack"

Stage 4: extend

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

Stage 5: extend

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

Stage 6: sort

| sort by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • The Filer has detected a new ransomware attack transforms: term
field:"SyslogMessage" kind:match value:"The Filer has detected a new ransomware attack"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
SyslogMessageproject
TimeGeneratedproject
patternextend
volume_nameextend

Tailscale: External (shared-in) device added

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

Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: b2c3d4e5-6789-0123-4567-890123450043
name: "Tailscale: External (shared-in) device added"
description: |
  Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let recent =
      Tailscale_Devices_CL
      | where TimeGenerated > ago(1h)
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where IsExternal == true
      | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags;
  let prior =
      Tailscale_Devices_CL
      | where TimeGenerated between (ago(2d) .. ago(1h))
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where IsExternal == true
      | distinct DeviceId;
  recent
  | join kind=leftanti prior on DeviceId
  | project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

let recent is inlined into the numbered stages below.

Let Binding: prior used in Stage 6

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

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

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where IsExternal == true

Stage 5: project

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

Stage 6: join (negated)

join kind=leftanti (prior) on DeviceId

Stage 7: project

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

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IsExternaleqtrueexcludes:IsExternal field:"IsExternal" value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientVersionproject
Createdproject
DeviceNameproject
Hostnameproject
LastSeenproject
Osproject
Tagsproject
TimeGeneratedproject
Userproject

Tenant Password Changed

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

Detects when a tenant password is updated.

Rule Body

id: afec0070-2b46-4366-a272-e2d9cc8a50f2
name: Tenant Password Changed
description: Detects when a tenant password is updated.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 24114\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\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 == 24114

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

ThreatConnect TI Map URL Entity to OfficeActivity Data

#
Severity
medium
Time window
14d
Group by
IndicatorId, Url
Source
github.com/Azure/Azure-Sentinel

'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 12c3b31b-66a6-53ff-b6ab-6ae45e56dc92
name: ThreatConnect TI Map URL Entity to OfficeActivity Data
version: 1.0.1
kind: Scheduled
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
     - OfficeActivity
  - connectorId: ThreatIntelligence
    dataTypes:
     - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
    ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where ExpirationDateTime > now() and Active == true
  // Filter out non ThreatConnect TI Sources
  | where SourceSystem startswith "ThreatConnect-"
  // Picking up only IOC's that contain the entities we want
  | where isnotempty(Url)
  // 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 (
    OfficeActivity
    | where TimeGenerated >= ago(dt_lookBack)
    //Extract the Url from a number of potential fields
    | extend Url = iif(OfficeWorkload == "AzureActiveDirectory",extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+);", 1,ModifiedProperties),tostring(parse_json(ModifiedProperties)[12].NewValue))
    | where isnotempty(Url)
    // Ensure we get a clean URL
    | extend Url = tostring(split(Url, ';')[0])
    | extend OfficeActivity_TimeGenerated = TimeGenerated
    // Project a single user identity that we can use for entity mapping
    | extend User = iif(isnotempty(UserId), UserId, iif(isnotempty(Actor), tostring(parse_json(Actor)[0].ID), tostring(parse_json(Parameters)[0].Value)))
  ) on Url
  | where OfficeActivity_TimeGenerated < ExpirationDateTime
  | summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, Url
  | project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Operation,
  UserType, OfficeWorkload, Parameters, Url, User
  | extend timestamp = OfficeActivity_TimeGenerated, Name = tostring(split(User, '@', 0)[0]), UPNSuffix = tostring(split(User, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 4: where

| where ExpirationDateTime > now() and Active == true

Stage 5: where

| where SourceSystem startswith "ThreatConnect-"

Stage 6: where

| where isnotempty(Url)

Stage 7: join

| join kind=innerunique (
  OfficeActivity
  | where TimeGenerated >= ago(dt_lookBack)
  | extend Url = iif(OfficeWorkload == "AzureActiveDirectory",extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+);", 1,ModifiedProperties),tostring(parse_json(ModifiedProperties)[12].NewValue))
  | where isnotempty(Url)
  | extend Url = tostring(split(Url, ';')[0])
  | extend OfficeActivity_TimeGenerated = TimeGenerated
  | extend User = iif(isnotempty(UserId), UserId, iif(isnotempty(Actor), tostring(parse_json(Actor)[0].ID), tostring(parse_json(Parameters)[0].Value)))
) on Url

Stage 8: where

where OfficeActivity_TimeGenerated < ExpirationDateTime

Stage 9: summarize

summarize OfficeActivity_TimeGenerated by IndicatorId, Url

Stage 10: project

project ActivityGroupNames, ConfidenceScore, Description, ExpirationDateTime, IndicatorId, OfficeActivity_TimeGenerated, OfficeWorkload, Operation, Parameters, ThreatType, Url, User, UserType

Stage 11: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ConfidenceScoreproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
OfficeActivity_TimeGeneratedproject
OfficeWorkloadproject
Operationproject
Parametersproject
ThreatTypeproject
Urlproject
Userproject
UserTypeproject
Nameextend
UPNSuffixextend
timestampextend

TI map Domain entity to EmailEvents

#
Severity
medium
Time window
14d
Group by
$left.TI_Domain, $right.RecipientEmailDomain, $right.SenderFromDomain, IndicatorId, RecipientEmailAddress
Source
github.com/Azure/Azure-Sentinel

Identifies a match in EmailEvents table from any Domain IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 96307710-8bb9-4b45-8363-a90c72ebf86f
name: TI map Domain entity to EmailEvents
description: |
  Identifies a match in EmailEvents table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - EmailEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename  EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));
  let SenderDomains = EmailEvents_ | distinct  SenderFromDomain | summarize make_list(SenderFromDomain);
  let RecipientDomains = EmailEvents_ | distinct  RecipientEmailDomain | summarize make_list(RecipientEmailDomain);
  let TI = materialize(ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack)
  | where isnotempty(DomainName)
  | extend TI_Domain = tolower(DomainName)
  | where TI_Domain in (SenderDomains) or TI_Domain in (RecipientDomains)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
  (union
    (TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.SenderFromDomain),
    (TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.RecipientEmailDomain))
  | where EmailEvents_TimeGenerated < ExpirationDateTime
  | summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by IndicatorId, RecipientEmailAddress
  | project EmailEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_Domain, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
  | extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
  | extend timestamp = EmailEvents_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RecipientEmailAddress
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Let Binding: EmailEvents_

let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename  EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));

Let Binding: SenderDomains

let SenderDomains = EmailEvents_ | distinct  SenderFromDomain | summarize make_list(SenderFromDomain);

Let Binding: RecipientDomains

let RecipientDomains = EmailEvents_ | distinct  RecipientEmailDomain | summarize make_list(RecipientEmailDomain);

Union (2 Sources)

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

Leg 1: TI

TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.SenderFromDomain

Leg 2: TI

TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.RecipientEmailDomain

Applied to the Combined Result

| where EmailEvents_TimeGenerated < ExpirationDateTime
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by IndicatorId, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_Domain, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"
DeliveryActionmatchBlockedexcludes:DeliveryAction field:"DeliveryAction" value:"Blocked"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ConfidenceLevelproject
ConfidenceScoreproject
DeliveryActionproject
DeliveryLocationproject
Descriptionproject
DomainNameproject
EmailDirectionproject
EmailEvents_TimeGeneratedproject
ExpirationDateTimeproject
IndicatorIdproject
RecipientEmailAddressproject
SenderFromAddressproject
Subjectproject
TI_Domainproject
ThreatTypeproject
TrafficLightProtocolLevelproject
Typeproject
Urlproject
Nameextend
UPNSuffixextend
timestampextend

UniFi Site Manager: Firmware Update Available

#
Status
available
Severity
low
Time window
1d
Group by
Id
Source
github.com/Azure/Azure-Sentinel

Identifies UniFi devices with firmware updates available. Keeping firmware patched is critical as updates often include security vulnerability fixes.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 83b88ab5-21ca-5dd2-df91-6db4354f9360
name: "UniFi Site Manager: Firmware Update Available"
description: |
  Identifies UniFi devices with firmware updates available. Keeping firmware patched is critical as updates often include security vulnerability fixes.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Devices_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
subTechniques: []
query: |
  Unifi_SiteManager_Devices_CL
  | where TimeGenerated > ago(1d)
  | summarize arg_max(TimeGenerated, *) by Id
  | where isnotempty(UpdateAvailable)
  | extend DeviceDetail = strcat(coalesce(Name, "Unnamed"), " (", Model, ") ", Version, " -> ", UpdateAvailable, " @", Ip)
  | summarize
      DevicesNeedingUpdate = count(),
      DeviceList = make_list(DeviceDetail),
      Models = make_set(Model),
      ProductLines = make_set(ProductLine)
  | where DevicesNeedingUpdate >= 1
  | extend
      TimeGenerated = now(),
      AffectedModels = strcat_array(Models, ", "),
      AffectedProductLines = strcat_array(ProductLines, ", "),
      DeviceListString = strcat_array(DeviceList, "; ")
  | extend Activity = strcat(DevicesNeedingUpdate, ' device(s) have firmware updates available: ', AffectedModels)
  | project
      TimeGenerated,
      DevicesNeedingUpdate,
      AffectedModels,
      AffectedProductLines,
      Activity,
      DeviceListString
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AffectedProductLines
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AnyAlert
kind: Scheduled
version: 1.0.2

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by Id

Stage 4: where

| where isnotempty(UpdateAvailable)

Stage 5: extend

| extend DeviceDetail = strcat(coalesce(Name, "Unnamed"), " (", Model, ") ", Version, " -> ", UpdateAvailable, " @", Ip)

Stage 6: summarize

| summarize
    DevicesNeedingUpdate = count(),
    DeviceList = make_list(DeviceDetail),
    Models = make_set(Model),
    ProductLines = make_set(ProductLine)

Stage 7: where

| where DevicesNeedingUpdate >= 1

Stage 8: extend

| extend
    TimeGenerated = now(),
    AffectedModels = strcat_array(Models, ", "),
    AffectedProductLines = strcat_array(ProductLines, ", "),
    DeviceListString = strcat_array(DeviceList, "; ")

Stage 9: extend

| extend Activity = strcat(DevicesNeedingUpdate, ' device(s) have firmware updates available: ', AffectedModels)

Stage 10: project

| project
    TimeGenerated,
    DevicesNeedingUpdate,
    AffectedModels,
    AffectedProductLines,
    Activity,
    DeviceListString

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
AffectedModelsproject
AffectedProductLinesproject
DeviceListStringproject
DevicesNeedingUpdateproject
TimeGeneratedproject

Workspace deletion activity from an infected device

#
Severity
medium
Time window
14d
Group by
$left.AadTenantId, $left.AadUserId, $right.AccountObjectId, $right.AccountTenantId, IpAddress, UserAccount
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query will alert on any sign-ins from devices infected with malware in correlation with workspace deletion activity. Attackers may attempt to delete workspaces containing compute instances after successful compromise to cause service unavailability to regular business operation.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Impact

Telemetry Coverage

Rule Body

id: a5b3429d-f1da-42b9-883c-327ecb7b91ff
name: Workspace deletion activity from an infected device
description: |
  'This query will alert on any sign-ins from devices infected with malware in correlation with workspace deletion activity. 
  Attackers may attempt to delete workspaces containing compute instances after successful compromise to cause service unavailability to regular business operation.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Impact
relevantTechniques:
  - T1078
  - T1489
query: |
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName == "Azure Active Directory Identity Protection"
    | where AlertName == "Sign-in from an infected device"
    | mv-apply EntityAccount=todynamic(Entities) on
    (
    where EntityAccount.Type == "account"
    | extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
    )
    | mv-apply EntityIp=todynamic(Entities) on
    (
    where EntityIp.Type == "ip"
    | extend IpAddress = tostring(EntityIp.Address)
    )
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserAccount = AccountUPN
    | extend UserName = AccountDisplayName
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | project  AlertName, AlertSeverity, CompromisedEntity, UserAccount, IpAddress, TimeGenerated, UserName
    | join kind=inner 
    (
    AzureActivity
    | where OperationNameValue has_any ("/workspaces/computes/delete", "workspaces/delete") 
    | where ActivityStatusValue has_any ("Succeeded", "Success")
    | project TimeGenerated, ResourceProviderValue, _ResourceId, SubscriptionId, UserAccount=Caller, IpAddress=CallerIpAddress, CorrelationId, OperationId, ResourceGroup, TenantId
    ) on IpAddress, UserAccount
    | extend AccountName = tostring(split(UserAccount, "@")[0]), AccountUPNSuffix = tostring(split(UserAccount, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserAccount
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: _ResourceId
version: 1.0.8
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Platform" ]

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where ProductName == "Azure Active Directory Identity Protection"

Stage 4: where

| where AlertName == "Sign-in from an infected device"

Stage 5: kusto:mv-apply

| mv-apply EntityAccount=todynamic(Entities) on
(
where EntityAccount.Type == "account"
| extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
)

Stage 6: kusto:mv-apply

| mv-apply EntityIp=todynamic(Entities) on
(
where EntityIp.Type == "ip"
| extend IpAddress = tostring(EntityIp.Address)
)

Stage 7: join

| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserAccount = AccountUPN
| extend UserName = AccountDisplayName
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId

Stage 8: extend

extend CompromisedEntity
CompromisedEntity =
if(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccount
elseCompromisedEntity

Stage 9: project

project AlertName, AlertSeverity, CompromisedEntity, IpAddress, TimeGenerated, UserAccount, UserName

Stage 10: join

| join kind=inner 
(
AzureActivity
| where OperationNameValue has_any ("/workspaces/computes/delete", "workspaces/delete") 
| where ActivityStatusValue has_any ("Succeeded", "Success")
| project TimeGenerated, ResourceProviderValue, _ResourceId, SubscriptionId, UserAccount=Caller, IpAddress=CallerIpAddress, CorrelationId, OperationId, ResourceGroup, TenantId
) on IpAddress, UserAccount

Stage 11: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountDisplayNameis_not_null
  • (no value, null check)
field:"AccountDisplayName" kind:is_not_null
ActivityStatusValuematch
  • Succeeded transforms: term
  • Success transforms: term
field:"ActivityStatusValue" kind:match
AlertNameeq
  • Sign-in from an infected device
field:"AlertName" kind:eq value:"Sign-in from an infected device"
EntityAccount.Typeeq
  • account transforms: kusto_dynamic_path
field:"EntityAccount.Type" kind:eq value:"account"
EntityIp.Typeeq
  • ip transforms: kusto_dynamic_path
field:"EntityIp.Type" kind:eq value:"ip"
OperationNameValuematch
  • /workspaces/computes/delete transforms: term
  • workspaces/delete transforms: term
field:"OperationNameValue" kind:match
ProductNameeq
  • Azure Active Directory Identity Protection
field:"ProductName" kind:eq value:"Azure Active Directory Identity Protection"
UserAccountis_not_null
  • (no value, null check)
field:"UserAccount" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertNameproject
AlertSeverityproject
CompromisedEntityproject
IpAddressproject
TimeGeneratedproject
UserAccountproject
UserNameproject
AccountNameextend
AccountUPNSuffixextend

Back to all Kusto rules