Detection rules › Kusto

Kusto rules: 1v

Anomalous login followed by Teams action

#
Severity
medium
Time window
14d
Group by
$left.UserId, $right.UserPrincipalName, OperationTimeGenerated, SuspiciousCountryPrevalence, SuspiciousIP, SuspiciousSigninCountry, UserPrincipalName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects anomalous IP address usage by user accounts and then checks to see if a suspicious Teams action is performed. Query calculates IP usage Delta for each user account and selects accounts where a delta >= 90% is observed between the most and least used IP. To further reduce results the query performs a prevalence check on the lowest used IP's country, only keeping IP's where the country is unusual for the tenant (dynamic ranges). Please note, if the initial logic of prevalence to find suspicious logon activity is noisy then consider adding filtering based on Location. Finally the user accounts activity within Teams logs is checked for suspicious commands (modifying user privileges or admin actions) during the period the suspicious IP was active.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2b701288-b428-4fb8-805e-e4372c574786
name: Anomalous login followed by Teams action
description: |
  'Detects anomalous IP address usage by user accounts and then checks to see if a suspicious Teams action is performed.
  Query calculates IP usage Delta for each user account and selects accounts where a delta >= 90% is observed between the most and least used IP.
  To further reduce results the query performs a prevalence check on the lowest used IP's country, only keeping IP's where the country is unusual for the tenant (dynamic ranges). 
  Please note,  if the initial logic of prevalence to find suspicious logon activity is noisy then consider adding filtering based on Location. 
  Finally the user accounts activity within Teams logs is checked for suspicious commands (modifying user privileges or admin actions) during the period the suspicious IP was active.'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1199
  - T1136
  - T1078
  - T1098
