Detection rules › Kusto

Kusto rules: 2w

AWSCloudTrail - Changes to internet facing AWS RDS Database instances

#
Status
available
Severity
low
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, AdditionalEventData, EventName, EventSource, EventTypeName, RecipientAccountId, ResponseElements, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName
Source
github.com/Azure/Azure-Sentinel

Identifies AWS CloudTrail events associated with changes to Amazon RDS database security groups and database security group ingress rules, which may indicate unauthorized modification of internet-facing RDS access. Validate whether the change was authorized and consistent with change control policy. RDS API Reference Docs: https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_Operations.html

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 8c2ef238-67a0-497d-b1dd-5c8a0f533e25
name: AWSCloudTrail - Changes to internet facing AWS RDS Database instances
description: |
  Identifies AWS CloudTrail events associated with changes to Amazon RDS database security groups and database
  security group ingress rules, which may indicate unauthorized modification of internet-facing RDS access. Validate whether
  the change was authorized and consistent with change control policy.
  RDS API Reference Docs: https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_Operations.html
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
  - DefenseEvasion
relevantTechniques:
  - T1098.001
  - T1562.007
query: |
  let EventNameList = dynamic(["AuthorizeDBSecurityGroupIngress","CreateDBSecurityGroup","DeleteDBSecurityGroup","RevokeDBSecurityGroupIngress"]);
  AWSCloudTrail
  | where EventName in~ (EventNameList)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent, UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, ResponseElements
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  AWSRegion: AWSRegion
  UserAgent: UserAgent
  EventName: EventName
  EventSource: EventSource
  EventType: EventTypeName
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS RDS database security change by {{AccountName}} in {{AWSRegion}}'
  alertDescriptionFormat: 'Event {{EventName}} change in RDS security settings by {{AccountName}} from {{SourceIpAddress}}.'
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let EventNameList = dynamic(["AuthorizeDBSecurityGroupIngress","CreateDBSecurityGroup","DeleteDBSecurityGroup","RevokeDBSecurityGroupIngress"]);

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in~ (EventNameList)

Stage 3: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 4: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent, UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, ResponseElements

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventNamein
  • AuthorizeDBSecurityGroupIngress
  • CreateDBSecurityGroup
  • DeleteDBSecurityGroup
  • RevokeDBSecurityGroupIngress
field:"aws::eventName" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
AdditionalEventDatasummarize
EndTimeUtcsummarize
EventNamesummarize
EventSourcesummarize
EventTypeNamesummarize
RecipientAccountIdsummarize
ResponseElementssummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimeUtcsummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityUserNamesummarize

Azure DevOps Audit Detection for known malicious tooling

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

'Azure DevOps has been targeted over the years with a handful of toolkits. This detection will look for some common signs known for a few of these tools.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: bc71cf84-c02c-4c0a-a64c-306d84f9ff89
name: Azure DevOps Audit Detection for known malicious tooling
description: |
  'Azure DevOps has been targeted over the years with a handful of toolkits.  This detection will look for some common signs known for a few of these tools.'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1119
query: |
  ADOAuditLogs
  // Look for known bad user agents
  | where UserAgent has_any ("ADOKit")
  | 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.4
kind: Scheduled

Stages and Predicates

Stage 1: source

ADOAuditLogs

Stage 2: where

| where UserAgent has_any ("ADOKit")

Stage 3: extend

| extend timestamp = TimeGenerated

Stage 4: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
UserAgentmatch
  • ADOKit transforms: term
field:"aws::userAgent" kind:match value:"ADOKit"

Output Fields

These fields are emitted when the rule matches.

FieldSource
timestampextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9 Scenario A Privileges

#

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

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

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9 Scenario A Privileges

Rule Body

id: 4f431f88-ada9-4a20-bbe5-fddc7add942e
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9 Scenario
  A Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC9
  Scenario A Privileges
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0ADCSESC9a"
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 == "T0ADCSESC9a"

Indicators

These rows show field, operator, and value matches.

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

Box - New external user

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

'Detects when new user created with SourceLogin containing non-corporate domain.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence

Rule Body

id: fd36ac88-cd92-4137-aa23-37a3648621fa
name: Box - New external user
description: |
  'Detects when new user created with SourceLogin containing non-corporate domain.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BoxDataConnector
    dataTypes:
      - BoxEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
query: |
  BoxEvents
  | where EventType =~ 'NEW_USER'
  | extend corp_domain = extract(@'@(.*)', 1, SourceLogin)
  | extend new_domain = tolower(extract(@'@(.*)', 1, SourceLogin))
  | where corp_domain != new_domain
  | extend AccountCustomEntity = SourceLogin
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BoxEvents

Stage 2: where

| where EventType =~ 'NEW_USER'

Stage 3: extend

| extend corp_domain = extract(@'@(.*)', 1, SourceLogin)

Stage 4: extend

| extend new_domain = tolower(extract(@'@(.*)', 1, SourceLogin))

Stage 5: where

| where corp_domain != new_domain

Stage 6: extend

| extend AccountCustomEntity = SourceLogin

Stage 7: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
corp_domainextend
new_domainextend
AccountCustomEntityextend
IPCustomEntityextend

Cross-Cloud Password Spray detection

#
Severity
medium
Time window
1m
Group by
AuthenticationRequirement, ClientAppUsed, ConditionalAccessStatus, IPAddress, OperationName, RiskDetail, UserAgent, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

This detection focuses on identifying potential cross-cloud brute force / Password Spray attempts involving Azure and AWS platforms. It monitors sign-in activities within the Azure Portal and AWS ConsoleLogins where brute force attempts are successful on both platforms in a synchronized manner.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 1f40ed57-f54b-462f-906a-ac3a89cc90d4
name: Cross-Cloud Password Spray detection
description: |
  'This detection focuses on identifying potential cross-cloud brute force / Password Spray attempts involving Azure and AWS platforms. It monitors sign-in activities within the Azure Portal and AWS ConsoleLogins where brute force attempts are successful on both platforms in a synchronized manner.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // Materialize a table named "Azure_Bruforce" containing Azure Portal sign-in logs within the last 1 day
  let Azure_Bruforce =  materialize (
      SigninLogs
  // Filter sign-in logs related to the Azure Portal
        | where AppDisplayName == "Azure Portal"
  // Exclude entries with empty OriginalRequestId
      | where isnotempty(OriginalRequestId)
  // Summarize various counts and sets based on brute force criteria
      | summarize 
        AzureSuccessfulEvent = countif(ResultType == 0), 
        AzureFailedEvent = countif(ResultType != 0), 
        totalAzureLoginEventId = dcount(OriginalRequestId), 
        AzureFailedEventsCount = dcountif(OriginalRequestId, ResultType != 0), 
        AzureSuccessfuleventsCount = dcountif(OriginalRequestId, ResultType == 0),
        AzureSetOfFailedevents = makeset(iff(ResultType != 0, OriginalRequestId, ""), 5), 
        AzureSetOfSuccessfulEvents = makeset(iff(ResultType == 0, OriginalRequestId, ""), 5) 
        by 
        IPAddress, 
        UserPrincipalName, 
        bin(TimeGenerated, 1min), 
        UserAgent,
        ConditionalAccessStatus,
        OperationName,
        RiskDetail,
        AuthenticationRequirement,
        ClientAppUsed
  // Extracting the name and UPN suffix from UserPrincipalName
      | extend
          Name = tostring(split(UserPrincipalName, '@')[0]),
          UPNSuffix = tostring(split(UserPrincipalName, '@')[1]));
  // Materialize a table named "AWS_Bruforce" containing AWS CloudTrail events related to ConsoleLogins within the last 1 day
  let AWS_Bruforce = materialize (
      AWSCloudTrail 
  // Filter CloudTrail events related to ConsoleLogin
      | where EventName == "ConsoleLogin" 
  // Extract ActionType from ResponseElements JSON
      | extend ActionType = tostring(parse_json(ResponseElements).ConsoleLogin)  
  // Summarize various counts and sets based on  brute force criteria 
      | summarize 
          AWSSuccessful=countif(ActionType == "Success"), 
          AWSFailed = countif(ActionType == "Failure"), 
          totalAwsEventId= dcount(AwsEventId), 
          AWSFailedEventsCount = dcountif(AwsEventId, ActionType == "Failure"), 
          AWSSuccessfuleventsCount = dcountif(AwsEventId, ActionType == "Success"), 
          AWSFailedevents = makeset(iff(ActionType == "Failure", AwsEventId, ""), 5), 
          AWSSuccessfulEvents = makeset(iff(ActionType == "Success", AwsEventId, ""), 5)  
  // Grouping by various attributes
          by 
          SourceIpAddress, 
          UserIdentityUserName,
          bin(TimeGenerated, 1min), 
          UserAgent );
  // Joining the Azure_Bruforce and AWS_Bruforce tables on matching IP addresses and UserAgents
  Azure_Bruforce
  | join kind=inner AWS_Bruforce on $left.IPAddress == $right.SourceIpAddress and $left.UserAgent == $right.UserAgent
  // Filtering based on conditions for failed and successful events
  | where (AWSFailedEventsCount >= 4 and AzureFailedEventsCount >= 5) and ((AzureSuccessfuleventsCount >= 1 and AzureFailedEvent > AzureSuccessfulEvent) or (AWSSuccessfuleventsCount >= 1 and AWSFailedEventsCount > AWSSuccessfuleventsCount))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
   AwsUser: UserIdentityUserName
   UserAgent: UserAgent
   AzureUser: UserPrincipalName
   AzureClientAppUsed: ClientAppUsed
