Detection rules › Kusto

Kusto rules: 1y

Abnormal Security - Account Takeover case opened

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

'Creates an incident when Abnormal Security opens an Account Takeover (ATO) case, indicating a mailbox or user account is suspected to be compromised. The ATO case severity, status, and observed indicators are surfaced for triage.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Rule Body

id: da243bf4-382b-46b9-9b4d-ce6ffe9e7beb
name: Abnormal Security - Account Takeover case opened
description: |
  'Creates an incident when Abnormal Security opens an Account Takeover (ATO) case,
  indicating a mailbox or user account is suspected to be compromised. The ATO case
  severity, status, and observed indicators are surfaced for triage.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AbnormalSecurityPush
    dataTypes:
      - ABNORMAL_SECURITY_ATO_CASE_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110
query: |
  ABNORMAL_SECURITY_ATO_CASE_CL
  | where isnotempty(abx_body_abx_body_ato_case_id_s)
  // Collapse to one row per ATO case (earliest event in the window) so repeated
  // events for the same case do not raise duplicate alerts.
  | summarize arg_min(TimeGenerated, *) by CaseId = abx_body_abx_body_ato_case_id_s
  | extend
      CompromisedAccount = abx_body_abx_body_compromised_account_s,
      CaseSeverity = abx_body_abx_body_severity_s,
      CaseStatus = abx_body_abx_body_status_s,
      Indicators = abx_body_abx_body_indicators_s,
      FirstDetected = abx_body_abx_body_first_detected_t
  | project TimeGenerated, CompromisedAccount, CaseId, CaseSeverity, CaseStatus, Indicators, FirstDetected
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CompromisedAccount
customDetails:
  AtoCaseId: CaseId
  CaseSeverity: CaseSeverity
  CaseStatus: CaseStatus
alertDetailsOverride:
  alertDisplayNameFormat: "Abnormal Security: Account Takeover case for {{CompromisedAccount}}"
  alertDescriptionFormat: "Abnormal Security opened ATO case {{CaseId}} (severity {{CaseSeverity}}) for the compromised account {{CompromisedAccount}}."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

ABNORMAL_SECURITY_ATO_CASE_CL

Stage 2: where

| where isnotempty(abx_body_abx_body_ato_case_id_s)

Stage 3: summarize

| summarize arg_min(TimeGenerated, *) by CaseId = abx_body_abx_body_ato_case_id_s

Stage 4: extend

| extend
    CompromisedAccount = abx_body_abx_body_compromised_account_s,
    CaseSeverity = abx_body_abx_body_severity_s,
    CaseStatus = abx_body_abx_body_status_s,
    Indicators = abx_body_abx_body_indicators_s,
    FirstDetected = abx_body_abx_body_first_detected_t

Stage 5: project

| project TimeGenerated, CompromisedAccount, CaseId, CaseSeverity, CaseStatus, Indicators, FirstDetected

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CaseIdproject
CaseSeverityproject
CaseStatusproject
CompromisedAccountproject
FirstDetectedproject
Indicatorsproject
TimeGeneratedproject

Azure DevOps Personal Access Token (PAT) misuse

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

'This Alert detects whenever a PAT is used in ways that PATs are not normally used. May require an allow list and baselining. Reference - https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page Use this query for baselining: ADOAuditLogs | distinct OperationName'

MITRE ATT&CK Coverage

Rule Body

id: ac891683-53c3-4f86-86b4-c361708e2b2b
name: Azure DevOps Personal Access Token (PAT) misuse
description: |
  'This Alert detects whenever a PAT is used in ways that PATs are not normally used. May require an allow list and baselining.
  Reference - https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page
  Use this query for baselining:
  ADOAuditLogs
  | distinct OperationName'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Impact
relevantTechniques:
  - T1496
  - T1559
query: |
  // Allowlisted UPNs should likely stay empty
  let AllowlistedUpns = datatable(UPN:string)['foo@bar.com', 'test@foo.com'];
  // Operation Name parts that will alert
  let HasAnyBlocklist = datatable(OperationNamePart:string)['Security.','Project.','AuditLog.','Extension.'];
  // Distinct Operation Names that will flag
  let HasExactBlocklist = datatable(OperationName:string)['Group.UpdateGroupMembership.Add','Library.ServiceConnectionExecuted','Pipelines.PipelineModified',
  'Release.ReleasePipelineModified', 'Git.RefUpdatePoliciesBypassed'];
  ADOAuditLogs
  | where AuthenticationMechanism startswith "PAT" and (OperationName has_any (HasAnyBlocklist) or OperationName in (HasExactBlocklist))
    and ActorUPN !in (AllowlistedUpns)
  | project TimeGenerated, AuthenticationMechanism, ProjectName, ActorUPN, ActorDisplayName, IpAddress, UserAgent, OperationName, Details, Data
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Let Binding: AllowlistedUpns used in Stage 2

let AllowlistedUpns = datatable(UPN:string)['foo@bar.com', 'test@foo.com'];

Let Binding: HasAnyBlocklist used in Stage 2

let HasAnyBlocklist = datatable(OperationNamePart:string)['Security.','Project.','AuditLog.','Extension.'];

Let Binding: HasExactBlocklist used in Stage 2

let HasExactBlocklist = datatable(OperationName:string)['Group.UpdateGroupMembership.Add','Library.ServiceConnectionExecuted','Pipelines.PipelineModified',
'Release.ReleasePipelineModified', 'Git.RefUpdatePoliciesBypassed'];

Stage 1: source

ADOAuditLogs

Stage 2: where

| where AuthenticationMechanism startswith "PAT" and (OperationName has_any (HasAnyBlocklist) or OperationName in (HasExactBlocklist))
  and ActorUPN !in (AllowlistedUpns)

Stage 3: project

| project TimeGenerated, AuthenticationMechanism, ProjectName, ActorUPN, ActorDisplayName, IpAddress, UserAgent, OperationName, Details, Data

Stage 4: extend

| extend timestamp = TimeGenerated

Stage 5: extend

| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ActorUPNeqAllowlistedUpnsexcludes:ActorUPN field:"ActorUPN" value:"AllowlistedUpns"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorDisplayNameproject
ActorUPNproject
AuthenticationMechanismproject
Dataproject
Detailsproject
IpAddressproject
OperationNameproject
ProjectNameproject
TimeGeneratedproject
UserAgentproject
timestampextend
AccountNameextend
AccountUPNSuffixextend

BitSight - drop in the headline rating

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

'Rule helps to detect if headline ratings is drop in BitSight.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance
Command & Control

Rule Body

id: b11fdc35-6368-4cc0-8128-52cd2e2cdda0
name: BitSight - drop in the headline rating
description: |
  'Rule helps to detect if headline ratings is drop in BitSight.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: BitSight
    dataTypes:
      - BitSightGraphData
queryFrequency: 1d
queryPeriod: 24h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
  - Reconnaissance
  - CommandAndControl
relevantTechniques:
  - T1591
  - T1090
query: |
    let timeframe = 24h;
    BitSightGraphData
    | where ingestion_time() > ago(timeframe)
    | where toint(RatingDifferance) < 0
    | project RatingDate, Rating, CompanyName, RatingDifferance