query: |
  //The bigger the window the better the data sample size, as we use IP prevalence, more sample data is better.
  //The minimum number of countries that the account has been accessed from [default: 2]
  let minimumCountries = 2;
  //The delta (%) between the largest in-use IP and the smallest [default: 95]
  let deltaThreshold = 95;
  //The maximum (%) threshold that the country appears in login data [default: 10]
  let countryPrevalenceThreshold = 10;
  //The time to project forward after the last login activity [default: 60min]
  let projectedEndTime = 60m;
  let queryfrequency = 1d;
  let queryperiod = 14d;
  let aadFunc = (tableName: string) {
      // Get successful signins to Teams
      let signinData =
          table(tableName)
          | where TimeGenerated > ago(queryperiod)
          | where AppDisplayName has "Teams" and ConditionalAccessStatus =~ "success"
          | extend Country = tostring(todynamic(LocationDetails)['countryOrRegion'])
          | where isnotempty(Country) and isnotempty(IPAddress);
      // Calculate prevalence of countries
      let countryPrevalence =
          signinData
          | summarize CountCountrySignin = count() by Country
          | extend TotalSignin = toscalar(signinData | summarize count())
          | extend CountryPrevalence = toreal(CountCountrySignin) / toreal(TotalSignin) * 100;
      // Count signins by user and IP address
      let userIpSignin =
          signinData
          | summarize CountIPSignin = count(), Country = any(Country), ListSigninTimeGenerated = make_list(TimeGenerated) by IPAddress, UserPrincipalName;
      // Calculate delta between the IP addresses with the most and minimum activity by user
      let userIpDelta =
          userIpSignin
          | summarize MaxIPSignin = max(CountIPSignin), MinIPSignin = min(CountIPSignin), DistinctCountries = dcount(Country), make_set(Country) by UserPrincipalName
          | extend UserIPDelta = toreal(MaxIPSignin - MinIPSignin) / toreal(MaxIPSignin) * 100;
      // Collect Team operations the user account has performed within a time range of the suspicious signins
      OfficeActivity
      | where TimeGenerated > ago(queryfrequency)
      | where Operation in~ ("TeamsAdminAction", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "AppInstalled", "BotAddedToTeam")
      | where not (Operation in~ ("MemberAdded", "MemberRemoved") and CommunicationType in~ ("GroupChat", "OneonOne")) // These events have been noisy and are related to initiaing chat conversation and not admin operations.
      | project OperationTimeGenerated = TimeGenerated, UserId = tolower(UserId), Operation
      | join kind = inner(
          userIpDelta
          // Check users with activity from distinct countries
          | where DistinctCountries >= minimumCountries
          // Check users with high IP delta
          | where UserIPDelta >= deltaThreshold
          // Add information about signins and countries
          | join kind = leftouter userIpSignin on UserPrincipalName
          | join kind = leftouter countryPrevalence on Country
          // Check activity that comes from nonprevalent countries
          | where CountryPrevalence < countryPrevalenceThreshold
          | project
              UserPrincipalName,
              SuspiciousIP = IPAddress,
              UserIPDelta,
              SuspiciousSigninCountry = Country,
              SuspiciousCountryPrevalence = CountryPrevalence,
              EventTimes = ListSigninTimeGenerated
      ) on $left.UserId == $right.UserPrincipalName
      // Check the signins occured 60 min before the Teams operations
      | mv-expand SigninTimeGenerated = EventTimes
      | extend SigninTimeGenerated = todatetime(SigninTimeGenerated)
      | where OperationTimeGenerated between (SigninTimeGenerated .. (SigninTimeGenerated + projectedEndTime))
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  | summarize arg_max(SigninTimeGenerated, *) by UserPrincipalName, SuspiciousIP, OperationTimeGenerated
  | summarize
      ActivitySummary = make_bag(pack(tostring(SigninTimeGenerated), pack("Operation", tostring(Operation), "OperationTime", OperationTimeGenerated)))
      by UserPrincipalName, SuspiciousIP, SuspiciousSigninCountry, SuspiciousCountryPrevalence
  | extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SuspiciousIP
version: 1.1.2
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let minimumCountries = 2;
let deltaThreshold = 95;
let countryPrevalenceThreshold = 10;
let projectedEndTime = 60m;
let queryfrequency = 1d;
let queryperiod = 14d;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName: string) {
    let signinData =
        table(tableName)
        | where TimeGenerated > ago(queryperiod)
        | where AppDisplayName has "Teams" and ConditionalAccessStatus =~ "success"
        | extend Country = tostring(todynamic(LocationDetails)['countryOrRegion'])
        | where isnotempty(Country) and isnotempty(IPAddress);

Let Binding: countryPrevalence

let countryPrevalence = signinData
        | summarize CountCountrySignin = count() by Country
        | extend TotalSignin = toscalar(signinData | summarize count())
        | extend CountryPrevalence = toreal(CountCountrySignin) / toreal(TotalSignin) * 100;

Let Binding: userIpSignin

let userIpSignin = signinData
        | summarize CountIPSignin = count(), Country = any(Country), ListSigninTimeGenerated = make_list(TimeGenerated) by IPAddress, UserPrincipalName;

Let Binding: userIpDelta used in Stages 7, 16

let userIpDelta = userIpSignin
        | summarize MaxIPSignin = max(CountIPSignin), MinIPSignin = min(CountIPSignin), DistinctCountries = dcount(Country), make_set(Country) by UserPrincipalName
        | extend UserIPDelta = toreal(MaxIPSignin - MinIPSignin) / toreal(MaxIPSignin) * 100;

Stage 1: union

union of 2 branches

Stage 2: source

OfficeActivity

Stage 3: where

where TimeGenerated > ago(86400s)

Stage 4: where

where Operation in~ ("AppInstalled", "BotAddedToTeam", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "TeamsAdminAction")

Stage 5: where

where not ((CommunicationType in~ ("GroupChat", "OneonOne") and Operation in~ ("MemberAdded", "MemberRemoved")))

Stage 6: project

project Operation, OperationTimeGenerated, UserId

Stage 7: join

join kind=inner (userIpDelta) on $left.UserId, $right.UserPrincipalName

Stage 8: mv-expand

mv-expand SigninTimeGenerated

Stage 9: extend

extend SigninTimeGenerated

Stage 10: where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h

where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h

Stage 11: source

OfficeActivity

Stage 12: where

where TimeGenerated > ago(86400s)

Stage 13: where

where Operation in~ ("AppInstalled", "BotAddedToTeam", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "TeamsAdminAction")

Stage 14: where

where not ((CommunicationType in~ ("GroupChat", "OneonOne") and Operation in~ ("MemberAdded", "MemberRemoved")))

Stage 15: project

project Operation, OperationTimeGenerated, UserId

Stage 16: join

join kind=inner (userIpDelta) on $left.UserId, $right.UserPrincipalName

Stage 17: mv-expand

mv-expand SigninTimeGenerated

Stage 18: extend

extend SigninTimeGenerated

Stage 19: where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h

where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h

Stage 20: summarize

summarize by UserPrincipalName, SuspiciousIP, OperationTimeGenerated

Stage 21: summarize

summarize ActivitySummary by UserPrincipalName, SuspiciousIP, SuspiciousSigninCountry, SuspiciousCountryPrevalence

Stage 22: extend

extend AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CountryPrevalencelt
  • 10 transforms: number
field:"CountryPrevalence" kind:lt value:"10"
DistinctCountriesge
  • 2 transforms: number
field:"DistinctCountries" kind:ge value:"2"
Operationin
  • AppInstalled
  • BotAddedToTeam
  • MemberAdded
  • MemberRemoved
  • MemberRoleChanged
  • TeamsAdminAction
field:"Operation" kind:in
UserIPDeltage
  • 95 transforms: number
field:"UserIPDelta" kind:ge value:"95"

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivitySummarysummarize
SuspiciousCountryPrevalencesummarize
SuspiciousIPsummarize
SuspiciousSigninCountrysummarize
UserPrincipalNamesummarize
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13 Privileges Against Tier Zero Group

#

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 - Non Tier Zero Principals With ADCS ESC13 Privileges Against Tier Zero Group

Rule Body

id: 82c9f9b1-2990-4d5c-a5f4-26e4e54c904c
name: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13 Privileges
  Against Tier Zero Group
description: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13
  Privileges Against Tier Zero Group
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 == "T0ADCSESC13"
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 == "T0ADCSESC13"

Indicators

These rows show field, operator, and value matches.

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

Copilot - Plugin Created by Non-Admin User

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

'Detects when a normal user creates a Copilot plugin. This can be used to inject malicious prompts, tools, or data exfiltration paths. This rule identifies potential persistence or privilege misuse scenarios where non-administrative users create plugins that could be leveraged for malicious purposes.'

MITRE ATT&CK Coverage

Rule Body

id: a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6
name: Copilot - Plugin Created by Non-Admin User
description: |
  'Detects when a normal user creates a Copilot plugin. This can be used to inject malicious prompts, tools, or data exfiltration paths.
  This rule identifies potential persistence or privilege misuse scenarios where non-administrative users create plugins that could be leveraged for malicious purposes.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftCopilot
    dataTypes:
      - CopilotActivity
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1546
  - T1098
query: |
  CopilotActivity
  | where RecordType == "CreateCopilotPlugin"
  | where ActorUserType != "Admin"
  | extend Data = parse_json(LLMEventData)
  | extend Plugin = tostring(Data.Resource[0].Property)
  | project TimeGenerated, ActorName, ActorUserId, SrcIpAddr, Plugin
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CopilotActivity

Stage 2: where

| where RecordType == "CreateCopilotPlugin"

Stage 3: where

| where ActorUserType != "Admin"

Stage 4: extend

| extend Data = parse_json(LLMEventData)

Stage 5: extend

| extend Plugin = tostring(Data.Resource[0].Property)

Stage 6: project

| project TimeGenerated, ActorName, ActorUserId, SrcIpAddr, Plugin

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorNameproject
ActorUserIdproject
Pluginproject
SrcIpAddrproject
TimeGeneratedproject

Cortex XDR Incident - Low

#

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
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '

Rule Body

id: 1426bbcf-a9ae-4aa5-9da6-abbf48f04115
name: Cortex XDR Incident - Low
description: |
  'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '
severity: Low
requiredDataConnectors:
  - connectorId: CortexXDR
    dataTypes:
      - CortexXDR_Incidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride: null
customDetails: null
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: users_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: xdr_url_s
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: hosts_s
sentinelEntitiesMappings: null              
query: | 
  CortexXDR_Incidents_CL
  | where severity_s contains "low"
  | where status_s !contains "resolved_auto"
  // the below line filters for incidents that has at least one user or one host in it. Comment the below line if you want to generated incidents that have 0 user or 0 host as well
  | where host_count_d > 0 or user_count_d > 0
  | project TimeGenerated, incident_id_s, status_s, severity_s, incident_sources_s, description_s, xdr_url_s, hosts_s, users_s, alert_categories_s, host_count_d, user_count_d, alert_count_d, creation_time_d, modification_time_d
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CortexXDR_Incidents_CL

Stage 2: where

| where severity_s contains "low"

Stage 3: where

| where status_s !contains "resolved_auto"

Stage 4: where

| where host_count_d > 0 or user_count_d > 0

Stage 5: project

| project TimeGenerated, incident_id_s, status_s, severity_s, incident_sources_s, description_s, xdr_url_s, hosts_s, users_s, alert_categories_s, host_count_d, user_count_d, alert_count_d, creation_time_d, modification_time_d

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
status_scontainsresolved_autoexcludes:status_s field:"status_s" value:"resolved_auto"

Output Fields

These fields are emitted when the rule matches.

FieldSource
TimeGeneratedproject
alert_categories_sproject
alert_count_dproject
creation_time_dproject
description_sproject
host_count_dproject
hosts_sproject
incident_id_sproject
incident_sources_sproject
modification_time_dproject
severity_sproject
status_sproject
user_count_dproject
users_sproject
xdr_url_sproject

Cyble Vision Alerts Cyble Web Applications

#

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

'Creates an incident for each discovered or exposed web application detected by Cyble Intelligence. Useful for SOC teams to investigate externally facing login portals, misconfigurations, and exposed infrastructure.'

MITRE ATT&CK Coverage

Rule Body

id: 359ddb25-eab1-4ef5-8303-ed3a9b680690
name: Cyble Vision Alerts Cyble Web Applications
description: |
  'Creates an incident for each discovered or exposed web application detected by Cyble Intelligence. Useful for SOC teams to investigate externally facing login portals, misconfigurations, and exposed infrastructure.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: PT5M
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Reconnaissance
relevantTechniques:
- T1595
- T1592
query: |
  Alerts_web_applications
  | where Service == "web_applications"
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: Cyble Web Application Found  {{WA_Title_Nested}}
  alertDescriptionFormat: |
    A web application was detected by Cyble Intelligence.
    Host {{WA_Host_Nested}}
    Port {{WA_Port_Nested}}
    URL {{WA_URL}}
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  Host: WA_Host_Nested
  DetectedURL: WA_URL
  Title: WA_Title_Nested
entityMappings:
- entityType: URL
  fieldMappings:
  - identifier: Url
    columnName: WA_URL
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IP
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_web_applications

Stage 2: where

| where Service == "web_applications"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Cyble Vision Alerts Docker

#

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 Docker Hub container entries related to monitored keywords. Uses Alerts_docker parser. Includes metadata such as developer, stars, downloads, and image URL. Raises one incident per alert.'

MITRE ATT&CK Coverage

Rule Body

id: 47dee28d-fa74-49cd-b5fb-397b047a73c0
name: Cyble Vision Alerts Docker
description: |
  'Detects Docker Hub container entries related to monitored keywords. Uses Alerts_docker parser. Includes metadata such as developer, stars, downloads, and image URL. Raises one incident per alert.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Exfiltration
- Execution
- Discovery
relevantTechniques:
- T1537
- T1610
- T1083
query: |
  Alerts_docker  
  | where Service == "docker" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: Docker Image Identified {{DK_Title}}
  alertDescriptionFormat: |
    A Docker Hub image related to monitored entity {{KeywordName}} was discovered. Developer {{DK_Developer}}. Downloads {{DK_Downloads}}. Review metadata and verify if the image poses supply chain or impersonation risk.
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  DK_Title: DK_Title
  DK_description: DK_Description
  DK_Developer: DK_Developer
  DK_DeveloperUrl: DK_DeveloperUrl
  DK_Downloads: DK_Downloads
  DK_Stars: DK_Stars
  DK_Url: DK_Url
entityMappings:
- entityType: Url
  fieldMappings:
  - identifier: Url
    columnName: DK_Url
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: KeywordName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_docker

Stage 2: where

| where Service == "docker"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Darktrace AI Analyst (Legacy)

#

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
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'This rule creates Microsoft Sentinel Incidents based on Darktrace AI Analyst Incidents, fetched every 5 minutes.'

MITRE ATT&CK Coverage

Rule Body

id: ffa2977f-3077-4bba-b1bf-f3417699cbb0
name: Darktrace AI Analyst (Legacy)
description: |
  'This rule creates Microsoft Sentinel Incidents based on Darktrace AI Analyst Incidents, fetched every 5 minutes.'
severity: High
requiredDataConnectors:
  - connectorId: DarktraceRESTConnector
    dataTypes:
      - darktrace_model_alerts_CL
queryFrequency: 5m
queryPeriod: 5m 
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - LateralMovement
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1059
  - T1021
  - T1071
query: |
  darktrace_model_alerts_CL 
  | where dtProduct_s =="AI Analyst"
  | project-rename  EventStartTime=startTime_s, EventEndTime = endTime_s, NetworkRuleName=title_s, DtCurrentGroup=externalId_g, ThreatCategory=dtProduct_s, ThreatRiskLevel=score_d, SrcHostname=hostname_s, SrcIpAddr=deviceIP_s, DtURL=url_s, DtSummary=summary_s, DtGroupScore=groupScore_d, DtGroupCategory=groupCategory_s, DtSrcDeviceName=bestDeviceName_s, DtIndentifier=identifier_s, ActivityID=activityId_s, DtGroupingID=groupingId_s, DtGroupByActivity=groupByActivity_b, DtSummaryFirstSentence=summaryFirstSentence_s, DtNewEvent=newEvent_b, DtCGLegacy=currentGroup_s, DtGroupPreviousGroups=groupPreviousGroups_s, DtTime=time_s, DtSeverity=Severity, DtLongitude=longitude_d, DtLatitude=latitude_d  
  | extend EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", DtSentinelCategory=DtGroupCategory
  | extend DtSentinelCategory = case (DtGroupCategory == "compliance", "Low", 
                                      DtGroupCategory == "suspicious", "Medium",
                                      "High") //compliance -> low, suspcious -> medium, critical -> high
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings: 
 - entityType: Host
   fieldMappings:
    - identifier: HostName
      columnName: SrcHostname
 - entityType: IP
   fieldMappings:
    - identifier: Address
      columnName: SrcIpAddr
customDetails:
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
  NetworkRuleName: NetworkRuleName
  DtCurrentGroup: DtCurrentGroup
  ThreatRiskLevel: ThreatRiskLevel
  DtSummary: DtSummary
  DtGroupScore: DtGroupScore
  DtGroupCategory: DtGroupCategory
  DtSentinelCategory: DtSentinelCategory
  DtSrcDeviceName: DtSrcDeviceName
  DtNewEvent: DtNewEvent
  DtSeverity: DtSeverity
alertDetailsOverride: 
  alertDisplayNameFormat: 'Darktrace: {{ThreatRiskLevel}} - {{NetworkRuleName}}'
  alertDescriptionFormat: '{{DtSummary}}'
  alertTacticsColumnName: # leave empty
  alertSeverityColumnName: # leave empty 
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: DtURL
    - alertProperty: ProviderName
      value: EventVendor
    - alertProperty: ProductName
      value: EventProduct
    - alertProperty: ProductComponentName
      value: ThreatCategory
    - alertProperty: Severity
      value: DtSentinelCategory
version: 1.1.1
kind: NRT               


Stages and Predicates

Stage 1: source

darktrace_model_alerts_CL

Stage 2: where

| where dtProduct_s =="AI Analyst"

Stage 3: project-rename

| project-rename  EventStartTime=startTime_s, EventEndTime = endTime_s, NetworkRuleName=title_s, DtCurrentGroup=externalId_g, ThreatCategory=dtProduct_s, ThreatRiskLevel=score_d, SrcHostname=hostname_s, SrcIpAddr=deviceIP_s, DtURL=url_s, DtSummary=summary_s, DtGroupScore=groupScore_d, DtGroupCategory=groupCategory_s, DtSrcDeviceName=bestDeviceName_s, DtIndentifier=identifier_s, ActivityID=activityId_s, DtGroupingID=groupingId_s, DtGroupByActivity=groupByActivity_b, DtSummaryFirstSentence=summaryFirstSentence_s, DtNewEvent=newEvent_b, DtCGLegacy=currentGroup_s, DtGroupPreviousGroups=groupPreviousGroups_s, DtTime=time_s, DtSeverity=Severity, DtLongitude=longitude_d, DtLatitude=latitude_d

Stage 4: extend

| extend EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", DtSentinelCategory=DtGroupCategory

Stage 5: extend

| extend DtSentinelCategory = case (DtGroupCategory == "compliance", "Low", 
                                    DtGroupCategory == "suspicious", "Medium",
                                    "High")
DtSentinelCategory =
ifDtGroupCategory == "compliance""Low"
elifDtGroupCategory == "suspicious""Medium"
else"High"

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityIDproject-rename
DtCGLegacyproject-rename
DtCurrentGroupproject-rename
DtGroupByActivityproject-rename
DtGroupCategoryproject-rename
DtGroupPreviousGroupsproject-rename
DtGroupScoreproject-rename
DtGroupingIDproject-rename
DtIndentifierproject-rename
DtLatitudeproject-rename
DtLongitudeproject-rename
DtNewEventproject-rename
DtSeverityproject-rename
DtSrcDeviceNameproject-rename
DtSummaryproject-rename
DtSummaryFirstSentenceproject-rename
DtTimeproject-rename
DtURLproject-rename
EventEndTimeproject-rename
EventStartTimeproject-rename
NetworkRuleNameproject-rename
SrcHostnameproject-rename
SrcIpAddrproject-rename
ThreatCategoryproject-rename
ThreatRiskLevelproject-rename
DtSentinelCategoryextend
EventProductextend
EventVendorextend

Detect Suspicious Commands Initiated by Webserver Processes

#
Status
available
Severity
high
Time window
1h
Group by
DeviceId, DeviceName
Source
github.com/Azure/Azure-Sentinel

This query was originally published in the threat analytics report, Operation Soft Cell. Operation Soft Cell is a series of campaigns targeting users' call logs at telecommunications providers throughout the world. These attacks date from as early as 2012. Operation Soft Cell operators sometimes use legitimate web server processes to launch commands, especially for network discovery and user/owner discovery. The following query detects activity of this kind. Reference - https://www.cybereason.com/blog/operation-soft-cell-a-worldwide-campaign-against-telecommunications-providers

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: fa2f7d8a-6726-465a-aa72-6f6e3d4c99d7
name: Detect Suspicious Commands Initiated by Webserver Processes
description: |
  This query was originally published in the threat analytics report, Operation Soft Cell.
  Operation Soft Cell is a series of campaigns targeting users' call logs at telecommunications providers throughout the world. These attacks date from as early as 2012.
  Operation Soft Cell operators sometimes use legitimate web server processes to launch commands, especially for network discovery and user/owner discovery. The following query detects activity of this kind.
  Reference - https://www.cybereason.com/blog/operation-soft-cell-a-worldwide-campaign-against-telecommunications-providers
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - DefenseEvasion
  - Discovery
relevantTechniques:
  - T1059
  - T1574
  - T1087
  - T1082
tags:
  - Operation Soft Cell
  - Webserver Process
  - Discovery
query: |
  // Suspicious commands launched by web server processes
  DeviceProcessEvents 
  | where (((InitiatingProcessParentFileName in("w3wp.exe", "beasvc.exe",
      "httpd.exe") or InitiatingProcessParentFileName startswith "tomcat")
      or InitiatingProcessFileName in("w3wp.exe", "beasvc.exe", "httpd.exe") or
      InitiatingProcessFileName startswith "tomcat"))
      and FileName in~('cmd.exe', 'powershell.exe')
  | where ProcessCommandLine contains '%temp%'
      or ProcessCommandLine has 'wget'
      or ProcessCommandLine has 'whoami'
      or ProcessCommandLine has 'certutil'
      or ProcessCommandLine has 'systeminfo'
      or ProcessCommandLine has 'ping'
      or ProcessCommandLine has 'ipconfig'
      or ProcessCommandLine has 'timeout'
  | summarize
      take_any(FileName),
      make_set(ProcessCommandLine, 100000),
      take_any(InitiatingProcessFileName),
      take_any(InitiatingProcessParentFileName)
      by DeviceId, DeviceName
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where (((InitiatingProcessParentFileName in("w3wp.exe", "beasvc.exe",
    "httpd.exe") or InitiatingProcessParentFileName startswith "tomcat")
    or InitiatingProcessFileName in("w3wp.exe", "beasvc.exe", "httpd.exe") or
    InitiatingProcessFileName startswith "tomcat"))
    and FileName in~('cmd.exe', 'powershell.exe')

Stage 3: where

| where ProcessCommandLine contains '%temp%'
    or ProcessCommandLine has 'wget'
    or ProcessCommandLine has 'whoami'
    or ProcessCommandLine has 'certutil'
    or ProcessCommandLine has 'systeminfo'
    or ProcessCommandLine has 'ping'
    or ProcessCommandLine has 'ipconfig'
    or ProcessCommandLine has 'timeout'

Stage 4: summarize

| summarize
    take_any(FileName),
    make_set(ProcessCommandLine, 100000),
    take_any(InitiatingProcessFileName),
    take_any(InitiatingProcessParentFileName)
    by DeviceId, DeviceName

Stage 5: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 6: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNamein
  • cmd.exe corpus 24 (splunk 17, elastic 6, kusto 1)
  • powershell.exe corpus 31 (splunk 19, elastic 11, kusto 1)
field:"process_basename" kind:in
InitiatingProcessFileNamein
  • beasvc.exe
  • httpd.exe
  • w3wp.exe
field:"parent_process_basename" kind:in
InitiatingProcessFileNamestarts_with
  • tomcat
field:"parent_process_basename" kind:starts_with value:"tomcat"
InitiatingProcessParentFileNamein
  • beasvc.exe
  • httpd.exe
  • w3wp.exe
field:"grandparent_process_basename" kind:in
InitiatingProcessParentFileNamestarts_with
  • tomcat
field:"grandparent_process_basename" kind:starts_with value:"tomcat"
ProcessCommandLinecontains
  • %temp% corpus 14 (sigma 13, kusto 1)
field:"CommandLine" kind:contains value:"%temp%"
ProcessCommandLinematch
  • certutil transforms: term
  • ipconfig transforms: term
  • ping transforms: term
  • systeminfo transforms: term
  • timeout transforms: term
  • wget transforms: term
  • whoami transforms: term
field:"CommandLine" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
DeviceNamesummarize
HostNameextend
DnsDomainextend

First access credential added to Application or Service Principal where no credential was present

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

This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated. If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential. Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2cfc3c6e-f424-4b88-9cc9-c89f482d016a
name: First access credential added to Application or Service Principal where no credential was present
description: |
  'This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated.
  If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential.
  Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1550.001
tags:
  - Solorigate
  - NOBELIUM
query: |
  AuditLogs
  | where OperationName has ("Certificates and secrets management")
  | where Result =~ "success"
  | where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "Application"
        | extend targetDisplayName = tostring(TargetResource.displayName),
                 targetId = tostring(TargetResource.id),
                 targetType = tostring(TargetResource.type),
                 keyEvents = TargetResource.modifiedProperties
    )
  | mv-apply Property = keyEvents on 
    (
        where Property.displayName =~ "KeyDescription"
        | extend new_value_set = parse_json(tostring(Property.newValue)),
                 old_value_set = parse_json(tostring(Property.oldValue))
    )
  | where old_value_set == "[]"
  | mv-expand new_value_set
  | parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
  | where keyUsage =~ "Verify"
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend InitiatingUserAgent = tostring(AdditionalDetail.value)
    )
  | project-away new_value_set, old_value_set, TargetResource, Property, AdditionalDetail
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent, 
  targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
  | extend Name = split(InitiatingUserPrincipalName, "@")[0], UPNSuffix = split(InitiatingUserPrincipalName, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: targetDisplayName
version: 1.1.5
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has ("Certificates and secrets management")

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "Application"
      | extend targetDisplayName = tostring(TargetResource.displayName),
               targetId = tostring(TargetResource.id),
               targetType = tostring(TargetResource.type),
               keyEvents = TargetResource.modifiedProperties
  )