kind: Scheduled 
version: 1.0.1

Stages and Predicates

let Azure_Bruforce is inlined into the numbered stages below.

Let Binding: AWS_Bruforce used in Stage 6

let AWS_Bruforce = materialize (
    AWSCloudTrail 
    | where EventName == "ConsoleLogin" 
    | extend ActionType = tostring(parse_json(ResponseElements).ConsoleLogin)  
    | summarize 
        AWSSuccessful=countif(ActionType == "Success"), 
        AWSFailed = countif(ActionType == "Failure"), 
        totalAwsEventId= dcount(AwsEventId), 
        AWSFailedEventsCount = dcountif(AwsEventId, ActionType == "Failure"), 
        AWSSuccessfuleventsCount = dcountif(AwsEventId, ActionType == "Success"), 
        AWSFailedevents = makeset(iff(ActionType == "Failure", AwsEventId, ""), 5), 
        AWSSuccessfulEvents = makeset(iff(ActionType == "Success", AwsEventId, ""), 5)  
        by 
        SourceIpAddress, 
        UserIdentityUserName,
        bin(TimeGenerated, 1min), 
        UserAgent );

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

Stage 1: source

SigninLogs

Stage 2: where

| where AppDisplayName == "Azure Portal"

Stage 3: where

| where isnotempty(OriginalRequestId)

Stage 4: summarize

| summarize 
      AzureSuccessfulEvent = countif(ResultType == 0), 
      AzureFailedEvent = countif(ResultType != 0), 
      totalAzureLoginEventId = dcount(OriginalRequestId), 
      AzureFailedEventsCount = dcountif(OriginalRequestId, ResultType != 0), 
      AzureSuccessfuleventsCount = dcountif(OriginalRequestId, ResultType == 0),
      AzureSetOfFailedevents = makeset(iff(ResultType != 0, OriginalRequestId, ""), 5), 
      AzureSetOfSuccessfulEvents = makeset(iff(ResultType == 0, OriginalRequestId, ""), 5) 
      by 
      IPAddress, 
      UserPrincipalName, 
      bin(TimeGenerated, 1min), 
      UserAgent,
      ConditionalAccessStatus,
      OperationName,
      RiskDetail,
      AuthenticationRequirement,
      ClientAppUsed

Stage 5: extend

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

Stage 6: join

Azure_Bruforce
| join kind=inner AWS_Bruforce on $left.IPAddress == $right.SourceIpAddress and $left.UserAgent == $right.UserAgent

Stage 7: where

where ((AWSFailedEventsCount > AWSSuccessfuleventsCount and AWSSuccessfuleventsCount >= 1) or (AzureFailedEvent > AzureSuccessfulEvent and AzureSuccessfuleventsCount >= 1)) and AWSFailedEventsCount >= 4 and AzureFailedEventsCount >= 5

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthenticationRequirementsummarize
AzureFailedEventsummarize
AzureFailedEventsCountsummarize
AzureSetOfFailedeventssummarize
AzureSetOfSuccessfulEventssummarize
AzureSuccessfulEventsummarize
AzureSuccessfuleventsCountsummarize
ClientAppUsedsummarize
ConditionalAccessStatussummarize
IPAddresssummarize
OperationNamesummarize
RiskDetailsummarize
UserAgentsummarize
UserPrincipalNamesummarize
totalAzureLoginEventIdsummarize
Nameextend
UPNSuffixextend

Discord CDN Risky File Download

#
Status
available
Severity
medium
Time window
1d
Group by
DeviceProduct, DiscordServerId, dcount_RequestURL, max_TimeGenerated, min_TimeGenerated, set_DeviceAction, set_RequestURL, set_SourceIP, set_SourceUserName
Source
github.com/Azure/Azure-Sentinel

'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads'

MITRE ATT&CK Coverage

Rule Body

id: 010bd98c-a6be-498c-bdcd-502308c0fdae
name: Discord CDN Risky File Download
description: |
  'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071.001
tags:
  - Discord
query: |
  let connectionThreshold = 1;
  let riskyExtensions = dynamic([".bin",".exe",".dll",".bin",".msi"]);
  CommonSecurityLog
  | where DeviceVendor =~ "ZScaler"
  | where RequestURL has_any("media.discordapp.net", "cdn.discordapp.com")
  | where RequestURL has "attachments"
  | where DeviceAction !~ "blocked"
  | extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, RequestURL)
  | summarize dcount(RequestURL), make_set(SourceUserName), make_set(SourceIP), make_set(RequestURL), min(TimeGenerated), max(TimeGenerated), make_set(DeviceAction) by DiscordServerId, DeviceProduct
  | where dcount_RequestURL <= connectionThreshold
  | mv-expand set_SourceUserName to typeof(string), set_RequestURL to typeof(string), set_DeviceAction to typeof(string), set_SourceIP to typeof(string)
  | summarize by DiscordServerId, DeviceProduct, dcount_RequestURL, set_SourceUserName, min_TimeGenerated, max_TimeGenerated, set_DeviceAction, set_SourceIP, set_RequestURL
  | project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, DeviceActionTaken=set_DeviceAction, DeviceProduct, SourceUser=set_SourceUserName, SourceIP=set_SourceIP, RequestURL=set_RequestURL
  | where RequestURL has_any (riskyExtensions)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SourceUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let connectionThreshold = 1;
let riskyExtensions = dynamic([".bin",".exe",".dll",".bin",".msi"]);

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor =~ "ZScaler"

Stage 3: where

| where RequestURL has_any("media.discordapp.net", "cdn.discordapp.com")

Stage 4: where

| where RequestURL has "attachments"

Stage 5: where

| where DeviceAction !~ "blocked"

Stage 6: extend

| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, RequestURL)

Stage 7: summarize

| summarize dcount(RequestURL), make_set(SourceUserName), make_set(SourceIP), make_set(RequestURL), min(TimeGenerated), max(TimeGenerated), make_set(DeviceAction) by DiscordServerId, DeviceProduct

Stage 8: where

| where dcount_RequestURL <= connectionThreshold

Stage 9: mv-expand

| mv-expand set_SourceUserName to typeof(string), set_RequestURL to typeof(string), set_DeviceAction to typeof(string), set_SourceIP to typeof(string)

Stage 10: summarize

| summarize by DiscordServerId, DeviceProduct, dcount_RequestURL, set_SourceUserName, min_TimeGenerated, max_TimeGenerated, set_DeviceAction, set_SourceIP, set_RequestURL

Stage 11: project

| project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, DeviceActionTaken=set_DeviceAction, DeviceProduct, SourceUser=set_SourceUserName, SourceIP=set_SourceIP, RequestURL=set_RequestURL

Stage 12: where

| where RequestURL has_any (riskyExtensions)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DeviceActionne
  • blocked
field:"DeviceAction" kind:ne value:"blocked"
DeviceVendoreq
  • ZScaler
field:"DeviceVendor" kind:eq value:"ZScaler"
RequestURLmatch
  • .bin transforms: term
  • .dll transforms: term
  • .exe transforms: term
  • .msi transforms: term
  • attachments transforms: term
  • cdn.discordapp.com transforms: term
  • media.discordapp.net transforms: term
field:"RequestURL" kind:match
dcount_RequestURLle
  • 1 transforms: number
field:"dcount_RequestURL" kind:le value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceActionTakenproject
DeviceProductproject
EndTimeproject
RequestURLproject
SourceIPproject
SourceUserproject
StartTimeproject

Files Copied to USB Drives

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

This query lists files copied to USB external drives with USB drive information based on FileCreated events associated with most recent USBDriveMount events befor file creations. But be aware that Advanced Hunting is not monitoring all the file types.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rule Body

id: 3ab04acf-e0e7-4f7c-8995-748ab4c848c2
name: Files Copied to USB Drives
description: | 
  This query lists files copied to USB external drives with USB drive information based on FileCreated events associated with most recent USBDriveMount events befor file creations. But be aware that Advanced Hunting is not monitoring all the file types.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
      - DeviceFileEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1041