incidentConfiguration:
    createIncident: false
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: 'BitSight : Alert for drop in the headline rating of {{CompanyName}}.'
    alertDescriptionFormat: 'Alert is generated for {{CompanyName}}.\n\nRating Date: {{RatingDate}}\nRating Drop: {{RatingDifferance}}'
customDetails:
  CompanyName: CompanyName
  CompanyRating: Rating
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 24h;

Stage 1: source

BitSightGraphData

Stage 2: where

| where ingestion_time() > ago(timeframe)

Stage 3: where

| where toint(RatingDifferance) < 0

Stage 4: project

| project RatingDate, Rating, CompanyName, RatingDifferance

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RatingDifferancelt
  • 0 transforms: toint
field:"RatingDifferance" kind:lt value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompanyNameproject
Ratingproject
RatingDateproject
RatingDifferanceproject

BloodHound Attack Path Finding - Key Vault Contributor Role on Tier Zero Resource

#

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 - Key Vault Contributor Role on Tier Zero Resource

Rule Body

id: 7b7347d2-3efb-47d9-a329-5b7ae4357d70
name: BloodHound Attack Path Finding - Key Vault Contributor Role on Tier Zero Resource
description: BloodHound Attack Path Finding - Key Vault Contributor Role on Tier Zero
  Resource
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 == "AzureT0KeyVaultContributor"
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 == "AzureT0KeyVaultContributor"

Indicators

These rows show field, operator, and value matches.

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

Commvault Cloud Alert

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

'This query identifies Alerts from Commvault Cloud.'

MITRE ATT&CK Coverage

Rule Body

id: 317e757e-c320-448e-8837-fc61a70fe609
name: Commvault Cloud Alert
description: |
  'This query identifies Alerts from Commvault Cloud.'
severity: Medium
status: Available
enabled: true
requiredDataConnectors:
  - connectorId: CommvaultSecurityIQConnector
    datatypes:
      - CommvaultAlertsCCF_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Impact
customDetails:
  Client: ClientName
  AnomalyType: AnomalyType
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Commvault Cloud Anomaly detected for client: {{ClientName}}'
  alertDescriptionFormat: 'Anomaly detected for Commvault client {{ClientName}}. AnomalyType flag: {{AnomalyType}}.'
  alertDynamicProperties: []
relevantTechniques:
  - T1578
  - T1531
tags:
  - Commvault
  - Metallic
  - Threat Intelligence
  - Ransomware
query: |
    CommvaultAlertsCCF_CL
    | where TimeGenerated > ago(5m)
    | where isnotnull(AnomalyType) and AnomalyType > 0
    | take 1000
entityMappings: null
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

CommvaultAlertsCCF_CL

Stage 2: where

| where TimeGenerated > ago(5m)

Stage 3: where

| where isnotnull(AnomalyType) and AnomalyType > 0

Stage 4: take

| take 1000

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AnomalyTypegt
  • 0 transforms: number
field:"AnomalyType" kind:gt value:"0"
AnomalyTypeis_not_null
  • (no value, null check)
field:"AnomalyType" kind:is_not_null

Conditional Access Policy Modified by New User

#
Severity
medium
Time window
14d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects a Conditional Access Policy being modified by a user who has not modified a policy in the last 14 days. A threat actor may try to modify policies to weaken the security controls in place. Investigate any change to ensure they are approved. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-infrastructure#conditional-access

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 25a7f951-54b7-4cf5-9862-ebc04306c590
name: Conditional Access Policy Modified by New User
description: |
  'Detects a Conditional Access Policy being modified by a user who has not modified a policy in the last 14 days.
    A threat actor may try to modify policies to weaken the security controls in place.
    Investigate any change to ensure they are approved.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-infrastructure#conditional-access'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let known_users = (AuditLogs
    | where TimeGenerated between(ago(14d)..ago(1d))
    | where OperationName has "conditional access policy"
    | where Result =~ "success"
    | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
    | summarize by InitiatingUserPrincipalName);
    AuditLogs
    | where TimeGenerated > ago(1d)
    | where OperationName has "conditional access policy"
    | where Result =~ "success"
    | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
    | extend InitiatingAppId = tostring(InitiatedBy.app.appId)
    | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
    | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
    | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
    | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
    | extend CAPolicyName = tostring(TargetResources[0].displayName)
    | where InitiatingUserPrincipalName !in (known_users)
    | extend NewPolicyValues = TargetResources[0].modifiedProperties[0].newValue
    | extend OldPolicyValues = TargetResources[0].modifiedProperties[0].oldValue
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
    | project-reorder TimeGenerated, OperationName, CAPolicyName, InitiatingAppId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, NewPolicyValues, OldPolicyValues
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: InitiatingAppId
      - identifier: Name
        columnName: InitiatingAppName
version: 1.1.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Let Binding: known_users used in Stage 6

let known_users = (AuditLogs
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where OperationName has "conditional access policy"
  | where Result =~ "success"
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | summarize by InitiatingUserPrincipalName);

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where OperationName has "conditional access policy"

Stage 4: where

| where Result =~ "success"

Stage 5: extend (7 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppId = tostring(InitiatedBy.app.appId)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CAPolicyName = tostring(TargetResources[0].displayName)

Stage 6: where

| where InitiatingUserPrincipalName !in (known_users)

Stage 7: extend (3 consecutive steps)

| extend NewPolicyValues = TargetResources[0].modifiedProperties[0].newValue
| extend OldPolicyValues = TargetResources[0].modifiedProperties[0].oldValue
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Stage 8: project-reorder

| project-reorder TimeGenerated, OperationName, CAPolicyName, InitiatingAppId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, NewPolicyValues, OldPolicyValues

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNamematch
  • conditional access policy transforms: term
field:"OperationName" kind:match value:"conditional access policy"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InitiatingUserPrincipalNameeqknown_usersexcludes:InitiatingUserPrincipalName field:"InitiatingUserPrincipalName" value:"known_users"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppIdextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
CAPolicyNameextend
NewPolicyValuesextend
OldPolicyValuesextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

Credential errors stateful anomaly on database

#
Status
available
Severity
medium
Time window
14d
Group by
ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, timeSlice
Source
github.com/Azure/Azure-Sentinel

'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When Brute Force attacks are attempted, majority of logins will use wrong credentials, thus will fail with error code 18456. Thus, if we see a large number of logins with such error codes, this could indicate Brute Force attack.'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: daa32afa-b5b6-427d-93e9-e32f3f359dd7
name: Credential errors stateful anomaly on database
description: |
  'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When Brute Force attacks are attempted, majority of logins will use wrong credentials, thus will fail with error code 18456. Thus, if we see a large number of logins with such error codes, this could indicate Brute Force attack.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1190
  - T1110.001
  - T1110.002
tags:
  - SQL
query: |
    let monitoredStatementsThreshold = 1;           // Minimal number of monitored statements in the slice to trigger an anomaly.
    let trainingSlicesThreshold = 5;                // The maximal amount of slices with monitored statements in the training window before anomaly detection is throttled.
    let timeSliceSize = 1h;                         // The size of the single timeSlice for individual aggregation.
    let detectionWindow = 1h;                       // The size of the recent detection window for detecting anomalies.  
    let trainingWindow = detectionWindow + 14d;     // The size of the training window before the detection window for learning the normal state.
    let monitoredErrors = pack_array(18456);        // List of sql error codes relevant for this detection.
    let processedData = materialize (
        AzureDiagnostics
        | where TimeGenerated >= ago(trainingWindow)
        | where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
        | project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
                  ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
                  IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
                  ResponseRows = response_rows_d, Statement = statement_s,
                  Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
                        , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
                        , 0),
                  State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
                        , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
                        , 0),
                  AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
        | summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
            , anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
            by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
        | extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
                                               (ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
        | where WindowType in ('detection', 'training'));
    let trainingSet =
        processedData
        | where WindowType == 'training'
        | summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
            by Database;
    processedData
    | where WindowType == 'detection' 
    | join kind = inner (trainingSet) on Database
    | extend IsErrorAnomalyOnStatement = iff(((countStatementsWithError >= monitoredStatementsThreshold) and (countSlicesWithErrors <= trainingSlicesThreshold)), true, false)
        , anomalyScore = round(countStatementsWithError/monitoredStatementsThreshold, 0)
    | where IsErrorAnomalyOnStatement == 'true'
    | project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithError, anyMonitoredStatement, anyInfo, anomalyScore
    | extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ApplicationName
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
alertDetailsOverride:
  alertDisplayNameFormat: 'Credential errors stateful anomaly on database {{Database}}'
  alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithError}} statements with monitored errors in the last hour. Investigate the database activity for potential credential brute force attacks.'
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let monitoredStatementsThreshold = 1;
let trainingSlicesThreshold = 5;
let timeSliceSize = 1h;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredErrors = pack_array(18456);