Stage 6: kusto:mv-apply

| mv-apply Property = keyEvents on 
  (
      where Property.displayName =~ "KeyDescription"
      | extend new_value_set = parse_json(tostring(Property.newValue)),
               old_value_set = parse_json(tostring(Property.oldValue))
  )

Stage 7: where

| where old_value_set == "[]"

Stage 8: mv-expand

| mv-expand new_value_set

Stage 9: parse

| parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *

Stage 10: where

| where keyUsage =~ "Verify"

Stage 11: kusto:mv-apply

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

Stage 12: project-away

| project-away new_value_set, old_value_set, TargetResource, Property, AdditionalDetail

Stage 13: extend (5 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))

Stage 14: project-reorder

| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent, 
targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId

Stage 15: extend

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
Nameextend
UPNSuffixextend

GitHub - User was added to the organization

#
Status
available
Severity
medium
Time window
7d
Source
github.com/Azure/Azure-Sentinel

Detect activities when a user was added to the organization. This query runs every day and its severity is Medium.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c4
name: GitHub - User was added to the organization
description: |
  'Detect activities when a user was added to the organization. This query runs every day and its severity is Medium.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
    GitHubAuditData
    | where Action == "org.add_member"
    | project Actor, Action
    | extend Name = iif(Actor contains "@", split(Actor, "@")[0], Actor)
    | extend UPNSuffix = iif(Actor contains "@", split(Actor, "@")[1], "")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