query: |
  let UsbDriveMount = DeviceEvents
  | where ActionType=="UsbDriveMounted"
  | extend ParsedFields=parse_json(AdditionalFields)
  | project DeviceId, DeviceName, DriveLetter=ParsedFields.DriveLetter, MountTime=TimeGenerated,
  ProductName=ParsedFields.ProductName,SerialNumber=ParsedFields.SerialNumber,Manufacturer=ParsedFields.Manufacturer
  | order by DeviceId asc, MountTime desc;
  let FileCreation = DeviceFileEvents
  | where InitiatingProcessAccountName != "system"
  | where ActionType == "FileCreated"
  | where FolderPath !startswith "C:\\"
  | where FolderPath !startswith "\\"
  | project ReportId,DeviceId,InitiatingProcessAccountDomain,
  InitiatingProcessAccountName,InitiatingProcessAccountUpn,
  FileName, FolderPath, SHA256, TimeGenerated, SensitivityLabel, IsAzureInfoProtectionApplied
  | order by DeviceId asc, TimeGenerated desc;
  FileCreation | lookup kind=inner (UsbDriveMount) on DeviceId
  | where FolderPath startswith DriveLetter
  | where TimeGenerated >= MountTime
  | partition hint.strategy=native by ReportId ( top 1 by MountTime )
  | order by DeviceId asc, TimeGenerated desc
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
  | extend FileHashAlgorithm = 'SHA256'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileName
      - identifier: Directory
        columnName: FolderPath
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: FileHashAlgorithm
      - identifier: Value
        columnName: SHA256
version: 1.0.0
kind: Scheduled

Stages and Predicates

let FileCreation is inlined into the numbered stages below.

Let Binding: UsbDriveMount used in Stages 1, 10

let UsbDriveMount = DeviceEvents
| where ActionType=="UsbDriveMounted"
| extend ParsedFields=parse_json(AdditionalFields)
| project DeviceId, DeviceName, DriveLetter=ParsedFields.DriveLetter, MountTime=TimeGenerated,
ProductName=ParsedFields.ProductName,SerialNumber=ParsedFields.SerialNumber,Manufacturer=ParsedFields.Manufacturer
| order by DeviceId asc, MountTime desc;

Stage 1: source

let UsbDriveMount

Stage 2: source

let FileCreation

Stage 3: source

DeviceFileEvents

Stage 4: where

where InitiatingProcessAccountName != "system"

Stage 5: where

where ActionType == "FileCreated"

Stage 6: where

where not (FolderPath startswith @"C:\")

Stage 7: where

where not (FolderPath startswith @"\")

Stage 8: project

project DeviceId, FileName, FolderPath, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessAccountUpn, IsAzureInfoProtectionApplied, ReportId, SHA256, SensitivityLabel, TimeGenerated

Stage 9: sort

sort by DeviceId, TimeGenerated

Stage 10: kusto:lookup

lookup kind=inner (UsbDriveMount) on DeviceId

Stage 11: where

where FolderPath startswith "DriveLetter"

Stage 12: where

where TimeGenerated >= MountTime

Stage 13: partition

partition

Stage 14: sort

sort by DeviceId, TimeGenerated

Stage 15: extend (3 consecutive steps)

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

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
FolderPathstarts_withC:\excludes:FolderPath field:"FolderPath" value:"C:\"
FolderPathstarts_with\excludes:FolderPath field:"FolderPath" value:"\"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdproject
FileNameproject
FolderPathproject
InitiatingProcessAccountDomainproject
InitiatingProcessAccountNameproject
InitiatingProcessAccountUpnproject
IsAzureInfoProtectionAppliedproject
ReportIdproject
SHA256project
SensitivityLabelproject
TimeGeneratedproject
HostNameextend
DnsDomainextend
FileHashAlgorithmextend

Google Threat Intelligence - Threat Hunting Hash

#
Severity
medium
Time window
1d
Group by
Id, lowerHash
Source
github.com/Azure/Azure-Sentinel

Google Threat Intelligence hash correlation.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: "8f9cd0e5-b4ab-4821-95e2-1082fcd784c7"
name: Google Threat Intelligence - Threat Hunting Hash
description: |
  'Google Threat Intelligence hash correlation.'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 30m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Google Threat Intelligence Match'
  alertDescriptionFormat: 'Correlation found from the {{Type}} table.'
query: |
  let ioc_lookBack = 1d;
  _Im_FileEvent
  | where isnotempty(Hash)
  | extend lowerHash=tolower(Hash)
  | join kind=inner (
  ThreatIntelIndicators
  | where ObservableKey contains 'file:hashes'
  | where isnotempty(ObservableValue)
  | where SourceSystem == "Google Threat Intelligence"
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
  | where IsActive == true and (isnull(ValidUntil) or ValidUntil > now())
  | extend lowerHash=tolower(ObservableValue)
  ) on lowerHash
  | project Hash=ObservableValue, HashType=extract("file:hashes.('[^']*')", 1, ObservableKey), Description=Data.description, Type, TimeGenerated
entityMappings:
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: Hash
      - identifier: Algorithm
        columnName: HashType
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let ioc_lookBack = 1d;

Stage 1: source

_Im_FileEvent

Stage 2: where

| where isnotempty(Hash)

Stage 3: extend

| extend lowerHash=tolower(Hash)

Stage 4: join

| join kind=inner (
ThreatIntelIndicators
| where ObservableKey contains 'file:hashes'
| where isnotempty(ObservableValue)
| where SourceSystem == "Google Threat Intelligence"
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
| where IsActive == true and (isnull(ValidUntil) or ValidUntil > now())
| extend lowerHash=tolower(ObservableValue)
) on lowerHash

Stage 5: project

project Description, Hash, HashType, TimeGenerated, Type

Stage 6: summarize aggregation inside the join branch

summarize by Id

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Idsummarize

Hunt for public facing devices via public tag

#
Group by
DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingReason, InternetFacingTransportProtocol
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

Find public facing devices via the public device tag in the DeviceInfo table. The internet facing reason is also included in this query.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

References

Telemetry Coverage

Rule Body

DeviceInfo
| where Timestamp > ago(7d)
| extend AdditionalFields = todynamic(AdditionalFields)
| where todatetime(AdditionalFields.InternetFacingLastSeen) > ago(7d)
| extend InternetFacingLastSeen = tostring(AdditionalFields.InternetFacingLastSeen)
    , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
    , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
    , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
    , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
    , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
    , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)
| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason
| project InternetFacingLastSeen, DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason

Stages and Predicates

Stage 1: source

DeviceInfo

Stage 2: where

| where Timestamp > ago(7d)

Stage 3: extend

| extend AdditionalFields = todynamic(AdditionalFields)

Stage 4: where

| where todatetime(AdditionalFields.InternetFacingLastSeen) > ago(7d)

Stage 5: extend

| extend InternetFacingLastSeen = tostring(AdditionalFields.InternetFacingLastSeen)
    , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
    , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
    , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
    , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
    , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
    , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)

Stage 6: summarize

| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason

Stage 7: project

| project InternetFacingLastSeen, DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNameproject
InternetFacingLastSeenproject
InternetFacingLocalIpproject
InternetFacingLocalPortproject
InternetFacingPublicScannedIpproject
InternetFacingPublicScannedPortproject
InternetFacingReasonproject
InternetFacingTransportProtocolproject

Lumen TI domain in DnsEvents

#
Severity
medium
Time window
14d
Group by
$left.TI_domainEntity, $right.DNS_domainEntity, DNS_domainEntity, Id
Source
github.com/Azure/Azure-Sentinel

This query searches for matches between Lumen threat intelligence domain indicators and DnsEvents.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 29bf5bcd-6795-4c79-a91f-aaef5a618bab
name: Lumen TI domain in DnsEvents
displayName: Lumen TI domain in DnsEvents
description: |
  This query searches for matches between Lumen threat intelligence domain indicators and DnsEvents.