let processedData is inlined into the numbered stages below.

Let Binding: trainingSet used in Stage 9

let trainingSet = processedData
    | where WindowType == 'training'
    | summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
        by Database;

Stages 1 to 7 define let processedData as the rule's main pipeline source. Stages 8 to 13 use it.

Stage 1: source

AzureDiagnostics

Stage 2: where

| where TimeGenerated >= ago(trainingWindow)

Stage 3: where

| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM")

Stage 4: project

| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
              ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
              IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
              ResponseRows = response_rows_d, Statement = statement_s,
              Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
                    , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
                    , 0),
              State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
                    , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
                    , 0),
              AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)

Stage 5: summarize

| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
        , anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
        by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId

Stage 6: extend

| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
                                           (ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
WindowType =
if/* macro: (timeSlice >= ago(detectionWindow)) */'detection'
elif/* macro: (ago(trainingWindow) <= timeSlice) */'training'
else'other'

Stage 7: where

| where WindowType in ('detection', 'training')

Stage 8: where

processedData
| where WindowType == 'detection'

Stage 9: join

| join kind = inner (trainingSet) on Database

Stage 10: extend

extend IsErrorAnomalyOnStatement, anomalyScore
IsErrorAnomalyOnStatement =
ifcountStatementsWithError >= monitoredStatementsThreshold and countSlicesWithErrors <= trainingSlicesThresholdtrue
elsefalse

Stage 11: where

where IsErrorAnomalyOnStatement == true

Stage 12: project

project ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, TimeGenerated, anomalyScore, anyInfo, anyMonitoredStatement, countEvents, countStatements, countStatementsWithError

Stage 13: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationNameproject
ClientIpproject
Databaseproject
HostNameproject
PrincipalNameproject
ResourceIdproject
TimeGeneratedproject
anomalyScoreproject
anyInfoproject
anyMonitoredStatementproject
countEventsproject
countStatementsproject
countStatementsWithErrorproject
Nameextend
UPNSuffixextend

Critical Threat Detected

#

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

Status
available
Severity
medium
Time window
1h
Group by
Device_Name, External_IP, Internal_IP, Threat_Name, Threat_Score, eventTime
Source
github.com/Azure/Azure-Sentinel

'This creates an incident in the event a critical threat was identified on a Carbon Black managed endpoint.'

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Rule Body

id: 2ca4e7fc-c61a-49e5-9736-5da8035c47e0
name: Critical Threat Detected
description: |
  'This creates an incident in the event a critical threat was identified on a Carbon Black managed endpoint.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: VMwareCarbonBlack
    dataTypes:
      - CarbonBlackNotifications_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
query: |
  let threshold = 8;
  CarbonBlackNotifications_CL
  | where threatHunterInfo_score_d >= threshold
  | extend eventTime = datetime(1970-01-01) + tolong(threatHunterInfo_time_d/1000) * 1sec
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by eventTime, Threat_Name = threatHunterInfo_reportName_s, Device_Name = deviceInfo_deviceName_s,  Internal_IP = deviceInfo_internalIpAddress_s, External_IP = deviceInfo_externalIpAddress_s, Threat_Score = threatHunterInfo_score_d
  | project-away count_

entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Device_Name
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Internal_IP
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 8;

Stage 1: source

CarbonBlackNotifications_CL

Stage 2: where

| where threatHunterInfo_score_d >= threshold

Stage 3: extend

| extend eventTime = datetime(1970-01-01) + tolong(threatHunterInfo_time_d/1000) * 1sec

Stage 4: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by eventTime, Threat_Name = threatHunterInfo_reportName_s, Device_Name = deviceInfo_deviceName_s,  Internal_IP = deviceInfo_internalIpAddress_s, External_IP = deviceInfo_externalIpAddress_s, Threat_Score = threatHunterInfo_score_d

Stage 5: project-away

| project-away count_

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Device_Namesummarize
EndTimesummarize
External_IPsummarize
Internal_IPsummarize
StartTimesummarize
Threat_Namesummarize
Threat_Scoresummarize
eventTimesummarize

CYFIRMA - Brand Intelligence - Domain Impersonation High Rule

#

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

"This analytics rule detects high-risk domain impersonation activity, where newly registered or existing domains closely resemble the legitimate brand name or organizational assets. These suspicious domains may use typosquatting, homoglyphs, or brand keywords to mislead users, steal credentials, or host phishing/malicious content. The domains are identified through CYFIRMA's external threat intelligence feeds and flagged due to potential misuse in impersonation, fraud, or social engineering attacks targeting employees, customers, or partners. Early detection of these domains enables proactive mitigation measures such as domain takedown, DNS blocking, and awareness campaigns."

MITRE ATT&CK Coverage

Rule Body

id: 10bdf525-5b89-4a25-933a-e63e73b915f1
name: CYFIRMA - Brand Intelligence - Domain Impersonation High Rule
description: |
  "This analytics rule detects high-risk domain impersonation activity, where newly registered or existing domains closely resemble the legitimate brand name or organizational assets. 
  These suspicious domains may use typosquatting, homoglyphs, or brand keywords to mislead users, steal credentials, or host phishing/malicious content. 
  The domains are identified through CYFIRMA's external threat intelligence feeds and flagged due to potential misuse in impersonation, fraud, or social engineering attacks targeting employees, customers, or partners. 
  Early detection of these domains enables proactive mitigation measures such as domain takedown, DNS blocking, and awareness campaigns."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaBrandIntelligenceAlertsDC
    dataTypes:
      - CyfirmaBIDomainITAssetAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - ResourceDevelopment
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1583.001
  - T1586.002
  - T1566.002
  - T1566.001
  - T1071.003
  - T1071.001
query: |
  // High severity-  Brand Intelligence - Domain Impersonation
  let timeFrame = 5m;
  CyfirmaBIDomainITAssetAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      Domain=domain,
      DRDomain=dr_domain,
      DRSubDomain=dr_sub_domain,
      DomainSquat=signature,
      HostProvider=host_provider,
      RegisteredDate=registered_date,
      CreatedDate=created_date,
      ThreatActor=suspected_threat_actor,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      Domain,
      DRDomain,
      DRSubDomain,
      DomainSquat,
      HostProvider,
      RegisteredDate,
      CreatedDate,
      ThreatActor,
      ProductName,
      ProviderName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainSquat
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  DomainSquat: DomainSquat
  Domain: Domain
  Description: Description
  DRDomain: DRDomain
  DRSubDomain: DRSubDomain
  AlertUID: AlertUID
  UID: UID
  ThreatActor: ThreatActor
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  CreatedDate: CreatedDate
  RegisteredDate: RegisteredDate
  HostProvider: HostProvider
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Alert - Malicious Domain Impersonation of Corporate Brand - Domain Squat: {{DomainSquat}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaBIDomainITAssetAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
    Description=description,
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    AlertUID=alert_uid,
    UID=uid,
    Domain=domain,
    DRDomain=dr_domain,
    DRSubDomain=dr_sub_domain,
    DomainSquat=signature,
    HostProvider=host_provider,
    RegisteredDate=registered_date,
    CreatedDate=created_date,
    ThreatActor=suspected_threat_actor,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Description,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    Domain,
    DRDomain,
    DRSubDomain,
    DomainSquat,
    HostProvider,
    RegisteredDate,
    CreatedDate,
    ThreatActor,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
CreatedDateproject
DRDomainproject
DRSubDomainproject
Descriptionproject
Domainproject
DomainSquatproject
FirstSeenproject
HostProviderproject
LastSeenproject
ProductNameproject
ProviderNameproject
RegisteredDateproject
RiskScoreproject
ThreatActorproject
TimeGeneratedproject
UIDproject

CYFIRMA - Data Breach and Web Monitoring - Phishing Campaign Detection Rule

#

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

"Detects phishing campaigns targeting enterprise domains, as identified through CYFIRMA's Data Breach and Dark Web Monitoring. These alerts may include malicious URLs used for credential harvesting, domain impersonation, or social engineering. Immediate triage and takedown actions are recommended."

MITRE ATT&CK Coverage

Rule Body

id: 17cce4fc-9b4c-4eef-a4c7-083b44545e6e
name: CYFIRMA - Data Breach and Web Monitoring - Phishing Campaign Detection Rule
description: |
  "Detects phishing campaigns targeting enterprise domains, as identified through CYFIRMA's Data Breach and Dark Web Monitoring. 
  These alerts may include malicious URLs used for credential harvesting, domain impersonation, or social engineering. 
  Immediate triage and takedown actions are recommended."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaDigitalRiskAlertsConnector
    dataTypes:
      - CyfirmaDBWMPhishingAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1566.001
  - T1566.002
  - T1566.003
query: |
  // High severity - Data Breach and Web Monitoring - Phishing Campaign Detection
  let timeFrame = 5m;
  CyfirmaDBWMPhishingAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=signature,
      Source=source,
      Impact='',
      Recommendation='',
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT',
      AlertTitle=Alert_title
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Source,
      Impact,
      Recommendation,
      ProductName,
      ProviderName,
      AlertTitle
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  RiskScore: RiskScore
  Source: Source
  Impact: Impact
  Description: Description
  Recommendation: Recommendation
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  TimeGenerated: TimeGenerated
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Alert:  Phishing Campaign Detection - {{AlertTitle}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaDBWMPhishingAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
    Description=description,
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    AlertUID=alert_uid,
    UID=uid,
    AssetType=asset_type,
    AssetValue=signature,
    Source=source,
    Impact='',
    Recommendation='',
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT',
    AlertTitle=Alert_title

Stage 4: project

| project
    TimeGenerated,
    Description,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    AssetType,
    AssetValue,
    Source,
    Impact,
    Recommendation,
    ProductName,
    ProviderName,
    AlertTitle

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertTitleproject
AlertUIDproject
AssetTypeproject
AssetValueproject
Descriptionproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
Recommendationproject
RiskScoreproject
Sourceproject
TimeGeneratedproject
UIDproject

Filewall - Blocked emails

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

Identifies emails blocked by Filewall for Microsoft 365 (Exchange).

MITRE ATT&CK Coverage

Rule Body

id: 9b784b65-2d16-4c9f-9f59-2a5d4c659f42
name: Filewall - Blocked emails
description: Identifies emails blocked by Filewall for Microsoft 365 (Exchange).
severity: High
status: Available
kind: Scheduled
query: | 
  FilewallM365ExchangeEvent()
  | where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'
  | where EventType == 'ThreatInfo' and EventResult == 'Blocked'
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
relevantTechniques:
- T1048
tactics:
- Exfiltration
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    matchingMethod: AllEntities
    reopenClosedIncident: false
    enabled: false
    lookbackDuration: PT1H
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: MailMessage
  fieldMappings:
  - identifier: Recipient
    columnName: EmailRecipientTo
  - identifier: Subject
    columnName: EmailSubject
  - identifier: Sender
    columnName: EmailSenderFrom
requiredDataConnectors:
- dataTypes:
  - FilewallExchange_CL
  connectorId: FilewallM365
version: 1.0.0

Stages and Predicates

Stage 1: source

FilewallM365ExchangeEvent()

Stage 2: where

| where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'

Stage 3: where

| where EventType == 'ThreatInfo' and EventResult == 'Blocked'

Indicators

These rows show field, operator, and value matches.

GitLab - TI - Connection from Malicious IP

#
Status
available
Severity
medium
Time window
1d
Group by
$left.TI_ipEntity, $right.IPAddress, IndicatorId
Source
github.com/Azure/Azure-Sentinel

'This query correlates Threat Intelligence data from Microsoft Sentinel with GitLab NGINX Access Logs (available in GitLab CE as well) to identify access from potentially TI-flagged IPs.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 7241740a-5280-4b74-820a-862312d721a8
name: GitLab - TI - Connection from Malicious IP 
description: |
  'This query correlates Threat Intelligence data from Microsoft Sentinel with GitLab NGINX Access Logs (available in GitLab CE as well) to identify access from potentially TI-flagged IPs.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  ThreatIntelligenceIndicator
  | where Action == true
  // Picking up only IOCs that contain the entities we want
  | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
  // Taking the first non-empty value based on potential IOC match availability
  | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
  | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
  | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
  //Exclude local addresses using ipv4_is_private operator
  |where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
  | join (GitLabAccess) on $left.TI_ipEntity == $right.IPAddress
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | project LatestIndicatorTime, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, TimeGenerated = EventTime, TI_ipEntity, IPAddress, URI
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPaddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where Action == true

Stage 3: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 6: join

| join (GitLabAccess) on $left.TI_ipEntity == $right.IPAddress

Stage 7: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 8: project

project ActivityGroupNames, ConfidenceScore, Description, ExpirationDateTime, IPAddress, IndicatorId, LatestIndicatorTime, TI_ipEntity, ThreatType, TimeGenerated, URI, Url

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ConfidenceScoreproject
Descriptionproject
ExpirationDateTimeproject
IPAddressproject
IndicatorIdproject
LatestIndicatorTimeproject
TI_ipEntityproject
ThreatTypeproject
TimeGeneratedproject
URIproject
Urlproject

Java Executing cmd to run Powershell

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

This query was originally published in the threat analytics report, Sysrv botnet evolution. Sysrv is a Go-based botnet that targets both Windows and Linux servers, and steals resources to mine cryptocurrency. The following query finds instances of the Java process being used to execute cmd.exe, and download and execute a PowerShell script.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 2c81c0a0-9823-4a14-b21a-2b4acd3335d2
name: Java Executing cmd to run Powershell
description: |
  This query was originally published in the threat analytics report, Sysrv botnet evolution.
  Sysrv is a Go-based botnet that targets both Windows and Linux servers, and steals resources to mine cryptocurrency.
  The following query finds instances of the Java process being used to execute cmd.exe, and download and execute a PowerShell script.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
    - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
tags:
  - Sysrv
  - Botnet
query: |
  DeviceProcessEvents                         
  | where InitiatingProcessFileName == 'java.exe' and FileName == 'cmd.exe' 
  and ProcessCommandLine has_all('powershell iex','DownloadString')
  | 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 InitiatingProcessFileName == 'java.exe' and FileName == 'cmd.exe' 
and ProcessCommandLine has_all('powershell iex','DownloadString')

Stage 3: extend

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

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostNameextend
DnsDomainextend

Malware Event Detected

#

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

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

Detects when restore points are marked as infected. This might indicate potential compromise of backup data.

Rule Body

id: b42424a6-10f4-447b-92a0-55ac38f4a475
name: Malware Event Detected
description: Detects when restore points are marked as infected. This might indicate
  potential compromise of backup data.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
  dataTypes:
  - VeeamMalwareEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: VeeamMalwareEvents_CL
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: VbrHostName
version: 1.0.1
kind: Scheduled
customDetails:
  VbrHostName: VbrHostName
  MachineDisplayName: MachineDisplayName
  MachineUuid: MachineUuid
  BackupObjectId: MachineBackupObjectId

Stages and Predicates

Stage 1: source

VeeamMalwareEvents_CL

Multi-Factor Authentication Token Revoked

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

Detects when a multi-factor authentication token is revoked.

Rule Body

id: 4d8f5244-cb56-4e95-ba65-0a7bec114a13
name: Multi-Factor Authentication Token Revoked
description: Detects when a multi-factor authentication token is revoked.
severity: Medium
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 == 40202\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = 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 == 40202

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = 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
UserNameproject

NGINX - Multiple user agents for single source

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

'Detects requests with different user agents from one source in short timeframe.'

MITRE ATT&CK Coverage

Rule Body

id: 83a0b48f-1cb7-4b4f-a018-23c3203a239b
name: NGINX - Multiple user agents for single source
description: |
  'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - NGINX_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 5;
  NGINXHTTPServer
  | summarize makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
  | extend ua_count = array_length(set_HttpUserAgentOriginal)
  | where ua_count > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5;

Stage 1: source

NGINXHTTPServer

Stage 2: summarize

| summarize makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 3: extend

| extend ua_count = array_length(set_HttpUserAgentOriginal)

Stage 4: where

| where ua_count > threshold

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ua_countgt
  • 5 transforms: number
field:"ua_count" kind:gt value:"5"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
ua_countextend
IPCustomEntityextend

Rename System Utilities

#
Status
available
Severity
medium
Time window
1d
Group by
OriginalFilename, SHA1
Source
github.com/Azure/Azure-Sentinel

Attackers often use LOLBINs that are renamed to avoid detection rules that are based on filenames. This rule detects renamed LOLBINs by first searching for all the known SHA1 hashes of the LOLBINs in your DeviceProcessEvents. This list is then used as reference to find other files executed which have a name that doesn't match the original filename. This query is really heavy on resources. Use it with care.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 335ddff8-b615-42cd-b593-86e419b45d78
name: Rename System Utilities
description: |
  Attackers often use LOLBINs that are renamed to avoid detection rules that are based on filenames.
  This rule detects renamed LOLBINs by first searching for all the known SHA1 hashes of the LOLBINs in your DeviceProcessEvents. This list is then used as reference to find other files executed which have a name that doesn't match the original filename. 
  This query is really heavy on resources. Use it with care.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1036.003
query: |
  // Removed update.exe as LOLBIN. It's a way too generic name and causes a lot of noise. 
  let lolbin = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]); 
  let allHashesLolbin = materialize(
  DeviceProcessEvents
  | where FileName in~ (lolbin) and not(isempty(SHA1))
  | extend OriginalFilename = tolower(FileName)
  | summarize by SHA1, OriginalFilename);
  allHashesLolbin
  | join kind=inner hint.strategy = broadcast DeviceProcessEvents on SHA1
  | where FileName !~ OriginalFilename
  | where not(OriginalFilename =~ "bash.exe" and FileName =~ "sh.exe")
  | extend message=strcat("Usage of ", OriginalFilename, ". Renamed as ", FileName)
  | project-reorder message
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: AccountSid 
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled

Stages and Predicates

let allHashesLolbin is inlined into the numbered stages below.

Let Binding: lolbin

let lolbin = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]);