GitHubAuditData

Stage 2: where

| where Action == "org.add_member"

Stage 3: project

| project Actor, Action

Stage 4: extend

| extend Name = iif(Actor contains "@", split(Actor, "@")[0], Actor)

Stage 5: extend

| extend UPNSuffix = iif(Actor contains "@", split(Actor, "@")[1], "")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • org.add_member
field:"Action" kind:eq value:"org.add_member"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
Actorproject
Nameextend
UPNSuffixextend

MFA Spamming followed by Successful login

#
Status
available
Severity
high
Time window
1d
Group by
City, IPAddress, Region, State, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window. Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: a8cc6d5c-4e7e-4b48-b4ac-d8a116c62a8b
name: MFA Spamming followed by Successful login
description: |
  'Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window.
  Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // Filter for sign-in logs ingested within the last day
  SigninLogs
  | where ingestion_time() > ago(1d)
  // Filter for records with AuthenticationRequirement set to multiFactorAuthentication
  | where AuthenticationRequirement == "multiFactorAuthentication"
  // Extract information from dynamic columns DeviceDetail and LocationDetails
  | extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
  // Extract specific attributes from DeviceDetail and LocationDetails
  | extend
        OS = tostring(DeviceDetail.operatingSystem),
        Browser = tostring(DeviceDetail.browser),
        State = tostring(LocationDetails.state),
        City = tostring(LocationDetails.city),
        Region = tostring(LocationDetails.countryOrRegion)
  // Expand multi-value property AuthenticationDetails into separate records
  | mv-expand todynamic(AuthenticationDetails)
  // Parse AuthResult from JSON in AuthenticationDetails and convert to string
  | extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
  // Summarize data by aggregating statistics for each user, IP, and AuthResult
  | summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region
  // Calculate AuthenticationWindow by finding time difference between start and end times
  | extend AuthenticationWindow = (EndTime - StartTime)
  // Filter for records with more than 10 failed attempts in 5-minute window and at least 1 successful attempt
  | where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1
  // Extract user's name and UPN suffix using split function
  | extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