severity: Medium
requiredDataConnectors:
  - connectorId: LumenThreatFeedConnector
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceUploadIndicatorsAPI
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: DNS
    dataTypes:
      - DnsEvents
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1d; // Data lookback for DnsEvents
  let ioc_lookBack = 14d; // TI lookback
  // Latest, active, non-expired Lumen domain indicators
  let Domain_Indicators = ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | where IsActive == true and ValidUntil > now()
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
    | where SourceSystem == 'Lumen'
    | where ObservableKey == 'domain-name:value' or ObservableValue contains '.'
    | extend TI_domainEntity = tostring(ObservableValue);
  Domain_Indicators
  | join kind=innerunique (
      DnsEvents
      | where TimeGenerated >= ago(dt_lookBack)
      | extend DNS_domainEntity = Name
      | extend DnsEvents_TimeGenerated = TimeGenerated
    ) on $left.TI_domainEntity == $right.DNS_domainEntity
  | where DnsEvents_TimeGenerated < ValidUntil
  | summarize arg_max(DnsEvents_TimeGenerated, *), StartTime = min(DnsEvents_TimeGenerated), EndTime = max(DnsEvents_TimeGenerated) by Id, DNS_domainEntity
  | project timestamp = EndTime, StartTime, EndTime, Name, QueryType, Computer, Id, Tags, ValidUntil, Confidence, TI_domainEntity, DNS_domainEntity, Type
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DNS_domainEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

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

let Domain_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: where

| where IsActive == true and ValidUntil > now()

Stage 4: summarize

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

Stage 5: where

| where SourceSystem == 'Lumen'

Stage 6: where

| where ObservableKey == 'domain-name:value' or ObservableValue contains '.'

Stage 7: extend

| extend TI_domainEntity = tostring(ObservableValue)

Stage 8: join

Domain_Indicators
| join kind=innerunique (
    DnsEvents
    | where TimeGenerated >= ago(dt_lookBack)
    | extend DNS_domainEntity = Name
    | extend DnsEvents_TimeGenerated = TimeGenerated
  ) on $left.TI_domainEntity == $right.DNS_domainEntity

Stage 9: where

where DnsEvents_TimeGenerated < ValidUntil

Stage 10: summarize

summarize EndTime, StartTime by Id, DNS_domainEntity

Stage 11: project

project Computer, Confidence, DNS_domainEntity, EndTime, Id, Name, QueryType, StartTime, TI_domainEntity, Tags, Type, ValidUntil, timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
Confidenceproject
DNS_domainEntityproject
EndTimeproject
Idproject
Nameproject
QueryTypeproject
StartTimeproject
TI_domainEntityproject
Tagsproject
Typeproject
ValidUntilproject
timestampproject

Lumen TI IPAddress in SecurityEvents

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

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

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 140a2cb5-4b4a-485c-aab3-2415c24d37e6
name: Lumen TI IPAddress in SecurityEvents
displayName: Lumen TI IPAddress in SecurityEvents
description: |
  This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SecurityEvents.
severity: Medium
requiredDataConnectors:
  - connectorId: LumenThreatFeedConnector
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceUploadIndicatorsAPI
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1d;  // Data lookback for SecurityEvent
  let ioc_lookBack = 14d; // TI lookback
  let IP_Indicators = ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | where IsActive == true and ValidUntil > now()
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
    | where SourceSystem == 'Lumen'
    | where ObservableKey == 'ipv4-addr:value'
    | extend TI_ipEntity = ObservableValue
    | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith 'fe80' and TI_ipEntity !startswith '::' and TI_ipEntity !startswith '127.';
  IP_Indicators
  | join kind=innerunique (
      SecurityEvent
      | where TimeGenerated >= ago(dt_lookBack)
      | extend SE_ipEntity = IpAddress
      | extend SecurityEvent_TimeGenerated = TimeGenerated
    ) on $left.TI_ipEntity == $right.SE_ipEntity
  | where SecurityEvent_TimeGenerated < ValidUntil
  | summarize arg_max(SecurityEvent_TimeGenerated, *), StartTime = min(SecurityEvent_TimeGenerated), EndTime = max(SecurityEvent_TimeGenerated) by Id, SE_ipEntity
  | project timestamp = EndTime, StartTime, EndTime, Computer, IpAddress, EventID, Activity, Id, Tags, ValidUntil, Confidence, TI_ipEntity, SE_ipEntity, Type
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SE_ipEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 8 define let IP_Indicators as the rule's main pipeline source. Stages 9 to 12 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: where

| where IsActive == true and ValidUntil > now()

Stage 4: summarize

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

Stage 5: where

| where SourceSystem == 'Lumen'

Stage 6: where

| where ObservableKey == 'ipv4-addr:value'

Stage 7: extend

| extend TI_ipEntity = ObservableValue

Stage 8: where

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

Stage 9: join

IP_Indicators
| join kind=innerunique (
    SecurityEvent
    | where TimeGenerated >= ago(dt_lookBack)
    | extend SE_ipEntity = IpAddress
    | extend SecurityEvent_TimeGenerated = TimeGenerated
  ) on $left.TI_ipEntity == $right.SE_ipEntity

Stage 10: where

where SecurityEvent_TimeGenerated < ValidUntil

Stage 11: summarize

summarize EndTime, StartTime by Id, SE_ipEntity

Stage 12: project

project Activity, Computer, Confidence, EndTime, EventID, Id, IpAddress, SE_ipEntity, StartTime, TI_ipEntity, Tags, Type, ValidUntil, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IsActiveeq
  • true corpus 12 (kusto 12)
field:"IsActive" kind:eq value:"true"
ObservableKeyeq
  • ipv4-addr:value corpus 10 (kusto 10)
field:"ObservableKey" kind:eq value:"ipv4-addr:value"
SourceSystemeq
  • Lumen corpus 8 (kusto 8)
field:"SourceSystem" kind:eq value:"Lumen"

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
Activityproject
Computerproject
Confidenceproject
EndTimeproject
EventIDproject
Idproject
IpAddressproject
SE_ipEntityproject
StartTimeproject
TI_ipEntityproject
Tagsproject
Typeproject
ValidUntilproject
timestampproject

Palo Alto - possible nmap scan on with top 100 option

#
Status
available
Severity
medium
Time window
2m
Group by
Computer, DestinationIP, SourceIP
Source
github.com/Azure/Azure-Sentinel

'Detect possible execution of Nmap top 100 option. This detection will detect scanning of 90% of Top 100 port of NMAP in less than 2 minutes. Unusual port only access through a scan are present in this list which is a good indicator of reconnaissance tactics. Whitelisting of Company scanners is required with implementation of the rule. Ref : https://nmap.org/book/performance-port-selection.html'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: 4d61bb9a-7f6d-45b1-ac0e-517e2a92f6fd
name: Palo Alto - possible nmap scan on with top 100 option
description: |
  'Detect possible execution of Nmap top 100 option. This detection will detect scanning of 90% of Top 100 port of NMAP in less than 2 minutes. Unusual port only access through a scan are present in this list which is a good indicator of reconnaissance tactics. Whitelisting of Company scanners is required with implementation of the rule. Ref : https://nmap.org/book/performance-port-selection.html'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
query: |
 CommonSecurityLog
 | where ipv4_is_private(SourceIP)
 | where DestinationPort in (7, 9, 13, 21, 22, 23, 25, 26, 37, 79, 81, 88, 106, 110, 111, 119, 135, 139, 143, 144, 179, 199, 389, 427, 444, 445, 465, 513, 514, 515, 543, 544, 548, 554, 587, 631, 646, 873, 990, 993, 995, 1025, 1026, 1027, 1028, 1029, 1110, 1433, 1720, 1723, 1755, 1900, 2000, 2001, 2049, 2121, 2717, 3000, 3128, 3306, 3389, 3986, 4899, 5000, 5009, 5051, 5060, 5101, 5190, 5357, 5432, 5631, 5666, 5800, 5900, 6000, 6001, 6646, 7070, 8000, 8008, 8009, 8080, 8081, 8443, 8888, 9100, 9999, 10000, 32768, 49152, 49153, 49154, 49155, 49156, 49157)
 | summarize
     dcount(DestinationPort),
     make_set(DestinationPort),
     make_set(ApplicationProtocol),
     make_set(Activity),
     make_set(SourcePort),
     StartTime = min(TimeGenerated),
     EndTime = max(TimeGenerated)
     by SourceIP, DestinationIP, Computer, bin(TimeGenerated, 2m)
 | where dcount_DestinationPort > 90

entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where ipv4_is_private(SourceIP)

Stage 3: where

| where DestinationPort in (7, 9, 13, 21, 22, 23, 25, 26, 37, 79, 81, 88, 106, 110, 111, 119, 135, 139, 143, 144, 179, 199, 389, 427, 444, 445, 465, 513, 514, 515, 543, 544, 548, 554, 587, 631, 646, 873, 990, 993, 995, 1025, 1026, 1027, 1028, 1029, 1110, 1433, 1720, 1723, 1755, 1900, 2000, 2001, 2049, 2121, 2717, 3000, 3128, 3306, 3389, 3986, 4899, 5000, 5009, 5051, 5060, 5101, 5190, 5357, 5432, 5631, 5666, 5800, 5900, 6000, 6001, 6646, 7070, 8000, 8008, 8009, 8080, 8081, 8443, 8888, 9100, 9999, 10000, 32768, 49152, 49153, 49154, 49155, 49156, 49157)