Stage 1: source

let allHashesLolbin

Stage 2: source

DeviceProcessEvents

Stage 3: where

where not (isempty(SHA1)) and FileName in~ ("AgentExecutor.exe", "Appvlp.exe", "At.exe", "Atbroker.exe", "Bash.exe", "Bginfo.exe", "Bitsadmin.exe", "Cdb.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Devtoolslauncher.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Dotnet.exe", "Dxcap.exe", "Esentutl.exe", "Eventvwr.exe", "Excel.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Mftrace.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdeploy.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Powerpnt.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "SQLToolsPS.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "Sqldumper.exe", "Sqlps.exe", "Squirrel.exe", "SyncAppvPublishingServer.exe", "Tracker.exe", "Ttdinject.exe", "Tttracer.exe", "Verclsid.exe", "Wab.exe", "Winword.exe", "Wmic.exe", "Wscript.exe", "Wsl.exe", "Wsreset.exe", "Xwizard.exe", "csi.exe", "dnx.exe", "msxsl.exe", "ntdsutil.exe", "rcsi.exe", "te.exe", "vbc.exe", "vsjitdebugger.exe")

Stage 4: extend

extend OriginalFilename

Stage 5: summarize

summarize by SHA1, OriginalFilename

Stage 6: join

join kind=inner (DeviceProcessEvents) on SHA1