SigninLogs

Stage 2: where

| where ingestion_time() > ago(1d)

Stage 3: where

| where AuthenticationRequirement == "multiFactorAuthentication"

Stage 4: extend

| extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)

Stage 5: extend

| extend
      OS = tostring(DeviceDetail.operatingSystem),
      Browser = tostring(DeviceDetail.browser),
      State = tostring(LocationDetails.state),
      City = tostring(LocationDetails.city),
      Region = tostring(LocationDetails.countryOrRegion)

Stage 6: mv-expand

| mv-expand todynamic(AuthenticationDetails)

Stage 7: extend

| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)

Stage 8: summarize

| summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region

Stage 9: extend

| extend AuthenticationWindow = (EndTime - StartTime)

Stage 10: where

| where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1

Stage 11: extend

| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Citysummarize
EndTimesummarize
FailedAttemptssummarize
IPAddresssummarize
InvolvedBrowsersummarize
InvolvedOSsummarize
Regionsummarize
StartTimesummarize
Statesummarize
SuccessfulAttemptssummarize
UserPrincipalNamesummarize
AuthenticationWindowextend
Nameextend
UPNSuffixextend

NordPass - User deletes items in bulk

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

This will alert you if a user deletes items in bulk, namely, more than 10 items or in the span of 10 minutes. If a mix of bulk and one-off deletions were performed, this will group all actions and report the total number of items deleted.

MITRE ATT&CK Coverage

TacticTechniques
Collection
Impact

Rule Body

id: f72f630f-c890-49fe-b747-80f4fb3b6348
name: NordPass - User deletes items in bulk
kind: Scheduled
version: 1.0.0
displayName: User deletes items in bulk
description: |
  This will alert you if a user deletes items in bulk, namely, more than 10 items or in the span of 10 minutes. 
  If a mix of bulk and one-off deletions were performed, this will group all actions and report the total number of items deleted.
severity: High
query: |
  NordPassEventLogs_CL
  | where action == "items_deleted" and event_type == "item_delete"
  | extend item_count = array_length(todynamic(metadata).items)
  | summarize total_items = sum(item_count) by user_email
  | where total_items >= 10
requiredDataConnectors:
  - connectorId: NordPass
    dataTypes:
      - NordPassEventLogs_CL
queryFrequency: 5m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - Collection
relevantTechniques:
  - T1485
  - T1074
incidentConfiguration:
  createIncident: false
customDetails:
  ItemCount: total_items
  User: user_email
entityMappings:
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: user_email

Stages and Predicates

Stage 1: source

NordPassEventLogs_CL

Stage 2: where