Stage 4: summarize

| summarize
    dcount(DestinationPort),
    make_set(DestinationPort),
    make_set(ApplicationProtocol),
    make_set(Activity),
    make_set(SourcePort),
    StartTime = min(TimeGenerated),
    EndTime = max(TimeGenerated)
    by SourceIP, DestinationIP, Computer, bin(TimeGenerated, 2m)

Stage 5: where

| where dcount_DestinationPort > 90

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DestinationPortin
  • 10000
  • 1025
  • 1026
  • 1027
  • 1028
  • 1029
  • 106
  • 110
  • 111
  • 1110
  • 119
  • 13
  • 135
  • 139
  • 143
  • 1433
  • 144
  • 1720
  • 1723
  • 1755
  • 179
  • 1900
  • 199
  • 2000
  • 2001
  • 2049
  • 21
  • 2121
  • 22
  • 23
  • 25
  • 26
  • 2717
  • 3000
  • 3128
  • 32768
  • 3306
  • 3389
  • 37
  • 389
  • +56 more values (see full rule source)
field:"DestinationPort" kind:in
SourceIPcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"SourceIP" kind:cidr_match
dcount_DestinationPortgt
  • 90 transforms: number
field:"dcount_DestinationPort" kind:gt value:"90"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
DestinationIPsummarize
EndTimesummarize
SourceIPsummarize
StartTimesummarize

Password Spray

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

Below queries detect password spray attacks using sliding window count plugin. Because of implementation of the sliding window, queries work better than the bin() usage, but may create duplicate alerts. Grouping can be used in such cases. Sentinel Query:

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique

References

Telemetry Coverage

Rule Body

// Author       : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/advanced-kql-for-threat-hunting-window-functions-part-2-dce3e321f54b
//
// Description  : Detect if there are more than 2 distinct users seen from the same IP in a 3h window.
//
// Query parameters:
//
let start = ago(12h);
let end = now();
let lookbackWindow = 3h;
let bin = 1h;
let threshold = 2;
SecurityEvent
| where EventID in (4624, 4625)
| where IpAddress !in ("127.0.0.1", "::1", "-")
| evaluate sliding_window_counts(TargetUserName, TimeGenerated, start, end, lookbackWindow, bin, IpAddress)
| sort by IpAddress, TimeGenerated asc
| where Dcount >= 2

// Author       : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/advanced-kql-for-threat-hunting-window-functions-part-2-dce3e321f54b
//
// Description  : Detect if there are more than 2 distinct users seen from the same IP in a 3h window.
//
// Query parameters:
//
let start = ago(12h);
let end = now();
let lookbackWindow = 3h;
let bin = 1h;
let threshold = 2;
DeviceLogonEvents
| where Timestamp > ago(lookbackWindow)
| where RemoteIP !in ("127.0.0.1","::1","-") and isnotempty(RemoteIP)
| evaluate sliding_window_counts(AccountName, Timestamp, start, end, lookbackWindow, bin, RemoteIP)
| where Dcount > threshold

Stages and Predicates

Parameters

let start = ago(12h);
let end = now();
let lookbackWindow = 3h;
let bin = 1h;
let threshold = 2;

Stage 1: source

SecurityEvent

Stage 2: where

where EventID in (4624, 4625)

Stage 3: where

where not (IpAddress in ("-", "127.0.0.1", "::1"))

Stage 4: evaluate

evaluate

Stage 5: sort

sort by IpAddress, TimeGenerated

Stage 6: where

where Dcount >= 2

Stage 7: source

DeviceLogonEvents

Stage 8: where

where Timestamp > ago(10800s)

Stage 9: where

where not (RemoteIP in ("-", "127.0.0.1", "::1")) and isnotempty(RemoteIP)

Stage 10: evaluate

evaluate

Stage 11: where

where Dcount > 2

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Dcountge
  • 2 transforms: number
field:"Dcount" kind:ge value:"2"
Dcountgt
  • 2 transforms: number
field:"Dcount" kind:gt value:"2"
EventIDin
  • 4624
  • 4625
field:"EventID" kind:in
RemoteIPis_not_null
  • (no value, null check)
field:"RemoteIP" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

Password spray attack against ADFSSignInLogs

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

'Identifies evidence of password spray activity against Connect Health for AD FS sign-in events by looking for failures from multiple accounts from the same IP address within a time window. Reference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/ADFSSignInLogsPasswordSpray.md'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 5533fe80-905e-49d5-889a-df27d2c3976d
name: Password spray attack against ADFSSignInLogs
description: |
  'Identifies evidence of password spray activity against Connect Health for AD FS sign-in events by looking for failures from multiple accounts from the same IP address within a time window. Reference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft%20Entra%20ID/ADFSSignInLogsPasswordSpray.md'
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - ADFSSignInLogs
severity: Medium
queryFrequency: 30m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let queryfrequency = 30m;
  let accountthreshold = 10;
  let successCodes = dynamic([0, 50144]);
  ADFSSignInLogs
  | extend IngestionTime = ingestion_time()
  | where IngestionTime > ago(queryfrequency)
  | where not(todynamic(AuthenticationDetails)[0].authenticationMethod == "Integrated Windows Authentication")
  | summarize
      DistinctFailureCount = dcountif(UserPrincipalName, ResultType !in (successCodes)),
      DistinctSuccessCount = dcountif(UserPrincipalName, ResultType in (successCodes)),
      SuccessAccounts = make_set_if(UserPrincipalName, ResultType in (successCodes), 250),
      arg_min(TimeGenerated, *)
      by IPAddress
  | where DistinctFailureCount > DistinctSuccessCount and DistinctFailureCount >= accountthreshold
  //| extend SuccessAccounts = iff(array_length(SuccessAccounts) != 0, SuccessAccounts, dynamic(["null"]))
  //| mv-expand SuccessAccounts
  | project TimeGenerated, Category, OperationName, IPAddress, DistinctFailureCount, DistinctSuccessCount, SuccessAccounts, AuthenticationRequirement, ConditionalAccessStatus, IsInteractive, UserAgent, NetworkLocationDetails, DeviceDetail, TokenIssuerType, TokenIssuerName, ResourceIdentity
entityMappings:
- entityType: IP
  fieldMappings:
    - identifier: Address
      columnName: IPAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 30m;
let accountthreshold = 10;
let successCodes = dynamic([0, 50144]);

Stage 1: source

ADFSSignInLogs

Stage 2: extend

| extend IngestionTime = ingestion_time()

Stage 3: where

| where IngestionTime > ago(queryfrequency)

Stage 4: where

| where not(todynamic(AuthenticationDetails)[0].authenticationMethod == "Integrated Windows Authentication")

Stage 5: summarize

| summarize
    DistinctFailureCount = dcountif(UserPrincipalName, ResultType !in (successCodes)),
    DistinctSuccessCount = dcountif(UserPrincipalName, ResultType in (successCodes)),
    SuccessAccounts = make_set_if(UserPrincipalName, ResultType in (successCodes), 250),
    arg_min(TimeGenerated, *)
    by IPAddress

Stage 6: where

| where DistinctFailureCount > DistinctSuccessCount and DistinctFailureCount >= accountthreshold

Stage 7: project

| project TimeGenerated, Category, OperationName, IPAddress, DistinctFailureCount, DistinctSuccessCount, SuccessAccounts, AuthenticationRequirement, ConditionalAccessStatus, IsInteractive, UserAgent, NetworkLocationDetails, DeviceDetail, TokenIssuerType, TokenIssuerName, ResourceIdentity

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AuthenticationDetails[0].authenticationMethodeqIntegrated Windows Authenticationexcludes:AuthenticationDetails[0].authenticationMethod field:"AuthenticationDetails[0].authenticationMethod" value:"Integrated Windows Authentication"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthenticationRequirementproject
Categoryproject
ConditionalAccessStatusproject
DeviceDetailproject
DistinctFailureCountproject
DistinctSuccessCountproject
IPAddressproject
IsInteractiveproject
NetworkLocationDetailsproject
OperationNameproject
ResourceIdentityproject
SuccessAccountsproject
TimeGeneratedproject
TokenIssuerNameproject
TokenIssuerTypeproject
UserAgentproject

Pathlock TDnR - ICM Security Events

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

Detects security events from the SAP Internet Communication Manager (ICM) security log, forwarded by Pathlock Threat Detection and Response. ICM anomalies may indicate SSL/TLS configuration attacks, HTTP request smuggling, or attempts to exploit the ICM layer of SAP web-facing components.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c33
name: Pathlock TDnR - ICM Security Events
kind: Scheduled
description: >-
  Detects security events from the SAP Internet Communication Manager (ICM) security log,
  forwarded by Pathlock Threat Detection and Response. ICM anomalies may indicate SSL/TLS
  configuration attacks, HTTP request smuggling, or attempts to exploit the ICM layer
  of SAP web-facing components.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  Pathlock_TDnR_CL
  | where DataSource == "ICM_SECURITY_LOG"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "ICM_SECURITY_LOG"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Pathlock TDnR - Logical OS Command Changes

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