Stage 7: where

where FileName != OriginalFilename

Stage 8: where

where not ((FileName =~ "sh.exe" and OriginalFilename =~ "bash.exe"))

Stage 9: extend

extend message

Stage 10: project-reorder

project-reorder

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNamein
  • AgentExecutor.exe
  • Appvlp.exe
  • At.exe
  • Atbroker.exe
  • Bash.exe
  • Bginfo.exe
  • Bitsadmin.exe
  • Cdb.exe
  • CertReq.exe
  • Certutil.exe
  • Cmd.exe
  • Cmdkey.exe
  • Cmstp.exe
  • Control.exe
  • Csc.exe
  • Cscript.exe
  • Desktopimgdownldr.exe
  • Devtoolslauncher.exe
  • Dfsvc.exe
  • Diantz.exe
  • Diskshadow.exe
  • Dnscmd.exe
  • Dotnet.exe
  • Dxcap.exe
  • Esentutl.exe
  • Eventvwr.exe
  • Excel.exe
  • Expand.exe
  • Extexport.exe
  • Extrac32.exe
  • Findstr.exe
  • Forfiles.exe
  • Ftp.exe
  • GfxDownloadWrapper.exe
  • Gpscript.exe
  • Hh.exe
  • Ie4uinit.exe
  • Ieexec.exe
  • Ilasm.exe
  • Infdefaultinstall.exe
  • +63 more values (see full rule source)