| where action == "items_deleted" and event_type == "item_delete"

Stage 3: extend

| extend item_count = array_length(todynamic(metadata).items)

Stage 4: summarize

| summarize total_items = sum(item_count) by user_email

Stage 5: where

| where total_items >= 10

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
total_itemssummarize
user_emailsummarize

Port scan detected (ASIM Network Session schema)

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

This rule identifies a possible port scan, in which a single source tries to access a large number of different ports is a short time frame. This may indicate that a port scanner is trying to identify open ports in order to penetrate a system. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM NetworkSession schema

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rule Body

id: 1da9853f-3dea-4ea9-b7e5-26730da3d537
name: Port scan detected  (ASIM Network Session schema)
description: |
  'This rule identifies a possible port scan, in which a single source tries to access a large number of different ports is a short time frame. This may indicate that a [port scanner](https://en.wikipedia.org/wiki/Port_scanner) is trying to identify open ports in order to penetrate a system.
  This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM NetworkSession schema'
alertDetailsOverride:
  alertDisplayNameFormat: Potential port scan from {{SrcIpAddr}}
  alertDescriptionFormat: A port scan has been performed from address {{SrcIpAddr}} over {{AttemptedPortsCount}} ports within 5 minutes. This may indicate that a [port scanner](https://en.wikipedia.org/wiki/Port_scanner) is trying to identify open ports in order to penetrate a system. 

severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWSS3
    dataTypes:
      - AWSVPCFlow
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftSysmonForLinux
    dataTypes:
      - Syslog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
  - connectorId: AzureNSG
    dataTypes:
      - AzureDiagnostics
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: Corelight
    dataTypes:
      - Corelight_CL
  - connectorId: AIVectraStream
    dataTypes:
      - VectraStream
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoMeraki
    dataTypes:
      - Syslog
      - CiscoMerakiNativePoller

queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1046
tags:
  - ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Sophos%20XG%20Firewall/Analytic%20Rules/PortScanDetected.yaml
    version: 1.0.0
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
query: |
  let PortScanThreshold = 50;
  _Im_NetworkSession
  | where ipv4_is_private(SrcIpAddr) == False
  | where SrcIpAddr !in ("127.0.0.1", "::1")
  | summarize AttemptedPortsCount=dcount(DstPortNumber), AttemptedPorts=make_set(DstPortNumber, 100), ReportedBy=make_set(strcat(EventVendor, "/", EventProduct), 20) by SrcIpAddr, bin(TimeGenerated, 5m)
  | where AttemptedPortsCount > PortScanThreshold

entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr

customDetails:
  AttemptedPortsCount: AttemptedPortsCount

version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let PortScanThreshold = 50;

Stage 1: source

_Im_NetworkSession

Stage 2: where

| where ipv4_is_private(SrcIpAddr) == False

Stage 3: where

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

Stage 4: summarize

| summarize AttemptedPortsCount=dcount(DstPortNumber), AttemptedPorts=make_set(DstPortNumber, 100), ReportedBy=make_set(strcat(EventVendor, "/", EventProduct), 20) by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 5: where

| where AttemptedPortsCount > PortScanThreshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AttemptedPortsCountgt
  • 50 transforms: number
field:"AttemptedPortsCount" kind:gt value:"50"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddrcidr_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:SrcIpAddr
SrcIpAddrin127.0.0.1, ::1excludes:SrcIpAddr field:"SrcIpAddr" value:"127.0.0.1" field:"SrcIpAddr" value:"::1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AttemptedPortssummarize
AttemptedPortsCountsummarize
ReportedBysummarize
SrcIpAddrsummarize

Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Anomaly based (ASIM DNS Solution)

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

This rule makes use of the series decompose anomaly method to detect clients with a high NXDomain response count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when new IP address DNS activity is identified as an outlier when compared to the baseline, indicating a recurring pattern. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 01191239-274e-43c9-b154-3a042692af06
name: Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Anomaly based (ASIM DNS Solution)
description: |
  'This rule makes use of the series decompose anomaly method to detect clients with a high NXDomain response count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when new IP address DNS activity is identified as an outlier when compared to the baseline, indicating a recurring pattern. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1008
query: |
  let threshold = 2.5;
  let min_t = ago(14d);
  let max_t = now();
  let timeframe = 1d;
  // calculate avg. eps(events per second)
  let eps = materialize (_Im_Dns
    | project TimeGenerated
    | where TimeGenerated > ago(5m)
    | count
    | extend Count = Count / 300);
  let maxSummarizedTime = toscalar (
    union isfuzzy=true 
        (
        DNS_Summarized_Logs_ip_CL 
        | where EventTime_t >= min_t
        | summarize max_TimeGenerated=max(EventTime_t)
        | extend max_TimeGenerated = datetime_add('hour', 1, max_TimeGenerated)
        ),
        (
        print(min_t)
        | project max_TimeGenerated = print_0
        )
    | summarize maxTimeGenerated = max(max_TimeGenerated) 
    );
  let summarizationexist = materialize(
    union isfuzzy=true 
        (
        DNS_Summarized_Logs_ip_CL
        | where EventTime_t > ago(1d) 
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend sumexist = (maxv > 1)
    );
  let allData = union isfuzzy=true
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) > 1000
        | join (summarizationexist) on sumexist)
        | join (
            _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(2d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
            | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
            )
            on exists
        | project-away exists, maxv, sum*
        ),
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) between (501 .. 1000)
        | join (summarizationexist) on sumexist)
        | join (
            _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(3d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
            | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
            )
            on exists
        | project-away exists, maxv, sum*
        ),
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) <= 500
        | join (summarizationexist) on sumexist)
        | join (
            _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(4d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
            | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
            )
            on exists
        | project-away exists, maxv, sum*
        ),
        (
        DNS_Summarized_Logs_ip_CL
        | where EventTime_t > min_t and EventResultDetails_s == 'NXDOMAIN'
        | project-rename
            SrcIpAddr=SrcIpAddr_s,
            DnsQuery=DnsQuery_s,
            Count=count__d,
            EventTime=EventTime_t
        | extend Count = toint(Count)
        );
  allData
  | make-series QueryCount=dcount(DnsQuery) on EventTime from min_t to max_t step timeframe by SrcIpAddr
  // include calculated Anomalies, Score and Baseline
  | extend (anomalies, score, baseline) = series_decompose_anomalies(QueryCount, threshold, -1, 'linefit')
  | mv-expand anomalies, score, baseline, EventTime, QueryCount
  | extend
    anomalies = toint(anomalies),
    score = toint(score),
    baseline = toint(baseline),
    EventTime = todatetime(EventTime),
    Total = tolong(QueryCount)
  | where EventTime >= ago(timeframe)
  | where score >= threshold * 2
  // Join allData to include DnsQuery details
  | join kind=inner(allData
    | where TimeGenerated >= ago(timeframe)
    | summarize DNSQueries = make_set(DnsQuery, 1000) by SrcIpAddr)
    on SrcIpAddr
  | project-away SrcIpAddr1
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  DNSQueries: DNSQueries
  AnomalyScore: score
  baseline: baseline
  Total: Total