Detects changes to logical OS command definitions in SAP (transaction SM49), forwarded by Pathlock Threat Detection and Response. Modifications to OS commands may enable arbitrary OS-level code execution within the SAP system context, representing a critical escalation vector.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c38
name: Pathlock TDnR - Logical OS Command Changes
kind: Scheduled
description: >-
  Detects changes to logical OS command definitions in SAP (transaction SM49), forwarded by Pathlock
  Threat Detection and Response. Modifications to OS commands may enable arbitrary OS-level code
  execution within the SAP system context, representing a critical escalation vector.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Persistence
relevantTechniques:
  - T1059
  - T1543
query: |
  Pathlock_TDnR_CL
  | where DataSource == "OS_CMD_CHANGES"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "OS_CMD_CHANGES"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Shadow Credentials Added to Account (Alternative)

#
Group by
DedupEntity
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query searches for modifications to the 'msDS-KeyCredentialLink' property in Active Directory. There are two different events which contain information to detect such changes: 5136 and 4662. This detection uses the 4662, which is an alternative if 5136 is not available.

Known False Positives

  • The property can also be used for legitimate purposes. However, the legitimate use of the property is limited in most environments.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let RuleId = "0297";
let DedupFields = dynamic(["TimeGenerated"]);
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 4662
| where Properties has "5b47d60f-6090-40b2-9f37-2a4de88f3063" // msDS-KeyCredentialLink.
| where Properties has "%%7685" or (binary_and(toint(AccessMask), 0x10) == 0x10) // "Write Property" or "Write Extended Attributes": https://gist.github.com/brianreitz/d5b9397a2e8b3d52ceb9359897e07c3f
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")    
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e // Sorting is required to ensure make_list is deterministic.
    | summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity
// End de-duplication logic.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let RuleId = "0297";
let DedupFields = dynamic(["TimeGenerated"]);

Stage 1: source

SecurityEvent

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where EventID == 4662

Stage 4: where

| where Properties has "5b47d60f-6090-40b2-9f37-2a4de88f3063"

Stage 5: where

| where Properties has "%%7685" or (binary_and(toint(AccessMask), 0x10) == 0x10)

Stage 6: extend (3 consecutive steps)

| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
| extend DedupFieldValues=pack_all()

Stage 7: kusto:mv-apply

| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e
    | summarize DedupValues=make_list(DedupValue)
)

Stage 8: extend

| extend DedupEntity=strcat_array(DedupValues, "|")

Stage 9: project-away

| project-away DedupFieldValues, DedupValues

Stage 10: join (negated)

join kind=leftanti (SecurityAlert) on DedupEntity

Stage 11: summarize aggregation inside the mv-apply branch

summarize

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccessMaskeq
  • 16 transforms: binary_and
field:"AccessMask" kind:eq value:"16"
EventIDeq
  • 4662 corpus 6 (kusto 3, elastic 2, chronicle 1)
field:"EventID" kind:eq value:"4662"
Propertiesmatch
  • %%7685 transforms: term
  • 5b47d60f-6090-40b2-9f37-2a4de88f3063 transforms: term
field:"Properties" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AlertNamematch0297excludes:AlertName field:"AlertName" value:"0297"
ProviderNamematchASIexcludes:ProviderName field:"ProviderName" value:"ASI"

TI map IP entity to Azure Key Vault logs

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

Identifies a match in Azure Key Vault logs from any IP IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 57c7e832-64eb-411f-8928-4133f01f4a25
name: TI map IP entity to Azure Key Vault logs
description: |
  Identifies a match in Azure Key Vault logs from any IP IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureKeyVault
    dataTypes:
      - KeyVaultData
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | 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)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and AzureDiagnostics logs for Key Vault events
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureDiagnostics
        | where ResourceType =~ "VAULTS"
        | where TimeGenerated >= ago(dt_lookBack)
        | extend KeyVaultEvents_TimeGenerated = TimeGenerated, ClientIP = CallerIPAddress
    )
    on $left.TI_ipEntity == $right.ClientIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where KeyVaultEvents_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and ClientIP, and keep the log entry with the latest timestamp
    | summarize KeyVaultEvents_TimeGenerated = arg_max(KeyVaultEvents_TimeGenerated, *) by IndicatorId, ClientIP
    // Select the desired output fields
    | project KeyVaultEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, ClientIP, ResourceId, SubscriptionId, OperationName, ResultType, CorrelationId, id_s, clientInfo_s, httpStatusCode_d,
      identity_claim_appid_g, identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, Type
    // Rename the timestamp field
    | extend timestamp = KeyVaultEvents_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
version: 1.3.4
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

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

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

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

Stage 5: where

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

Stage 6: summarize

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

Stage 7: where

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

Stage 8: join

IP_Indicators
| join kind=innerunique (
      AzureDiagnostics
      | where ResourceType =~ "VAULTS"
      | where TimeGenerated >= ago(dt_lookBack)
      | extend KeyVaultEvents_TimeGenerated = TimeGenerated, ClientIP = CallerIPAddress
  )
  on $left.TI_ipEntity == $right.ClientIP

Stage 9: where

where KeyVaultEvents_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize KeyVaultEvents_TimeGenerated by IndicatorId, ClientIP

Stage 11: project

project ActivityGroupNames, ClientIP, ConfidenceScore, CorrelationId, Description, ExpirationDateTime, IndicatorId, KeyVaultEvents_TimeGenerated, OperationName, ResourceId, ResultType, SubscriptionId, TI_ipEntity, ThreatType, Type, Url, clientInfo_s, httpStatusCode_d, id_s, identity_claim_appid_g, identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g

Stage 12: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
ConfidenceScoreproject
CorrelationIdproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
KeyVaultEvents_TimeGeneratedproject
OperationNameproject
ResourceIdproject
ResultTypeproject
SubscriptionIdproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
clientInfo_sproject
httpStatusCode_dproject
id_sproject
identity_claim_appid_gproject
identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_gproject
timestampextend

UniFi Site Manager: New critical notifications appeared

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

Identifies when the UniFi critical-notification count increases since the previous poll, signaling at least one new critical-severity alarm has appeared.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: e4b75722-7239-f247-558f-d2e851ea0b38
name: "UniFi Site Manager: New critical notifications appeared"
description: |
  Identifies when the UniFi critical-notification count increases since the previous poll, signaling at least one new critical-severity alarm has appeared.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
queryFrequency: 15m
queryPeriod: 45m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1499
subTechniques:
  - T1499.002
query: |
  let prev = Unifi_SiteManager_Sites_CL
      | where TimeGenerated between (ago(30m) .. ago(15m))
      | summarize arg_max(TimeGenerated, *) by tostring(SiteId)
      | project siteId_s = tostring(SiteId), prevCritical = CriticalNotifications;
  Unifi_SiteManager_Sites_CL
  | where TimeGenerated > ago(15m)
  | summarize arg_max(TimeGenerated, *) by tostring(SiteId)
  | extend siteId_s = tostring(SiteId), CriticalCount = CriticalNotifications
  | join kind=inner prev on siteId_s
  | where CriticalCount > prevCritical
  | extend NewCriticals = CriticalCount - prevCritical
  | extend Activity = strcat(NewCriticals, ' new critical notification(s) - count went ', prevCritical, ' -> ', CriticalCount)
  | project TimeGenerated, SiteId, SiteName, Activity, prevCritical, CriticalCount, NewCriticals
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
      - identifier: DnsDomain
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Let Binding: prev used in Stage 5

let prev = Unifi_SiteManager_Sites_CL
    | where TimeGenerated between (ago(30m) .. ago(15m))
    | summarize arg_max(TimeGenerated, *) by tostring(SiteId)
    | project siteId_s = tostring(SiteId), prevCritical = CriticalNotifications;

Stage 1: source

Unifi_SiteManager_Sites_CL

Stage 2: where

| where TimeGenerated > ago(15m)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by tostring(SiteId)

Stage 4: extend

| extend siteId_s = tostring(SiteId), CriticalCount = CriticalNotifications

Stage 5: join

| join kind=inner prev on siteId_s

Stage 6: where

where CriticalCount > prevCritical

Stage 7: extend

extend NewCriticals

Stage 8: extend

extend Activity

Stage 9: project

