Detection rules › Kusto
Kusto rules: 2w
AWSCloudTrail - Changes to internet facing AWS RDS Database instances
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Defense Impairment |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventName | in |
| field:"aws::eventName" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | summarize |
AccountName | summarize |
AccountUPNSuffix | summarize |
AdditionalEventData | summarize |
EndTimeUtc | summarize |
EventName | summarize |
EventSource | summarize |
EventTypeName | summarize |
RecipientAccountId | summarize |
ResponseElements | summarize |
SessionMfaAuthenticated | summarize |
SourceIpAddress | summarize |
StartTimeUtc | summarize |
UserAgent | summarize |
UserIdentityAccountId | summarize |
UserIdentityPrincipalid | summarize |
UserIdentityUserName | summarize |
Azure DevOps Audit Detection for known malicious tooling
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
UserAgent | match |
| field:"aws::userAgent" kind:match value:"ADOKit" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC9a" |
Box - New external user
#'Detects when new user created with SourceLogin containing non-corporate domain.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"NEW_USER" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
corp_domain | extend |
new_domain | extend |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Cross-Cloud Password Spray detection
#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
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Sign-in naming a target application |
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS Console Login (Panther)
- AWS Console Login Failed During MFA Challenge (Splunk)
- AWS ConsoleLogin Failed Authentication (Sigma)
- AWS CreateLoginProfile (Splunk)
- AWS Credential Access Failed Login (Splunk)
- AWS High Number Of Failed Authentications For User (Splunk)
- AWS High Number Of Failed Authentications From Ip (Splunk)
- AWS Multiple Failed MFA Requests For User (Splunk)
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
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.
| Field | Source |
|---|---|
AuthenticationRequirement | summarize |
AzureFailedEvent | summarize |
AzureFailedEventsCount | summarize |
AzureSetOfFailedevents | summarize |
AzureSetOfSuccessfulEvents | summarize |
AzureSuccessfulEvent | summarize |
AzureSuccessfuleventsCount | summarize |
ClientAppUsed | summarize |
ConditionalAccessStatus | summarize |
IPAddress | summarize |
OperationName | summarize |
RiskDetail | summarize |
UserAgent | summarize |
UserPrincipalName | summarize |
totalAzureLoginEventId | summarize |
Name | extend |
UPNSuffix | extend |
Discord CDN Risky File Download
#'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
| Tactic | Techniques |
|---|---|
| Command & Control |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceAction | ne |
| field:"DeviceAction" kind:ne value:"blocked" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"ZScaler" |
RequestURL | match |
| field:"RequestURL" kind:match |
dcount_RequestURL | le |
| field:"dcount_RequestURL" kind:le value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceActionTaken | project |
DeviceProduct | project |
EndTime | project |
RequestURL | project |
SourceIP | project |
SourceUser | project |
StartTime | project |
Files Copied to USB Drives
#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
| Tactic | Techniques |
|---|---|
| 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
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 =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq |
FolderPath | starts_with |
| field:"FolderPath" kind:starts_with value:"DriveLetter" |
InitiatingProcessAccountName | ne |
| field:"InitiatingProcessAccountName" kind:ne value:"system" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
FolderPath | starts_with | C:\ | excludes:FolderPath field:"FolderPath" value:"C:\" |
FolderPath | starts_with | \ | excludes:FolderPath field:"FolderPath" value:"\" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | project |
FileName | project |
FolderPath | project |
InitiatingProcessAccountDomain | project |
InitiatingProcessAccountName | project |
InitiatingProcessAccountUpn | project |
IsAzureInfoProtectionApplied | project |
ReportId | project |
SHA256 | project |
SensitivityLabel | project |
TimeGenerated | project |
HostName | extend |
DnsDomain | extend |
FileHashAlgorithm | extend |
Google Threat Intelligence - Threat Hunting Hash
#Google Threat Intelligence hash correlation.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Hash | is_not_null | field:"Hash" kind:is_not_null | |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | contains |
| field:"ObservableKey" kind:contains value:"file:hashes" |
ObservableValue | is_not_null | field:"ObservableValue" kind:is_not_null | |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"Google Threat Intelligence" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Id | summarize |
Hunt for public facing devices via public tag
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceInfo action any: Device information |
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.
| Field | Source |
|---|---|
DeviceName | project |
InternetFacingLastSeen | project |
InternetFacingLocalIp | project |
InternetFacingLocalPort | project |
InternetFacingPublicScannedIp | project |
InternetFacingPublicScannedPort | project |
InternetFacingReason | project |
InternetFacingTransportProtocol | project |
Lumen TI domain in DnsEvents
#This query searches for matches between Lumen threat intelligence domain indicators and DnsEvents.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | eq |
| field:"ObservableKey" kind:eq value:"domain-name:value" |
ObservableValue | contains |
| field:"ObservableValue" kind:contains value:"." |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"Lumen" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | project |
Confidence | project |
DNS_domainEntity | project |
EndTime | project |
Id | project |
Name | project |
QueryType | project |
StartTime | project |
TI_domainEntity | project |
Tags | project |
Type | project |
ValidUntil | project |
timestamp | project |
Lumen TI IPAddress in SecurityEvents
#This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SecurityEvents.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | eq |
| field:"ObservableKey" kind:eq value:"ipv4-addr:value" |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"Lumen" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
Computer | project |
Confidence | project |
EndTime | project |
EventID | project |
Id | project |
IpAddress | project |
SE_ipEntity | project |
StartTime | project |
TI_ipEntity | project |
Tags | project |
Type | project |
ValidUntil | project |
timestamp | project |
Palo Alto - possible nmap scan on with top 100 option
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationPort | in |
| field:"DestinationPort" kind:in |
SourceIP | cidr_match |
| field:"SourceIP" kind:cidr_match |
dcount_DestinationPort | gt |
| field:"dcount_DestinationPort" kind:gt value:"90" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
DestinationIP | summarize |
EndTime | summarize |
SourceIP | summarize |
StartTime | summarize |
Password Spray
#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
| Tactic | Techniques |
|---|---|
| Credential Access | No 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Dcount | ge |
| field:"Dcount" kind:ge value:"2" |
Dcount | gt |
| field:"Dcount" kind:gt value:"2" |
EventID | in |
| field:"EventID" kind:in |
RemoteIP | is_not_null | field:"RemoteIP" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IpAddress | in | -, 127.0.0.1, ::1 | excludes:IpAddress field:"IpAddress" value:"-" field:"IpAddress" value:"127.0.0.1" field:"IpAddress" value:"::1" |
RemoteIP | in | -, 127.0.0.1, ::1 | excludes:RemoteIP field:"RemoteIP" value:"-" field:"RemoteIP" value:"127.0.0.1" field:"RemoteIP" value:"::1" |
Password spray attack against ADFSSignInLogs
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DistinctFailureCount | ge |
| field:"DistinctFailureCount" kind:ge value:"10" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AuthenticationDetails[0].authenticationMethod | eq | Integrated Windows Authentication | excludes:AuthenticationDetails[0].authenticationMethod field:"AuthenticationDetails[0].authenticationMethod" value:"Integrated Windows Authentication" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AuthenticationRequirement | project |
Category | project |
ConditionalAccessStatus | project |
DeviceDetail | project |
DistinctFailureCount | project |
DistinctSuccessCount | project |
IPAddress | project |
IsInteractive | project |
NetworkLocationDetails | project |
OperationName | project |
ResourceIdentity | project |
SuccessAccounts | project |
TimeGenerated | project |
TokenIssuerName | project |
TokenIssuerType | project |
UserAgent | project |
Pathlock TDnR - ICM Security Events
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"ICM_SECURITY_LOG" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Pathlock TDnR - Logical OS Command Changes
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Persistence |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"OS_CMD_CHANGES" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Shadow Credentials Added to Account (Alternative)
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
References
- https://docs.microsoft.com/en-us/defender-for-identity/configure-windows-event-collection
- https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4662
- https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab
- https://github.com/ShutdownRepo/pywhisker
- https://github.com/MichaelGrafnetter/DSInternals
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4662: An operation was performed on an object. |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AccessMask | eq |
| field:"AccessMask" kind:eq value:"16" |
EventID | eq |
| field:"EventID" kind:eq value:"4662" |
Properties | match |
| field:"Properties" kind:match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AlertName | match | 0297 | excludes:AlertName field:"AlertName" value:"0297" |
ProviderName | match | ASI | excludes:ProviderName field:"ProviderName" value:"ASI" |
TI map IP entity to Azure Key Vault logs
#Identifies a match in Azure Key Vault logs from any IP IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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 =isnotempty(NetworkIP)NetworkIPNetworkDestinationIPStage 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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ClientIP | project |
ConfidenceScore | project |
CorrelationId | project |
Description | project |
ExpirationDateTime | project |
IndicatorId | project |
KeyVaultEvents_TimeGenerated | project |
OperationName | project |
ResourceId | project |
ResultType | project |
SubscriptionId | project |
TI_ipEntity | project |
ThreatType | project |
Type | project |
Url | project |
clientInfo_s | project |
httpStatusCode_d | project |
id_s | project |
identity_claim_appid_g | project |
identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g | project |
timestamp | extend |
UniFi Site Manager: New critical notifications appeared
#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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Source |
|---|---|
Activity | project |
CriticalCount | project |
NewCriticals | project |
SiteId | project |
SiteName | project |
TimeGenerated | project |
prevCritical | project |
Uniqkey - Self-granted privilege or access change
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | in |
| field:"Category" kind:in |
TargetId | is_not_null | field:"TargetId" kind:is_not_null | |
TargetName | is_not_null | field:"TargetName" kind:is_not_null | |
TargetType | eq |
| field:"TargetType" kind:eq value:"employee" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
ActionId | project |
ActorEmail | project |
ActorType | project |
Category | project |
ClientSystem | project |
SrcIpAddr | project |
TargetName | project |
TargetType | project |
TimeGenerated | project |
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.
The policy detects users that were assigned to one of the systems default admin roles.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"User assigned to a default admin role" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
User State changed from Guest to Member
#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
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Update user |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
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.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UpdatingAppName | extend |
UpdatingServicePrincipalId | extend |
UpdatingUserPrincipalName | extend |
UpdatingUserAadUserId | extend |
UpdatingUserIPAddress | extend |
UpdatingUser | extend |
UpdatedUserPrincipalName | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
TargetAccountName | extend |
TargetAccountUPNSuffix | extend |
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.
The policy detects user accounts without mutli-factor authentication
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"User without MFA" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
ZeroTrust(TIC3.0) Control Assessment Posture Change
#'Zero Trust(TIC3.0) Control Assessments have Deviated from Configured Threshold Baselines'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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 =RecommendationDisplayName has "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"))))))))))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.
| Field | Kind | Values | Search |
|---|---|---|---|
PassedControlsPercentage | lt |
| field:"PassedControlsPercentage" kind:lt value:"70" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ControlFamily | project |
Failed | project |
Passed | project |
PassedControlsPercentage | project |
RemediationLink | project |
Total | project |
URLCustomEntity | project |