alertDetailsOverride:
  alertDisplayNameFormat: "[Anomaly] Potential DGA (Domain Generation Algorithm) originating from client IP: '{{SrcIpAddr}}' has been detected."
  alertDescriptionFormat: "Client has been identified with high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). This client is found to be communicating with multiple Domains which do not exist.\n\nBaseline Domain or DNS query count from this client: '{{baseline}}'\n\nCurrent Domain or DNS query count from this client: '{{Total}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 2.5;
let min_t = ago(14d);
let max_t = now();
let timeframe = 1d;

Let Binding: eps

let eps = materialize (_Im_Dns
  | project TimeGenerated
  | where TimeGenerated > ago(5m)
  | count
  | extend Count = Count / 300);

Let Binding: maxSummarizedTime

let maxSummarizedTime = toscalar (
  union isfuzzy=true 
      (
      DNS_Summarized_Logs_ip_CL 
      | where EventTime_t >= min_t
      | summarize max_TimeGenerated=max(EventTime_t)
      | extend max_TimeGenerated = datetime_add('hour', 1, max_TimeGenerated)
      ),
      (
      print(min_t)
      | project max_TimeGenerated = print_0
      )
  | summarize maxTimeGenerated = max(max_TimeGenerated) 
  );

Let Binding: summarizationexist

let summarizationexist = materialize(
  union isfuzzy=true 
      (
      DNS_Summarized_Logs_ip_CL
      | where EventTime_t > ago(1d) 
      | project v = int(2)
      ),
      (
      print int(1) 
      | project v = print_0
      )
  | summarize maxv = max(v)
  | extend sumexist = (maxv > 1)
  );

Union isfuzzy=true (4 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: datatable(exists:, datatable(exists:, datatable(exists:, DNS_Summarized_Logs_ip_CL

Leg 1: datatable(exists:

(datatable(exists: int, sumexist: bool)[1, false]
      | where toscalar(eps) > 1000
      | join (summarizationexist) on sumexist)
      | join (
          _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(2d)), endtime=now())
          | where TimeGenerated > maxSummarizedTime
          | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
          | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
          )
          on exists
      | project-away exists, maxv, sum*

Leg 2: datatable(exists:

(datatable(exists: int, sumexist: bool)[1, false]
      | where toscalar(eps) between (501 .. 1000)
      | join (summarizationexist) on sumexist)
      | join (
          _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(3d)), endtime=now())
          | where TimeGenerated > maxSummarizedTime
          | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
          | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
          )
          on exists
      | project-away exists, maxv, sum*

Leg 3: datatable(exists:

(datatable(exists: int, sumexist: bool)[1, false]
      | where toscalar(eps) <= 500
      | join (summarizationexist) on sumexist)
      | join (
          _Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(4d)), endtime=now())
          | where TimeGenerated > maxSummarizedTime
          | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
          | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
          )
          on exists
      | project-away exists, maxv, sum*

Leg 4: DNS_Summarized_Logs_ip_CL

DNS_Summarized_Logs_ip_CL
      | where EventTime_t > min_t and EventResultDetails_s == 'NXDOMAIN'
      | project-rename
          SrcIpAddr=SrcIpAddr_s,
          DnsQuery=DnsQuery_s,
          Count=count__d,
          EventTime=EventTime_t
      | extend Count = toint(Count)

Applied to the Combined Result

| make-series QueryCount=dcount(DnsQuery) on EventTime from min_t to max_t step timeframe by SrcIpAddr | extend (anomalies, score, baseline) = series_decompose_anomalies(QueryCount, threshold, -1, 'linefit') | mv-expand anomalies, score, baseline, EventTime, QueryCount | extend
  anomalies = toint(anomalies),
  score = toint(score),
  baseline = toint(baseline),
  EventTime = todatetime(EventTime),
  Total = tolong(QueryCount) | where EventTime >= ago(timeframe) | where score >= threshold * 2 | join kind=inner(allData
  | where TimeGenerated >= ago(timeframe)
  | summarize DNSQueries = make_set(DnsQuery, 1000) by SrcIpAddr)
  on SrcIpAddr | project-away SrcIpAddr1

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
QueryCountsummarize
SrcIpAddrsummarize
anomaliesextend
baselineextend
scoreextend
EventTimeextend
Totalextend

Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Static threshold based (ASIM DNS Solution)

#
Status
available
Severity
medium
Time window
10d
Group by
DNSQueryCount, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

This rule identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 89ba52fa-96a7-4653-829a-ca49bb13336c
name: Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Static threshold based (ASIM DNS Solution)
description: |
  'This rule identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1008
query: |
  let threshold = 100;
  let lookback = 10d;
  let referenceendtime = 1d;
  let nxDomainDnsEvents = (stime: datetime, etime: datetime) {
    _Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
    | where DnsQueryTypeName in ("A", "AAAA")
    | where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
    | where DnsQuery !contains "/" and DnsQuery contains "."
  };
  nxDomainDnsEvents (stime=ago(referenceendtime), etime=now())
  | summarize
    StartTimeUtc = min(TimeGenerated),
    EndTimeUtc = max(TimeGenerated),
    DNSQueryCount=dcount(DnsQuery)
    by SrcIpAddr
  | where DNSQueryCount > threshold
  // Filter out previously seen IPs
  | join kind=leftanti (nxDomainDnsEvents (stime=ago(lookback), etime=ago(referenceendtime))
    | summarize DNSQueryCount=dcount(DnsQuery) by SrcIpAddr, bin(TimeGenerated,1d)
    | where DNSQueryCount > threshold)
    on SrcIpAddr
  // Pull out sample NXDomain responses for those remaining potentially infected IPs
  | join kind = inner (nxDomainDnsEvents (stime=ago(lookback), etime=now())
    | summarize by DnsQuery, SrcIpAddr)
    on SrcIpAddr
  | summarize
    StartTimeUtc = min(StartTimeUtc),
    EndTimeUtc = max(EndTimeUtc),
    DNSQueries=make_list(DnsQuery, 100)
    by SrcIpAddr, DNSQueryCount
  | extend DNSQueryThreshold=threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  DNSQueries: DNSQueries
  DNSQueryThreshold: DNSQueryThreshold
  DNSQueryCount: DNSQueryCount
alertDetailsOverride:
  alertDisplayNameFormat: "[Static threshold] Potential DGA (Domain Generation Algorithm) originating from client IP: '{{SrcIpAddr}}' has been detected."
  alertDescriptionFormat: "Client has been identified with high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). This client is found to be communicating with multiple Domains which do not exist.\n\nDGA DNS query count baseline is: '{{DNSQueryThreshold}}'\n\nCurrent failed DNS query count from this client: '{{DNSQueryCount}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;
let lookback = 10d;
let referenceendtime = 1d;

let nxDomainDnsEvents is inlined into the numbered stages below.

Stages 1 to 4 define let nxDomainDnsEvents as the rule's main pipeline source. Stages 5 to 10 use it.

Stage 1: source

_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)