project Activity, CriticalCount, NewCriticals, SiteId, SiteName, TimeGenerated, prevCritical

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
CriticalCountproject
NewCriticalsproject
SiteIdproject
SiteNameproject
TimeGeneratedproject
prevCriticalproject

Uniqkey - Self-granted privilege or access change

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

Identifies privilege, sharing or group-membership changes in Uniqkey where the actor and the target of the change are the same person. In a healthy administrative process, access changes are made by a different administrator, so a self-referential grant is a strong indicator of insider privilege escalation or a compromised administrator account expanding its own reach.

MITRE ATT&CK Coverage

Rule Body

id: 4b1fefb3-419a-4bbe-8a74-ef8288a8d8d0
name: Uniqkey - Self-granted privilege or access change
version: 1.0.0
kind: Scheduled
description: |-
  Identifies privilege, sharing or group-membership changes in Uniqkey where the actor and the target of the change are the same person. In a healthy administrative process, access changes are made by a different administrator, so a self-referential grant is a strong indicator of insider privilege escalation or a compromised administrator account expanding its own reach.
severity: Medium
requiredDataConnectors:
  - connectorId: UniqkeyEventsConnector
    dataTypes:
      - UniqkeyEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
- Persistence
relevantTechniques:
- T1078
- T1098
query: |-
  UniqkeyEvents_CL
  | where Category in ("privilege_management", "sharing", "group_management")
  | where TargetType == "employee"
  | where (isnotempty(TargetId) and TargetId == ActorId) or (isnotempty(TargetName) and TargetName =~ ActorEmail)
  | project TimeGenerated, ActorEmail, ActorType, Category, Action, ActionId, TargetType, TargetName, ClientSystem, SrcIpAddr
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorEmail
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Stage 1: source

UniqkeyEvents_CL

Stage 2: where

| where Category in ("privilege_management", "sharing", "group_management")

Stage 3: where

| where TargetType == "employee"

Stage 4: where

| where (isnotempty(TargetId) and TargetId == ActorId) or (isnotempty(TargetName) and TargetName =~ ActorEmail)

Stage 5: project

| project TimeGenerated, ActorEmail, ActorType, Category, Action, ActionId, TargetType, TargetName, ClientSystem, SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryin
  • group_management
  • privilege_management
  • sharing
field:"Category" kind:in
TargetIdis_not_null
  • (no value, null check)
field:"TargetId" kind:is_not_null
TargetNameis_not_null
  • (no value, null check)
field:"TargetName" kind:is_not_null
TargetTypeeq
  • employee
field:"TargetType" kind:eq value:"employee"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActionIdproject
ActorEmailproject
ActorTypeproject
Categoryproject
ClientSystemproject
SrcIpAddrproject
TargetNameproject
TargetTypeproject
TimeGeneratedproject

User assigned to a default admin role

#

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

The policy detects users that were assigned to one of the systems default admin roles.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: c04ed74c-3b23-48cd-9c11-fd10cffddc64
name: User assigned to a default admin role
kind: Scheduled
description: The policy detects users that were assigned to one of the systems default admin roles.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "User assigned to a default admin role"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - User assigned to a default admin role
  alertDescriptionFormat: |
    User assigned to a default admin role. The rule detects users that were assigned to one of the systems default admin roles.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "User assigned to a default admin role"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • User assigned to a default admin role transforms: term
field:"policy_name_s" kind:match value:"User assigned to a default admin role"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

User State changed from Guest to Member

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

Detects when a guest account in a tenant is converted to a member of the tenant. Monitoring guest accounts and the access they are provided is important to detect potential account abuse. Accounts converted to members should be investigated to ensure the activity was legitimate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

PlatformRecord / event type
AzureUpdate user

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: a09a0b8e-30fe-4ebf-94a0-cffe50f579cd
name: User State changed from Guest to Member
description: |
  'Detects when a guest account in a tenant is converted to a member of the tenant.
    Monitoring guest accounts and the access they are provided is important to detect potential account abuse.
    Accounts converted to members should be investigated to ensure the activity was legitimate.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
tags:
  - AADSecOpsGuide