field:"FileName" kind:in

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SHA1is_null(no value, null check)excludes:SHA1
FileNameeqsh.exeexcludes:FileName field:"FileName" value:"sh.exe"
OriginalFilenameeqbash.exeexcludes:OriginalFilename field:"OriginalFilename" value:"bash.exe"

Output Fields

These fields are emitted when the rule matches.

FieldSource
OriginalFilenamesummarize
SHA1summarize
messageextend

Rubrik Threat Monitoring

#
Status
available
Severity
medium
Time window
10m
Group by
count_, custom_details_objectId_g, eventname
Source
github.com/Azure/Azure-Sentinel

'Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 0083cbc4-776e-42ca-8694-6950fd605df9
name: Rubrik Threat Monitoring
version: 1.0.0
kind: Scheduled
description: |
  'Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RubrikSecurityCloudAzureFunctions
    dataTypes: 
      - RubrikEventsData
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics: 
  - Persistence
relevantTechniques:
  - T1546
query: |
       Rubrik_Events_Data_CL
       | where custom_details_eventName_s endswith "AnalysisMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringHashCatalogAnalysisFailed" or custom_details_eventName_s contains "ThreatMonitoringHashMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringYaraMatchesFound"
       | extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, summary_s)),    yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, summary_s)),    fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, summary_s))
       | extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28)
       | where count_ > 0
       | summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_
incidentConfiguration:
      createIncident: true
      groupingConfiguration:
        enabled: true
        reopenClosedIncident: false
        lookbackDuration: P7D
        matchingMethod: Selected
        groupByCustomDetails:
        - ObjectName
        - ObjectId
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{custom_details_objectName_s}}
customDetails:
  EventName: custom_details_eventName_s
  Summary: summary_s
  ClusterIdentifier: custom_details_clusterId_g
  ObjectId: custom_details_objectId_g
  ObjectName: custom_details_objectName_s
  ObjectType: custom_details_objectType_s
  Url: custom_details_url_s

Stages and Predicates

Stage 1: source

Rubrik_Events_Data_CL

Stage 2: where

| where custom_details_eventName_s endswith "AnalysisMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringHashCatalogAnalysisFailed" or custom_details_eventName_s contains "ThreatMonitoringHashMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringYaraMatchesFound"

Stage 3: extend

| extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, summary_s)),    yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, summary_s)),    fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, summary_s))

Stage 4: extend

| extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28)

Stage 5: where

| where count_ > 0

Stage 6: summarize

| summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
count_gt
  • 0 transforms: number
field:"count_" kind:gt value:"0"
custom_details_eventName_scontains
  • ThreatMonitoringHashCatalogAnalysisFailed
  • ThreatMonitoringHashMatchesFound
  • ThreatMonitoringYaraMatchesFound
field:"custom_details_eventName_s" kind:contains
custom_details_eventName_sends_with
  • AnalysisMatchesFound
field:"custom_details_eventName_s" kind:ends_with value:"AnalysisMatchesFound"

Output Fields

These fields are emitted when the rule matches.

FieldSource
count_summarize
custom_details_objectId_gsummarize
eventnamesummarize

Semperis DSP Zerologon vulnerability

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

'This indicator looks for security vulnerability to CVE-2020-1472, which was patched by Microsoft in August 2020. Without this patch, an unauthenticated attacker can exploit CVE-2020-1472 to elevate their privileges and get administrative access on the domain.'

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: 85c1f9e4-6f14-46bf-82d5-dbe495b92aab
name: Semperis DSP Zerologon vulnerability
description: |
  'This indicator looks for security vulnerability to CVE-2020-1472, which was patched by Microsoft in August 2020. Without this patch, an unauthenticated attacker can exploit CVE-2020-1472 to elevate their privileges and get administrative access on the domain.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SemperisDSP
    dataTypes:
      - dsp_parser
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1068
query: |
  dsp_parser
  | where EventID == 9212
  | where SecurityIndicatorName == "Zerologon vulnerability"
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 2.0.7
kind: Scheduled

Stages and Predicates

Stage 1: source

dsp_parser

Stage 2: where

| where EventID == 9212

Stage 3: where

| where SecurityIndicatorName == "Zerologon vulnerability"

Stage 4: extend

| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

Snowflake - User granted admin privileges

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

'Detects when user asigned admin privileges.'

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: 5ed33eee-0ab6-4bf5-9e9b-6100db83d39a
name: Snowflake - User granted admin privileges
description: |
  'Detects when user asigned admin privileges.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Snowflake
    dataTypes:
      - Snowflake
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
  let adm_roles = dynamic(['SECURITYADMIN', 'SYSADMIN', 'ACCOUNTADMIN']);
  Snowflake
  | where QueryType =~ 'GRANT'
  | where QueryText startswith 'GRANT ROLE'
  | extend granted_role = extract(@'GRANT ROLE\s(\S+)\sto\suser', 1, QueryText)
  | where granted_role in~ (adm_roles)
  | extend usr = extract(@'GRANT ROLE\s(\S+)\sto\suser(\S+)', 2, QueryText)
  | extend AccountCustomEntity = usr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let adm_roles = dynamic(['SECURITYADMIN', 'SYSADMIN', 'ACCOUNTADMIN']);

Stage 1: source

Snowflake

Stage 2: where

| where QueryType =~ 'GRANT'

Stage 3: where

| where QueryText startswith 'GRANT ROLE'

Stage 4: extend

| extend granted_role = extract(@'GRANT ROLE\s(\S+)\sto\suser', 1, QueryText)

Stage 5: where

| where granted_role in~ (adm_roles)

Stage 6: extend

| extend usr = extract(@'GRANT ROLE\s(\S+)\sto\suser(\S+)', 2, QueryText)

Stage 7: extend

| extend AccountCustomEntity = usr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
granted_roleextend
usrextend
AccountCustomEntityextend

StealthTalk - Login outside work zone

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

Identifies a single StealthTalk authentication originating from a country or city that does not match the user's assigned (expected) geographic zone. Each individual mismatch is treated as an incident - there is no aggregation threshold, since a single login from an unexpected country is high-confidence evidence of a credential issue. An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs from AssignedCity. Source IPv4, raw event ID, and both the observed and expected geo-locations are surfaced as entities and custom details for the SOC analyst.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Stealth