Stage 2: where

| where DnsQueryTypeName in ("A", "AAAA")

Stage 3: where

| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False

Stage 4: where

| where DnsQuery !contains "/" and DnsQuery contains "."

Stage 5: summarize

nxDomainDnsEvents
| summarize
  StartTimeUtc = min(TimeGenerated),
  EndTimeUtc = max(TimeGenerated),
  DNSQueryCount=dcount(DnsQuery)
  by SrcIpAddr

Stage 6: where

| where DNSQueryCount > threshold

Stage 7: join (negated)

join kind=leftanti (nxDomainDnsEvents) on SrcIpAddr

Stage 8: join

| join kind = inner (nxDomainDnsEvents (stime=ago(lookback), etime=now())
  | summarize by DnsQuery, SrcIpAddr)
  on SrcIpAddr

Stage 9: summarize

summarize DNSQueries, EndTimeUtc, StartTimeUtc by SrcIpAddr, DNSQueryCount

Stage 10: extend

extend DNSQueryThreshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DNSQueryCountgt
  • 100 transforms: number
field:"DNSQueryCount" kind:gt value:"100"
DnsQuerycontains
  • . corpus 6 (elastic 3, kusto 2, splunk 1)
field:"QueryName" kind:contains value:"."
DnsQueryTypeNamein
  • A corpus 2 (kusto 2)
  • AAAA corpus 2 (kusto 2)
field:"DnsQueryTypeName" kind:in

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DNSQueriessummarize
DNSQueryCountsummarize
EndTimeUtcsummarize
SrcIpAddrsummarize
StartTimeUtcsummarize
DNSQueryThresholdextend

Ransom Protect User Blocked

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

Detects malicious users blocked by CTERA Ransom Protect AI engine.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: d5d4766b-e547-44da-9d85-48ff393db201
name: Ransom Protect User Blocked
description: 'Detects malicious users blocked by CTERA Ransom Protect AI engine.'
status: Available
severity: High
kind: NRT
requiredDataConnectors:
  - connectorId: CTERA
    dataTypes:
      - Syslog
tactics:
    - Impact
relevantTechniques:
    - T1486
query: |
    Syslog
    | where SyslogMessage contains "Ransom Protect mechanism blocked"
    | extend 
        Portal = extract("portal:(\\w+)", 1, SyslogMessage),
        EdgeFiler = extract("Edge Filer:(\\w+-\\d+)", 1, SyslogMessage),
        IP = extract("IP:([0-9.]+)", 1, SyslogMessage),
        User = extract("user:(\\w+)", 1, SyslogMessage),
        BlockedTime = extract("at ([^ ]+)", 1, SyslogMessage)
    | project TimeGenerated, Portal, EdgeFiler, IP, User, BlockedTime
suppressionDuration: PT5H
suppressionEnabled: false
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: false
        reopenClosedIncident: false
        lookbackDuration: PT5H
        matchingMethod: AllEntities
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride: 
    alertnameFormat: 'CTERA Ransom Protect User Blocked'
    alertDescriptionFormat: CTERA Ransom Protect blocked a malicious user at {{TimeGenerated}}.
customDetails:
    EdgeFiler: EdgeFiler
entityMappings:
- entityType: Account
  fieldMappings:
      - identifier: FullName
        columnName: User
- entityType: IP
  fieldMappings:
      - identifier: Address
        columnName: IP
version: 1.0.2

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where SyslogMessage contains "Ransom Protect mechanism blocked"

Stage 3: extend

| extend 
    Portal = extract("portal:(\\w+)", 1, SyslogMessage),
    EdgeFiler = extract("Edge Filer:(\\w+-\\d+)", 1, SyslogMessage),
    IP = extract("IP:([0-9.]+)", 1, SyslogMessage),
    User = extract("user:(\\w+)", 1, SyslogMessage),
    BlockedTime = extract("at ([^ ]+)", 1, SyslogMessage)

Stage 4: project

| project TimeGenerated, Portal, EdgeFiler, IP, User, BlockedTime

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagecontains
  • Ransom Protect mechanism blocked
field:"SyslogMessage" kind:contains value:"Ransom Protect mechanism blocked"

Output Fields

These fields are emitted when the rule matches.

FieldSource
BlockedTimeproject
EdgeFilerproject
IPproject
Portalproject
TimeGeneratedproject
Userproject

Tape Media Pool Deleted

#
Status
available
Severity
informational
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Detects when a tape media pool is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: ceb625a3-a8d7-4a42-9ea1-40f01c337ecd
name: Tape Media Pool Deleted
description: Detects when a tape media pool is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 23630\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 == 23630

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

Veeam ONE Job Disabled

#

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
3h
Source
github.com/Azure/Azure-Sentinel

Detects when a Veeam ONE job is disabled.

Rule Body

id: 1a9e31d6-2116-4ff1-8ee7-abc957c34350
name: Veeam ONE Job Disabled
description: Detects when a Veeam ONE job is disabled.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
  dataTypes:
  - VeeamOneTriggeredAlarms_CL
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: VeeamOneTriggeredAlarms_CL | where PredefinedAlarmId == 316
version: 1.0.1
kind: Scheduled
customDetails:
  PredefinedAlarmId: PredefinedAlarmId
  TriggeredAlarmId: TriggeredAlarmId
  Description: Description
  Name: Name
  VoneHostName: VoneHostName
  TriggeredTime: TriggeredTime
  Status: Status
  Comment: Comment
  ObjectId: ObjectId
  ObjectName: ObjectName
  ObjectType: ObjectType

Stages and Predicates

Stage 1: source

VeeamOneTriggeredAlarms_CL

Stage 2: where

| where PredefinedAlarmId == 316

Indicators

These rows show field, operator, and value matches.

VMware ESXi - Low patch disk space

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

'This rule is triggered when low patch disk store space is detected.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 48d992ba-d404-4159-a8c6-46f51d1325c7
name: VMware ESXi - Low patch disk space
description: |
  'This rule is triggered when low patch disk store space is detected.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1529
query: |
  let threshold = 100;
  VMwareESXi
  | where SyslogMessage has ('Patch store disk')
  | extend sp = toreal(extract(@'free space is:\s(\d+)', 1, SyslogMessage)) / 1000000000
  | where sp < threshold
  | extend h = 'Hypervisor'
  | extend HostCustomEntity = h
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

VMwareESXi

Stage 2: where

| where SyslogMessage has ('Patch store disk')

Stage 3: extend

| extend sp = toreal(extract(@'free space is:\s(\d+)', 1, SyslogMessage)) / 1000000000

Stage 4: where

| where sp < threshold

Stage 5: extend

| extend h = 'Hypervisor'

Stage 6: extend

| extend HostCustomEntity = h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • Patch store disk transforms: term
field:"SyslogMessage" kind:match value:"Patch store disk"
splt
  • 100 transforms: number
field:"sp" kind:lt value:"100"

Output Fields

These fields are emitted when the rule matches.

FieldSource
spextend
hextend
HostCustomEntityextend

Back to all Kusto rules