query: |
  AuditLogs
    | where OperationName =~ "Update user"
    | where Result =~ "success"
    | mv-expand TargetResources
    | mv-expand TargetResources.modifiedProperties
    | where TargetResources_modifiedProperties.displayName =~ "TargetId.UserType"
    | extend UpdatingAppName = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
    | extend UpdatingServicePrincipalId = tostring(parse_json(tostring(InitiatedBy.app)).servicePrincipalId)
    | extend UpdatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend UpdatingUserAadUserId = tostring(parse_json(tostring(InitiatedBy.user)).id)
    | extend UpdatingUserIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
    | extend UpdatingUser = iif(isnotempty(UpdatingServicePrincipalId), UpdatingServicePrincipalId, UpdatingUserPrincipalName)
    | extend UpdatedUserPrincipalName = tostring(TargetResources.userPrincipalName)
    | project-reorder TimeGenerated, UpdatedUserPrincipalName, UpdatingUser
    | where parse_json(tostring(TargetResources_modifiedProperties.newValue)) =~ "\"Member\"" and parse_json(tostring(TargetResources_modifiedProperties.oldValue)) =~ "\"Guest\""
    | extend InitiatingAccountName = tostring(split(UpdatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(UpdatingUserPrincipalName, "@")[1])
    | extend TargetAccountName = tostring(split(UpdatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(UpdatedUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UpdatingAppName
      - identifier: AadUserId
        columnName: UpdatingServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UpdatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UpdatingUserAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UpdatedUserPrincipalName
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: UpdatingUserIPAddress
version: 1.1.0
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName =~ "Update user"

Stage 3: where

| where Result =~ "success"

Stage 4: mv-expand

| mv-expand TargetResources

Stage 5: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 6: where

| where TargetResources_modifiedProperties.displayName =~ "TargetId.UserType"

Stage 7: extend (7 consecutive steps)

| extend UpdatingAppName = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend UpdatingServicePrincipalId = tostring(parse_json(tostring(InitiatedBy.app)).servicePrincipalId)
| extend UpdatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend UpdatingUserAadUserId = tostring(parse_json(tostring(InitiatedBy.user)).id)
| extend UpdatingUserIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| extend UpdatingUser = iif(isnotempty(UpdatingServicePrincipalId), UpdatingServicePrincipalId, UpdatingUserPrincipalName)
| extend UpdatedUserPrincipalName = tostring(TargetResources.userPrincipalName)

Stage 8: project-reorder

| project-reorder TimeGenerated, UpdatedUserPrincipalName, UpdatingUser

Stage 9: where

| where parse_json(tostring(TargetResources_modifiedProperties.newValue)) =~ "\"Member\"" and parse_json(tostring(TargetResources_modifiedProperties.oldValue)) =~ "\"Guest\""

Stage 10: extend

| extend InitiatingAccountName = tostring(split(UpdatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(UpdatingUserPrincipalName, "@")[1])

Stage 11: extend

| extend TargetAccountName = tostring(split(UpdatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(UpdatedUserPrincipalName, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Update user corpus 4 (sigma 2, kusto 2)
field:"OperationName" kind:eq value:"Update user"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"
TargetResources_modifiedProperties.displayNameeq
  • TargetId.UserType transforms: kusto_dynamic_path
field:"TargetResources_modifiedProperties.displayName" kind:eq value:"TargetId.UserType"
TargetResources_modifiedProperties.newValueeq
  • "Member" transforms: kusto_accessor_expr:{"expression":"parse_json(tostring(TargetResources_modifiedProperties.newValue))","field":"TargetResources_modifiedProperties.newValue"}, kusto_dynamic_path
field:"TargetResources_modifiedProperties.newValue" kind:eq
TargetResources_modifiedProperties.oldValueeq
  • "Guest" transforms: kusto_accessor_expr:{"expression":"parse_json(tostring(TargetResources_modifiedProperties.oldValue))","field":"TargetResources_modifiedProperties.oldValue"}, kusto_dynamic_path
field:"TargetResources_modifiedProperties.oldValue" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
UpdatingAppNameextend
UpdatingServicePrincipalIdextend
UpdatingUserPrincipalNameextend
UpdatingUserAadUserIdextend
UpdatingUserIPAddressextend
UpdatingUserextend
UpdatedUserPrincipalNameextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetAccountUPNSuffixextend

User without MFA

#

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

The policy detects user accounts without mutli-factor authentication

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 71a7b0de-f13d-44b9-9caa-668f1bad0ce6
name: User without MFA
kind: Scheduled
description: The policy detects user accounts without mutli-factor authentication
severity: Medium
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "User without MFA"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - User without MFA
  alertDescriptionFormat: |
    User without MFA. The policy detects user accounts without mutli-factor authentication
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "User without MFA"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • User without MFA transforms: term
field:"policy_name_s" kind:match value:"User without MFA"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

ZeroTrust(TIC3.0) Control Assessment Posture Change

#
Status
available
Severity
medium
Time window
7d
Group by
AssessedResourceId, ControlFamily, RecommendationDisplayName
Source
github.com/Azure/Azure-Sentinel

'Zero Trust(TIC3.0) Control Assessments have Deviated from Configured Threshold Baselines'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 4942992d-a4d3-44b0-9cf4-b5a23811d82d
name: ZeroTrust(TIC3.0) Control Assessment Posture Change
description: |
  'Zero Trust(TIC3.0) Control Assessments have Deviated from Configured Threshold Baselines'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  SecurityRecommendation
  | where RecommendationDisplayName <> ""
  | extend ControlFamily=iff(RecommendationDisplayName has_any("email"), "Email",
      iff(RecommendationDisplayName has_any("apps", "teams", "meeting", "call"), "Unified Communications & Collaboration",
      iff(RecommendationDisplayName has_any("dns", "domain"), "DNS",
      iff(RecommendationDisplayName has_any("endpoint protection", "malware", "file", "files", "IaaSAntimalware"), "Files",
      iff(RecommendationDisplayName has_any("Security Center", "defender", "adaptive", "HoneyTokens", "honey", "deception", "intrusion", "incident", "incidents"), "Intrusion Detection",
      iff(RecommendationDisplayName has_any("firewall", "watcher", "proxy", "certificate", "url", "web"), "Web",
      iff(RecommendationDisplayName has_any("network", "segment", "network security groups", "subnet", "application gateway", "security groups", "IP forwarding", "port", "ports", "networks"), "Networking",
      iff(RecommendationDisplayName has_any("backup", "denial", "DDoS", "load", "scale", "front", "traffic manager", "pool", "disaster", "region", "redundant", "geo"), "Resiliency",
      iff(RecommendationDisplayName has_any("encrypt", "rest", "transit", "data", "http", "https", "TLS", "transfer", "transit", "Secure Socket", "SSH", "just", "FTP", "server-side", "storage", "database", "databases", "SQL", "disk", "disks"), "Data Protection",
      iff(RecommendationDisplayName has_any("private", "vpn", "automation", "playbook", "logic", "notification", "authorized", "safe", "network gateway", "express", "VPC"), "Enterprise",
      iff(RecommendationDisplayName has_any("recover", "log", "configured", "configuration", "identity", "privilege", "admin", "authentication", "JIT", "just", "password", "time", "sync", "vulnerability", "Vulnerabilities", "updates", "update", "upgrade", "audit", "account", "guest", "shared", "access", "machines", "rights", "VM", "key", "keys", "IAM", "EC2", "GuardDuty", "logs", "CloudTrail", "MFA", "External accounts", "accounts", "config", "credentials", "privileged", "owner", "owners", "login", "logon", "virtual machine", "container", "containers", "Kubernetes"), "Universal Security Capabilities", "Other")))))))))))
  | summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName
  | summarize
      Failed=countif(RecommendationState == "Unhealthy"),
      Passed=countif(RecommendationState == "Healthy"),
      Total=countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy")
      by ControlFamily
  | extend PassedControlsPercentage = (Passed / todouble(Total)) * 100
  | extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/5')
  | extend URLCustomEntity = RemediationLink
  | project ControlFamily, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, URLCustomEntity
  | where PassedControlsPercentage < 70 //Adjust PassedRatePercentage Thresholds within Organizational Needs
  | sort by PassedControlsPercentage asc
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityRecommendation

Stage 2: where

| where RecommendationDisplayName <> ""

Stage 3: extend

| extend ControlFamily=iff(RecommendationDisplayName has_any("email"), "Email",
    iff(RecommendationDisplayName has_any("apps", "teams", "meeting", "call"), "Unified Communications & Collaboration",
    iff(RecommendationDisplayName has_any("dns", "domain"), "DNS",
    iff(RecommendationDisplayName has_any("endpoint protection", "malware", "file", "files", "IaaSAntimalware"), "Files",
    iff(RecommendationDisplayName has_any("Security Center", "defender", "adaptive", "HoneyTokens", "honey", "deception", "intrusion", "incident", "incidents"), "Intrusion Detection",
    iff(RecommendationDisplayName has_any("firewall", "watcher", "proxy", "certificate", "url", "web"), "Web",
    iff(RecommendationDisplayName has_any("network", "segment", "network security groups", "subnet", "application gateway", "security groups", "IP forwarding", "port", "ports", "networks"), "Networking",
    iff(RecommendationDisplayName has_any("backup", "denial", "DDoS", "load", "scale", "front", "traffic manager", "pool", "disaster", "region", "redundant", "geo"), "Resiliency",
    iff(RecommendationDisplayName has_any("encrypt", "rest", "transit", "data", "http", "https", "TLS", "transfer", "transit", "Secure Socket", "SSH", "just", "FTP", "server-side", "storage", "database", "databases", "SQL", "disk", "disks"), "Data Protection",
    iff(RecommendationDisplayName has_any("private", "vpn", "automation", "playbook", "logic", "notification", "authorized", "safe", "network gateway", "express", "VPC"), "Enterprise",
    iff(RecommendationDisplayName has_any("recover", "log", "configured", "configuration", "identity", "privilege", "admin", "authentication", "JIT", "just", "password", "time", "sync", "vulnerability", "Vulnerabilities", "updates", "update", "upgrade", "audit", "account", "guest", "shared", "access", "machines", "rights", "VM", "key", "keys", "IAM", "EC2", "GuardDuty", "logs", "CloudTrail", "MFA", "External accounts", "accounts", "config", "credentials", "privileged", "owner", "owners", "login", "logon", "virtual machine", "container", "containers", "Kubernetes"), "Universal Security Capabilities", "Other")))))))))))
ControlFamily =
ifRecommendationDisplayName has "email""Email"
elseiff((RecommendationDisplayName has_any ("apps", "teams", "meeting", "call")), "Unified Communications & Collaboration", iff((RecommendationDisplayName has_any ("dns", "domain")), "DNS", iff((RecommendationDisplayName has_any ("endpoint protection", "malware", "file", "files", "IaaSAntimalware")), "Files", iff((RecommendationDisplayName has_any ("Security Center", "defender", "adaptive", "HoneyTokens", "honey", "deception", "intrusion", "incident", "incidents")), "Intrusion Detection", iff((RecommendationDisplayName has_any ("firewall", "watcher", "proxy", "certificate", "url", "web")), "Web", iff((RecommendationDisplayName has_any ("network", "segment", "network security groups", "subnet", "application gateway", "security groups", "IP forwarding", "port", "ports", "networks")), "Networking", iff((RecommendationDisplayName has_any ("backup", "denial", "DDoS", "load", "scale", "front", "traffic manager", "pool", "disaster", "region", "redundant", "geo")), "Resiliency", iff((RecommendationDisplayName has_any ("encrypt", "rest", "transit", "data", "http", "https", "TLS", "transfer", "transit", "Secure Socket", "SSH", "just", "FTP", "server-side", "storage", "database", "databases", "SQL", "disk", "disks")), "Data Protection", iff((RecommendationDisplayName has_any ("private", "vpn", "automation", "playbook", "logic", "notification", "authorized", "safe", "network gateway", "express", "VPC")), "Enterprise", iff((RecommendationDisplayName has_any ("recover", "log", "configured", "configuration", "identity", "privilege", "admin", "authentication", "JIT", "just", "password", "time", "sync", "vulnerability", "Vulnerabilities", "updates", "update", "upgrade", "audit", "account", "guest", "shared", "access", "machines", "rights", "VM", "key", "keys", "IAM", "EC2", "GuardDuty", "logs", "CloudTrail", "MFA", "External accounts", "accounts", "config", "credentials", "privileged", "owner", "owners", "login", "logon", "virtual machine", "container", "containers", "Kubernetes")), "Universal Security Capabilities", "Other"))))))))))

Stage 4: summarize

| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName

Stage 5: summarize

| summarize
    Failed=countif(RecommendationState == "Unhealthy"),
    Passed=countif(RecommendationState == "Healthy"),
    Total=countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy")
    by ControlFamily

Stage 6: extend (3 consecutive steps)

| extend PassedControlsPercentage = (Passed / todouble(Total)) * 100
| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/5')
| extend URLCustomEntity = RemediationLink

Stage 7: project

| project ControlFamily, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, URLCustomEntity

Stage 8: where

| where PassedControlsPercentage < 70

Stage 9: sort

| sort by PassedControlsPercentage asc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ControlFamilyproject
Failedproject
Passedproject
PassedControlsPercentageproject
RemediationLinkproject
Totalproject
URLCustomEntityproject

Back to all Kusto rules