Rule Body

id: a7c3e9b1-4f5d-4e2a-9b8c-1d2e3f4a5b6c
name: StealthTalk - Login outside work zone
description: |
  Identifies a single StealthTalk authentication originating from a country or city that does
  not match the user's assigned (expected) geographic zone. Each individual mismatch is
  treated as an incident - there is no aggregation threshold, since a single login from an
  unexpected country is high-confidence evidence of a credential issue.

  An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs
  from AssignedCity. Source IPv4, raw event ID, and both the observed and expected
  geo-locations are surfaced as entities and custom details for the SOC analyst.
severity: High
requiredDataConnectors:
  - connectorId: StealthTalkAnomalousAuth
    dataTypes:
      - StealthTalkAnomalousAuth_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - DefenseEvasion
  - CredentialAccess
relevantTechniques:
  - T1078
query: |
  let LookbackPeriod = 1h;
  StealthTalkAnomalousAuth_CL
  | where TimeGenerated >= ago(LookbackPeriod)
  | where EventType == "GeoAnomalyLogin"
  | where LoginCountry != AssignedCountry
       or LoginCity    != AssignedCity
  | extend
      CountryMismatch = LoginCountry != AssignedCountry,
      CityMismatch    = LoginCity    != AssignedCity,
      AlertName       = "LoginOutsideWorkZone",
      AlertDetails    = strcat(
          "User ", UserId,
          " logged in from ", LoginCity, " (", LoginCountry, ")",
          " - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
          " Source IP: ", IpAddress, ".",
          " Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
          " City mismatch: ", tostring(LoginCity != AssignedCity), "."
      )
  | project
      TimeGenerated, UserId, DeviceId,
      LoginCountry, LoginCity, AssignedCountry, AssignedCity,
      CountryMismatch, CityMismatch, IpAddress,
      AppVersion, RawEventId, AlertName, AlertDetails
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserId
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
customDetails:
  LoginCountry: LoginCountry
  LoginCity: LoginCity
  AssignedCountry: AssignedCountry
  AssignedCity: AssignedCity
  CountryMismatch: CountryMismatch
  CityMismatch: CityMismatch
  AppVersion: AppVersion
  EventReference: RawEventId
alertDetailsOverride:
  alertDisplayNameFormat: 'StealthTalk: Login Outside Work Zone - {{UserId}} from {{LoginCity}} ({{LoginCountry}})'
  alertDescriptionFormat: '{{AlertDetails}}'
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
      - Account
suppressionEnabled: false
suppressionDuration: 1h
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let LookbackPeriod = 1h;

Stage 1: source

StealthTalkAnomalousAuth_CL

Stage 2: where

| where TimeGenerated >= ago(LookbackPeriod)

Stage 3: where

| where EventType == "GeoAnomalyLogin"

Stage 4: where

| where LoginCountry != AssignedCountry
     or LoginCity    != AssignedCity

Stage 5: extend

| extend
    CountryMismatch = LoginCountry != AssignedCountry,
    CityMismatch    = LoginCity    != AssignedCity,
    AlertName       = "LoginOutsideWorkZone",
    AlertDetails    = strcat(
        "User ", UserId,
        " logged in from ", LoginCity, " (", LoginCountry, ")",
        " - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
        " Source IP: ", IpAddress, ".",
        " Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
        " City mismatch: ", tostring(LoginCity != AssignedCity), "."
    )

Stage 6: project

| project
    TimeGenerated, UserId, DeviceId,
    LoginCountry, LoginCity, AssignedCountry, AssignedCity,
    CountryMismatch, CityMismatch, IpAddress,
    AppVersion, RawEventId, AlertName, AlertDetails

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertDetailsproject
AlertNameproject
AppVersionproject
AssignedCityproject
AssignedCountryproject
CityMismatchproject
CountryMismatchproject
DeviceIdproject
IpAddressproject
LoginCityproject
LoginCountryproject
RawEventIdproject
TimeGeneratedproject
UserIdproject

Time series anomaly for data size transferred to public internet

#
Severity
medium
Time window
14d
Group by
AnomalyHour, DeviceVendor, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern. A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour. The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed . You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: f2dd4a3a-ebac-4994-9499-1a859938c947
name: Time series anomaly for data size transferred to public internet
description: |
  'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern.
  A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated.
  The higher the score, the further it is from the baseline value.
  The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour.
  The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed .
  You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 1
tactics:
  - Exfiltration
relevantTechniques:
  - T1030
tags:
  - DEV-0537
query: |
  let starttime = 14d;
  let endtime = 1d;
  let timeframe = 1h;
  let scorethreshold = 5;
  let bytessentperhourthreshold = 10;
  let TimeSeriesData = (union isfuzzy=true
  (
  VMConnection
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where isnotempty(DestinationIp) and isnotempty(SourceIp)
  | extend SourceIP = SourceIp, DestinationIP = DestinationIp
  | where ipv4_is_private(DestinationIP) == false
  | extend DeviceVendor = "VMConnection"
  | project TimeGenerated, BytesSent, DeviceVendor
  | make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
  ),
  (
  CommonSecurityLog
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | where ipv4_is_private(DestinationIP) == false
  | project TimeGenerated, SentBytes, DeviceVendor
  | make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
  )
  );
  //Filter anomolies against TimeSeriesData
  let TimeSeriesAlerts = materialize(TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(TotalBytesSent, scorethreshold, -1, 'linefit')
  | mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
  | where anomalies > 0 | extend AnomalyHour = TimeGenerated
  | extend TotalBytesSentinMBperHour = round(((TotalBytesSent / 1024)/1024),2), baselinebytessentperHour = round(((baseline / 1024)/1024),2), score = round(score,2)
  | project DeviceVendor, AnomalyHour, TimeGenerated, TotalBytesSentinMBperHour, baselinebytessentperHour, anomalies, score);
  let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
  //Union of all BaseLogs aggregated per hour
  let BaseLogs = (union isfuzzy=true
  (
  CommonSecurityLog
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | where ipv4_is_private(DestinationIP) == false
  | extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
  | summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
  | where TotalSentBytesinMB > bytessentperhourthreshold
  | sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
  | extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
  | where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
  | project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
  ),
  (
  VMConnection
  | where isnotempty(DestinationIp) and isnotempty(SourceIp)
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | extend SourceIP = SourceIp, DestinationIP = DestinationIp
  | where ipv4_is_private(DestinationIP) == false | extend DeviceVendor = "VMConnection"
  | extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
  | summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
  | where TotalSentBytesinMB > bytessentperhourthreshold
  | sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
  | extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
  | where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
  | project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
  )
  );
  // Join against base logs to retrive records associated with the hour of anomoly
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join (
      BaseLogs | extend AnomalyHour = TimeGeneratedHour
  ) on DeviceVendor, AnomalyHour | sort by score desc
  | project DeviceVendor, AnomalyHour,TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
  | summarize EventCount = count(), StartTimeUtc= min(TimeGeneratedMax), EndTimeUtc= max(TimeGeneratedMax), SourceIPMax= arg_max(SourceIP,*), TotalBytesSentinMB = sum(TotalSentBytesinMB), TotalBytesReceivedinMB = sum(TotalReceivedBytesinMB), SourceIPList = make_set(SourceIP, 100), DestinationIPList = make_set(DestinationIPList, 100) by AnomalyHour,TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
  | project DeviceVendor, AnomalyHour, StartTimeUtc, EndTimeUtc, SourceIPMax, SourceIPList, DestinationIPList, DestinationPortList, TotalBytesSentinMB, TotalBytesReceivedinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies, EventCount

entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIPMax
version: 1.0.6
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let bytessentperhourthreshold = 10;

let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.

Let Binding: AnomalyHours

let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);

Let Binding: BaseLogs used in Stage 23

let BaseLogs = (union isfuzzy=true
(
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| where ipv4_is_private(DestinationIP) == false
| extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
| summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
),
(
VMConnection
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false | extend DeviceVendor = "VMConnection"
| extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
| summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
)
);

Stages 1 to 21 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 22 to 27 use it.

Stage 1: union

union of 2 branches

Stage 2: source

VMConnection

Stage 3: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 4: where

| where isnotempty(DestinationIp) and isnotempty(SourceIp)

Stage 5: extend

| extend SourceIP = SourceIp, DestinationIP = DestinationIp

Stage 6: where

| where ipv4_is_private(DestinationIP) == false

Stage 7: extend

| extend DeviceVendor = "VMConnection"

Stage 8: project

| project TimeGenerated, BytesSent, DeviceVendor

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 9: make-series

| make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor

Stage 10: source

CommonSecurityLog

Stage 11: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 12: where

| where isnotempty(DestinationIP) and isnotempty(SourceIP)

Stage 13: where

| where ipv4_is_private(DestinationIP) == false

Stage 14: project

| project TimeGenerated, SentBytes, DeviceVendor

Stage 15: make-series

| make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor

Stage 16: extend

extend anomalies, baseline, score

Stage 17: mv-expand

mv-expand TotalBytesSent

Stage 18: where

where anomalies > 0

Stage 19: extend

extend AnomalyHour

Stage 20: extend

extend TotalBytesSentinMBperHour, baselinebytessentperHour, score

Stage 21: project

project AnomalyHour, DeviceVendor, TimeGenerated, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score

Stage 22: where

where TimeGenerated > ago(172800s)

Stage 23: join

join (BaseLogs) on DeviceVendor, AnomalyHour

Stage 24: sort

sort by score

Stage 25: project

project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, SourceIP, TimeGeneratedMax, TotalBytesSentinMBperHour, TotalReceivedBytesinMB, TotalSentBytesinMB, anomalies, baselinebytessentperHour, score

Stage 26: summarize

summarize DestinationIPList, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB by AnomalyHour, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies

Stage 27: project

project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DestinationIPis_not_null
  • (no value, null check)
field:"DestinationIP" kind:is_not_null
DestinationIpis_not_null
  • (no value, null check)
field:"DestinationIp" kind:is_not_null
Ranklt
  • 10 transforms: number
field:"Rank" kind:lt value:"10"
SourceIPis_not_null
  • (no value, null check)
field:"SourceIP" kind:is_not_null
SourceIpis_not_null
  • (no value, null check)
field:"SourceIp" kind:is_not_null
TotalSentBytesinMBgt
  • 10 transforms: number
field:"TotalSentBytesinMB" kind:gt value:"10"
anomaliesgt
  • 0 transforms: number
field:"anomalies" kind:gt value:"0"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationIPcidr_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:DestinationIP

Output Fields

These fields are emitted when the rule matches.

FieldSource
AnomalyHourproject
DestinationIPListproject
DestinationPortListproject
DeviceVendorproject
EndTimeUtcproject
EventCountproject
SourceIPListproject
SourceIPMaxproject
StartTimeUtcproject
TotalBytesReceivedinMBproject
TotalBytesSentinMBproject
TotalBytesSentinMBperHourproject
anomaliesproject
baselinebytessentperHourproject
scoreproject

Tomcat - Request from localhost IP address

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

'Detects request from localhost IP address.'

MITRE ATT&CK Coverage

Rule Body

id: a45dd6ea-4874-11ec-81d3-0242ac130003
name: Tomcat - Request from localhost IP address
description: |
  'Detects request from localhost IP address.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - Tomcat_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  TomcatEvent
  | where SrcIpAddr == "127.0.0.1"
  | extend File = extract(@'(.*\/)?(.*)', 2, tostring(UrlOriginal))
  | extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TomcatEvent

Stage 2: where

| where SrcIpAddr == "127.0.0.1"

Stage 3: extend

| extend File = extract(@'(.*\/)?(.*)', 2, tostring(UrlOriginal))

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fileextend
FileCustomEntityextend
IPCustomEntityextend

UniFi Site Manager: Data Connector Health

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

Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 1fb5c195-9bff-e18b-3e8f-5123f46bff9d
name: "UniFi Site Manager: Data Connector Health"
description: |
  Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
      - Unifi_SiteManager_Devices_CL
      - Unifi_SiteManager_Hosts_CL
      - Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 15m
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
subTechniques:
  - T1562.006
query: |
  // UniFi Data Connector Health Monitoring
  let StaleThreshold = 30;
  let DevicesHealth = Unifi_SiteManager_Devices_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Devices_CL", TableDisplay = "Devices";
  let HostsHealth = Unifi_SiteManager_Hosts_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Hosts_CL", TableDisplay = "Hosts";
  let SitesHealth = Unifi_SiteManager_Sites_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Sites_CL", TableDisplay = "Sites";
  let ISPHealth = Unifi_SiteManager_ISPMetrics_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_ISPMetrics_CL", TableDisplay = "ISP Metrics";
  union DevicesHealth, HostsHealth, SitesHealth, ISPHealth
  | extend 
      MinutesSinceData = datetime_diff('minute', now(), LastReceived),
      Status = case(
          datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
          datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
          datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
          "Critical"
      )
  | where MinutesSinceData >= StaleThreshold
  | extend TimeGenerated = now()
  | project
      TimeGenerated,
      TableName,
      TableDisplay,
      LastReceived,
      MinutesSinceData,
      Status
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: TableDisplay
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT4H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let StaleThreshold = 30;

Union (4 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: DevicesHealth, HostsHealth, SitesHealth, ISPHealth

Leg 1: DevicesHealth

Leg 2: HostsHealth

Leg 3: SitesHealth

Leg 4: ISPHealth

Applied to the Combined Result

| extend 
    MinutesSinceData = datetime_diff('minute', now(), LastReceived),
    Status = case(
        datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
        datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
        datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
        "Critical"
    ) | where MinutesSinceData >= StaleThreshold | extend TimeGenerated = now() | project
    TimeGenerated,
    TableName,
    TableDisplay,
    LastReceived,
    MinutesSinceData,
    Status

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MinutesSinceDatage
  • 30 transforms: number
field:"MinutesSinceData" kind:ge value:"30"

Output Fields

These fields are emitted when the rule matches.

FieldSource
LastReceivedproject
MinutesSinceDataproject
Statusproject
TableDisplayproject
TableNameproject
TimeGeneratedproject

Back to all Kusto rules