Detection rules › Kusto
Kusto rules: 0j
[Entra ID] Suspicious Continuous OAuth Token Usage
#Detects repeated use of the same OAuth token across different IPs or locations over time. This can indicate token theft or session abuse.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Success: Successful sign-in |
| Entra ID | SigninLogs sign-in event: Successful sign-in |
Rules Detecting the Same Action
These rules filter on the same operation.
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
- Detect non-admin requesting token for admin applications (Kusto)
Rule Body
id: 67802748-435b-4f80-9f61-b9a9ac6ea15c
name: "[Entra ID] Suspicious Continuous OAuth Token Usage"
version: 1.0.0
kind: Scheduled
description: |
Detects repeated use of the same OAuth token across different IPs or locations over time. This can indicate token theft or session abuse.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1606
query: |
// =====================
// Low-noise Token Reuse Detector (fixed timespan calc)
// =====================
let GapDays = 1;
let MinIPChanges = 2;
let MinLocChanges = 2;
let ExcludeApps = dynamic([
"Microsoft Authentication Broker",
"Microsoft Teams",
"Office 365"
]);
let Src =
union isfuzzy=true
AADNonInteractiveUserSignInLogs,
SigninLogs
| where ResultType == 0
| where isnotempty(UniqueTokenIdentifier);
let Past =
Src
| where TimeGenerated between (ago(14d) .. ago(1d))
| summarize
PastLastSeen = max(TimeGenerated),
PastUPNs = make_set(UserPrincipalName, 20),
PastIPs = make_set(IPAddress, 50),
PastLocs = make_set(tostring(Location), 50),
PastApps = make_set(AppDisplayName, 50)
by UniqueTokenIdentifier;
let Last24h =
Src
| where TimeGenerated >= ago(1d)
//| where AppDisplayName !in (ExcludeApps)
| summarize
CurrentFirstSeen = min(TimeGenerated),
CurrentLastSeen = max(TimeGenerated),
CurrentUPNs = make_set(UserPrincipalName, 20),
CurrentIPs = make_set(IPAddress, 50),
CurrentLocs = make_set(tostring(Location), 50),
CurrentApps = make_set(AppDisplayName, 50),
IPCount = dcount(IPAddress),
LocCount = dcount(tostring(Location))
by UniqueTokenIdentifier, ResultType;
Last24h
| join kind=inner Past on UniqueTokenIdentifier
| extend Gap = CurrentFirstSeen - PastLastSeen
| extend GapThreshold = totimespan(strcat(GapDays, "d"))
| where Gap >= GapThreshold
| where IPCount >= MinIPChanges or LocCount >= MinLocChanges
| extend NewIPs = set_difference(CurrentIPs, PastIPs)
| extend NewLocs = set_difference(CurrentLocs, PastLocs)
| where array_length(NewIPs) > 0 or array_length(NewLocs) > 0
| extend
CurrentUPNCount = array_length(CurrentUPNs),
PastUPNCount = array_length(PastUPNs)
| project
UniqueTokenIdentifier,
PastLastSeen,
CurrentFirstSeen,
CurrentLastSeen,
Gap,
GapThreshold,
CurrentUPNs,
CurrentUPNCount,
CurrentIPs,
IPCount,
NewIPs,
CurrentLocs,
LocCount,
NewLocs,
CurrentApps,
PastApps,
PastUPNs,
ResultType
| order by Gap desc, CurrentLastSeen desc
| extend UserNames = strcat_array(CurrentUPNs, ",")
| extend NewIP = strcat_array(NewIPs, ",")
| extend FirstNewIP = tostring(NewIPs[0])
| extend NewLoc = strcat_array(NewLocs, ",")
| extend PastUPN = strcat_array(PastUPNs, ",")
| extend Source_Network_IPLocation = ""
| project
Alert_Time_TW = datetime_utc_to_local(CurrentLastSeen, 'Asia/Taipei'),
Alert_Time_UTC0 = CurrentLastSeen,
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Network Access User",
Alert_Name_en = "Abnormal Sign-in Token Reuse",
Alert_Description_en=strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(CurrentLastSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
", detected suspected Token Reuse behavior (UniqueTokenIdentifier appeared repeatedly and the interval reached the threshold).",
"Token:",
iff(isnotempty(UniqueTokenIdentifier), UniqueTokenIdentifier, "<NoTokenId>"),
", users (last 2 days): ",
iff(isnotempty(tostring(UserNames)), tostring(UserNames), "<NoUserNames>"),
", previous last seen: ",
format_datetime(datetime_utc_to_local(PastLastSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
", current first seen: ",
format_datetime(datetime_utc_to_local(CurrentFirstSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
", gap: ",
tostring(Gap),
" days",
", IP count: ",
tostring(IPCount),
", current sign-in IP: ",
iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIP>"),
", location count: ",
tostring(LocCount),
", current sign-in location: ",
iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLoc>"),
"."
),
Alert_TriageStep_en=strcat(
" 1. Check whether this is truly token reuse. The reused IP is: ",
iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIPs>"),
", current sign-in location: ",
iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLocs>"),
" to determine whether it is an uncommon source, cross-country/cross-region, or anonymous cloud egress.",
" 2. Check whether multiple accounts share the same token. Current triggering account count: ",
tostring(CurrentUPNCount),
", accounts that previously triggered this token: ",
tostring(PastUPN),
"; if the UPN count is greater than 1, prioritize suspicion of token leakage or proxy/automation abuse.",
" 3. Check the applications currently involved in sign-in: ",
iff(isnotempty(tostring(CurrentApps)), tostring(CurrentApps), "<NoCurrentApps>"),
"previous sign-in applications: ",
iff(isnotempty(tostring(PastApps)), tostring(PastApps), "<NoPastApps>"),
" to determine whether they include administrative/highly sensitive applications or abnormally newly added apps."
),
Alert_Containment_en=strcat(
"1. Immediately revoke sign-in tokens/sessions and force re-sign-in for users (last 2 days): ",
iff(isnotempty(tostring(UserNames)), tostring(UserNames), "<NoUserNames>"),
" to block continued access using the reused token. ",
"2. If the current sign-in IP or location is abnormal, immediately block the current sign-in IP: ",
iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIP>"),
", current sign-in location: ",
iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLoc>"),
", and tighten Conditional Access (MFA/compliant device/named location). ",
"3. Immediately require account security recovery: reset the password, re-register MFA, and check for suspicious devices or added authentication methods. "
),
Alert_Remediation_en=strcat(
"1. Strengthen risk-based access: establish Conditional Access policies to block new IPs/new locations. ",
"2. Implement token protection and endpoint governance: promote compliant devices, reduce long-lived token risk, and restrict access from unmanaged devices or legacy clients. ",
"3. Inventory automation and applications: regularly check for abnormally added applications and high-privilege applications, and remove unnecessary permissions and old credentials. ",
"4. Establish automated response: automatically revoke tokens, block IPs, notify users for confirmation, and create incident tickets for follow-up investigation when alerts trigger."
),
Event_Code = ResultType,
//Event_Description = ResultDescription,
Event_TimeRange_Start_UTC0 = CurrentFirstSeen,
Event_TimeRange_End_UTC0 = CurrentLastSeen,
Event_TimeRange_Start_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),
Event_TimeRange_End_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),
Source_Identity_FullName = UserNames,
Source_Network_IPAddress = NewIP,
Source_Network_IPLocation = Source_Network_IPLocation,
Target_Identity_FullName = UniqueTokenIdentifier,
//Target_Network_IPAddress = UniqueTokenIdentifier,
//Target_Resource_ID = "",
Target_Resource_Name = "Microsoft Entra ID",
Target_Resource_Type = "Microsoft Entra ID"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Source_Identity_FullName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Source_Network_IPAddress
alertDetailsOverride:
alertDynamicProperties: []
Stages and Predicates
Parameters
let GapDays = 1;
let MinIPChanges = 2;
let MinLocChanges = 2;
let Src and let Last24h are inlined into the numbered stages below.
Let Binding: ExcludeApps
let ExcludeApps = dynamic([
"Microsoft Authentication Broker",
"Microsoft Teams",
"Office 365"
]);
Let Binding: Past
let Past = Src
| where TimeGenerated between (ago(14d) .. ago(1d))
| summarize
PastLastSeen = max(TimeGenerated),
PastUPNs = make_set(UserPrincipalName, 20),
PastIPs = make_set(IPAddress, 50),
PastLocs = make_set(tostring(Location), 50),
PastApps = make_set(AppDisplayName, 50)
by UniqueTokenIdentifier;
Stages 1 to 7 define let Last24h as the rule's main pipeline source. Stages 8 to 20 use it.
Stage 1: union
union of 2 branches
Stage 2: source
AADNonInteractiveUserSignInLogs
Stage 3: source
SigninLogs
Stage 4: where
where ResultType == 0
Stage 5: where
where isnotempty(UniqueTokenIdentifier)
Stage 6: where
where TimeGenerated >= ago(86400s)
Stage 7: summarize
summarize CurrentApps, CurrentFirstSeen, CurrentIPs, CurrentLastSeen, CurrentLocs, CurrentUPNs, IPCount, LocCount by UniqueTokenIdentifier, ResultType
Stage 8: join
join kind=inner (Past) on UniqueTokenIdentifier
Stage 9: extend
extend Gap
Stage 10: extend
extend GapThreshold
Stage 11: where
where Gap >= GapThreshold
Stage 12: where
where (IPCount >= 2 or LocCount >= 2)
Stage 13: extend
extend NewIPs
Stage 14: extend
extend NewLocs
Stage 15: where
where (NewIPs > 0 or NewLocs > 0)
Stage 16: extend
extend CurrentUPNCount, PastUPNCount
Stage 17: project
project CurrentApps, CurrentFirstSeen, CurrentIPs, CurrentLastSeen, CurrentLocs, CurrentUPNCount, CurrentUPNs, Gap, GapThreshold, IPCount, LocCount, NewIPs, NewLocs, PastApps, PastLastSeen, PastUPNs, ResultType, UniqueTokenIdentifier
Stage 18: sort
sort by CurrentLastSeen, Gap
Stage 19: extend (6 consecutive steps)
extend FirstNewIP, NewIP, NewLoc, PastUPN, Source_Network_IPLocation, UserNames
Stage 20: project
project Alert_Category_en, Alert_Containment_en, Alert_Description_en, Alert_Name_en, Alert_Remediation_en, Alert_SubCategory_en, Alert_Time_TW, Alert_Time_UTC0, Alert_TriageStep_en, Event_Code, Event_TimeRange_End_TW, Event_TimeRange_End_UTC0, Event_TimeRange_Start_TW, Event_TimeRange_Start_UTC0, Source_Identity_FullName, Source_Network_IPAddress, Source_Network_IPLocation, Target_Identity_FullName, Target_Resource_Name, Target_Resource_Type
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Alert_Category_en | project |
Alert_Containment_en | project |
Alert_Description_en | project |
Alert_Name_en | project |
Alert_Remediation_en | project |
Alert_SubCategory_en | project |
Alert_Time_TW | project |
Alert_Time_UTC0 | project |
Alert_TriageStep_en | project |
Event_Code | project |
Event_TimeRange_End_TW | project |
Event_TimeRange_End_UTC0 | project |
Event_TimeRange_Start_TW | project |
Event_TimeRange_Start_UTC0 | project |
Source_Identity_FullName | project |
Source_Network_IPAddress | project |
Source_Network_IPLocation | project |
Target_Identity_FullName | project |
Target_Resource_Name | project |
Target_Resource_Type | project |
AWS Security Hub - Detect CloudTrail trails lacking KMS encryption
#This query detects AWS CloudTrail trails that are not configured to use server-side encryption with a customer managed KMS key using AWS Security Hub control CloudTrail.2 findings. Unencrypted CloudTrail logs increase the risk of unauthorized access to sensitive audit data at rest.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Rule Body
id: 9c2f6c3b-7fd8-4c5a-9d9d-3c4f9e6a7b21
name: AWS Security Hub - Detect CloudTrail trails lacking KMS encryption
description: |
This query detects AWS CloudTrail trails that are not configured to use server-side encryption with a customer managed KMS key using AWS Security Hub control CloudTrail.2 findings.
Unencrypted CloudTrail logs increase the risk of unauthorized access to sensitive audit data at rest.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWSSecurityHub
dataTypes:
- AWSSecurityHubFindings
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
- DefenseEvasion
relevantTechniques:
- T1565.001
- T1562.008
tags:
- CIS AWS Foundations Benchmark v1.4.0
- CIS AWS Foundations Benchmark v1.2.0
- NIST 800-53 r5
- PCI DSS v3.2.1
query: |
AWSSecurityHubFindings
| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
| where tostring(AwsSecurityFindingGeneratorId) == "security-control/CloudTrail.2"
or tostring(ComplianceSecurityControlId) == "CloudTrail.2"
| mv-expand Resource = Resources
| where tostring(Resource.Type) == "AwsCloudTrailTrail"
| extend TrailId = tostring(Resource.Id)
| summarize TimeGenerated = max(TimeGenerated)
by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
AwsSecurityFindingId, ComplianceSecurityControlId, TrailId
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AwsAccountId
- identifier: CloudAppAccountId
columnName: AwsAccountId
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: TrailId
customDetails:
ComplianceControlId: ComplianceSecurityControlId
Region: AwsRegion
FindingId: AwsSecurityFindingId
alertDetailsOverride:
alertDisplayNameFormat: "AWS CloudTrail trail {{TrailId}} lacks KMS encryption"
alertDescriptionFormat: "AWS CloudTrail trail ({{TrailId}}) lacks customer-managed KMS encryption for Account {{AwsAccountId}}."
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSSecurityHubFindings
Stage 2: where
| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
Stage 3: where
| where tostring(AwsSecurityFindingGeneratorId) == "security-control/CloudTrail.2"
or tostring(ComplianceSecurityControlId) == "CloudTrail.2"
Stage 4: mv-expand
| mv-expand Resource = Resources
Stage 5: where
| where tostring(Resource.Type) == "AwsCloudTrailTrail"
Stage 6: extend
| extend TrailId = tostring(Resource.Id)
Stage 7: summarize
| summarize TimeGenerated = max(TimeGenerated)
by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
AwsSecurityFindingId, ComplianceSecurityControlId, TrailId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AwsSecurityFindingGeneratorId | eq |
| field:"AwsSecurityFindingGeneratorId" kind:eq value:"security-control/CloudTrail.2" |
ComplianceSecurityControlId | eq |
| field:"ComplianceSecurityControlId" kind:eq value:"CloudTrail.2" |
ComplianceStatus | eq |
| field:"ComplianceStatus" kind:eq value:"FAILED" |
RecordState | eq |
| field:"RecordState" kind:eq value:"ACTIVE" |
Resource.Type | eq |
| field:"Resource.Type" kind:eq value:"AwsCloudTrailTrail" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AwsAccountId | summarize |
AwsRegion | summarize |
AwsSecurityFindingDescription | summarize |
AwsSecurityFindingId | summarize |
AwsSecurityFindingTitle | summarize |
ComplianceSecurityControlId | summarize |
TimeGenerated | summarize |
TrailId | summarize |
AWSCloudTrail - Failed Attempts to Change AWS CloudTrail Logs
#Identifies FAILED or denied attempts to manipulate AWS CloudTrail, CloudWatch/EventBridge, or VPC Flow Logs. Successful manipulation is covered by the higher-severity 'AWSCloudTrail - Successful Tampering with AWS CloudTrail Logs' rule. These failed attempts can indicate reconnaissance or an actor probing for insufficient permissions as part of defense evasion. For more information, visit: AWS CloudTrail API: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html AWS CloudWatch/EventBridge API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_Operations.html AWS DeleteFlowLogs API: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteFlowLogs.html
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- A CloudTrail Was Created or Updated (Panther)
- Account Security Configuration Changed (Panther)
- ASL AWS Defense Evasion Delete Cloudtrail (Splunk)
- ASL AWS Defense Evasion Delete CloudWatch Log Group (Splunk)
- ASL AWS Defense Evasion Stop Logging Cloudtrail (Splunk)
- ASL AWS Defense Evasion Update Cloudtrail (Splunk)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWS VPC Flow Logs Deleted (Sigma)
Rule Body
id: 610d3850-c26f-4f20-8d86-f10fdf2425f5
name: AWSCloudTrail - Failed Attempts to Change AWS CloudTrail Logs
description: |
Identifies FAILED or denied attempts to manipulate AWS CloudTrail, CloudWatch/EventBridge, or VPC Flow Logs.
Successful manipulation is covered by the higher-severity 'AWSCloudTrail - Successful Tampering with AWS CloudTrail Logs' rule.
These failed attempts can indicate reconnaissance or an actor probing for insufficient permissions as part of defense evasion.
For more information, visit:
AWS CloudTrail API: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html
AWS CloudWatch/EventBridge API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_Operations.html
AWS DeleteFlowLogs API: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteFlowLogs.html
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AWSS3
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562.008
query: |
let EventNameList = dynamic(["UpdateTrail","DeleteTrail","StopLogging","DeleteFlowLogs","DeleteEventBus","DeleteLogGroup"]);
AWSCloudTrail
| where (EventName in~ (EventNameList) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).enableLogFileValidation) == false) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).isMultiRegionTrail) == false)) and (isnotempty(ErrorMessage) or isnotempty(ErrorCode))
| 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
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
alertDetailsOverride:
alertDisplayNameFormat: "AWS CloudTrail log manipulation FAILED attempt: {{EventName}} by {{AccountName}} from {{SourceIpAddress}}"
alertDescriptionFormat: "Failed event {{EventName}} occurred in {{AWSRegion}} for account {{RecipientAccountId}}"
customDetails:
EventName: EventName
EventTypeName: EventTypeName
EventSource: EventSource
AWSRegion: AWSRegion
UserAgent: UserAgent
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let EventNameList = dynamic(["UpdateTrail","DeleteTrail","StopLogging","DeleteFlowLogs","DeleteEventBus","DeleteLogGroup"]);
Stage 1: source
AWSCloudTrail
Stage 2: where
| where (EventName in~ (EventNameList) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).enableLogFileValidation) == false) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).isMultiRegionTrail) == false)) and (isnotempty(ErrorMessage) or isnotempty(ErrorCode))
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
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorCode | is_not_null | field:"ErrorCode" kind:is_not_null | |
ErrorMessage | is_not_null | field:"ErrorMessage" kind:is_not_null | |
EventName | eq |
| field:"EventName" kind:eq value:"UpdateTrail" |
EventName | in |
| field:"EventName" kind:in |
RequestParameters.enableLogFileValidation | eq |
| field:"RequestParameters.enableLogFileValidation" kind:eq value:"false" |
RequestParameters.isMultiRegionTrail | eq |
| field:"RequestParameters.isMultiRegionTrail" kind:eq value:"false" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | summarize |
AccountName | summarize |
AccountUPNSuffix | summarize |
EndTimeUtc | summarize |
EventName | summarize |
EventSource | summarize |
EventTypeName | summarize |
RecipientAccountId | summarize |
SessionMfaAuthenticated | summarize |
SourceIpAddress | summarize |
StartTimeUtc | summarize |
UserAgent | summarize |
UserIdentityAccountId | summarize |
UserIdentityPrincipalid | summarize |
UserIdentityUserName | summarize |
Azure Machine Learning Write Operations
#'Shows the most prevalent users who perform write operations on Azure Machine Learning resources. List the common source IP address for each of those accounts. If an operation is not from those IP addresses, it may be worthy of investigation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Impact |
Rule Body
id: 68c89998-8052-4c80-a1f6-9d81060b6d57
name: Azure Machine Learning Write Operations
description: |
'Shows the most prevalent users who perform write operations on Azure Machine Learning resources. List the common source IP address for each of those accounts. If an operation is not from those IP addresses, it may be worthy of investigation.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
tactics:
- InitialAccess
- Execution
- Impact
relevantTechniques:
- T1078
- T1059
- T1496
queryFrequency: 1d # Added queryFrequency property
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
query: |
AzureActivity
| where ResourceProviderValue == "MICROSOFT.MACHINELEARNINGSERVICES" // Filter activities related to Microsoft Machine Learning Services
| extend SCOPE = tostring(parse_json(Authorization).scope) // Parse Authorization scope as string
| extend subname = split(Hierarchy, "/") // Split Hierarchy to extract Subscription Name and ID
| extend ['Subscription Name'] = subname[-2], ['Subscription ID'] = subname[-1] // Extract Subscription Name and ID
| extend Properties = parse_json(Properties) // Parse Properties as JSON
| extend Properties_entity = tostring(Properties.entity) // Cast Properties.entity to string
| where isnotempty(Properties_entity) // Filter activities where Properties.entity is not empty
// | where Properties_entity contains "deepseek" // Filter activities where Properties.entity contains "deepseek"
| where OperationNameValue contains "write" // Filter activities where OperationNameValue contains "write"
| where OperationNameValue !contains "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE" // Exclude role assignments
| extend LLM = tostring(split(Properties_entity, "/")[-1]) // Extract the last segment of Properties_entity and cast it to string
| distinct TimeGenerated, tostring(['Subscription Name']), ResourceGroup, tostring(['Subscription ID']), Caller, CallerIpAddress, OperationNameValue, LLM, _ResourceId // Select distinct relevant fields for output
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Caller
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: _ResourceId
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
AzureActivity
Stage 2: where
| where ResourceProviderValue == "MICROSOFT.MACHINELEARNINGSERVICES"
Stage 3: extend (5 consecutive steps)
| extend SCOPE = tostring(parse_json(Authorization).scope)
| extend subname = split(Hierarchy, "/")
| extend ['Subscription Name'] = subname[-2], ['Subscription ID'] = subname[-1]
| extend Properties = parse_json(Properties)
| extend Properties_entity = tostring(Properties.entity)
Stage 4: where
| where isnotempty(Properties_entity)
Stage 5: where
| where OperationNameValue contains "write"
Stage 6: where
| where OperationNameValue !contains "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
Stage 7: extend
| extend LLM = tostring(split(Properties_entity, "/")[-1])
Stage 8: distinct
| distinct TimeGenerated, tostring(['Subscription Name']), ResourceGroup, tostring(['Subscription ID']), Caller, CallerIpAddress, OperationNameValue, LLM, _ResourceId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationNameValue | contains |
| field:"azure_ad::operation_name_value" kind:contains value:"write" |
Properties_entity | is_not_null | field:"Properties_entity" kind:is_not_null | |
ResourceProviderValue | eq |
| field:"ResourceProviderValue" kind:eq value:"MICROSOFT.MACHINELEARNINGSERVICES" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
OperationNameValue | contains | MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE | excludes:OperationNameValue field:"OperationNameValue" value:"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SCOPE | extend |
subname | extend |
Subscription ID | extend |
Subscription Name | extend |
Properties | extend |
Properties_entity | extend |
LLM | extend |
Bitglass - Multiple failed logins
#'Detects multiple failed logins.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 7c570bfc-9f20-490e-80e8-b898c7ce4bda
name: Bitglass - Multiple failed logins
description: |
'Detects multiple failed logins.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: Bitglass
dataTypes:
- Bitglass
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let threshold = 10;
Bitglass
| where EventType =~ 'access'
| where EventResultDetails =~ 'Failed login attempt.'
| summarize count() by User, bin(TimeGenerated, 10m)
| where count_ >= threshold
| extend AccountCustomEntity = User
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
Bitglass
Stage 2: where
| where EventType =~ 'access'
Stage 3: where
| where EventResultDetails =~ 'Failed login attempt.'
Stage 4: summarize
| summarize count() by User, bin(TimeGenerated, 10m)
Stage 5: where
| where count_ >= threshold
Stage 6: extend
| extend AccountCustomEntity = User
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResultDetails | eq |
| field:"EventResultDetails" kind:eq value:"Failed login attempt." |
EventType | eq |
| field:"EventType" kind:eq value:"access" |
count_ | ge |
| field:"count_" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
User | summarize |
AccountCustomEntity | extend |
Disabling Security Services via Registry
#This query checks for processes modifying the registry to disable security features. This is a common technique used by threat actors for defence evasion.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 32b29155-3fd3-4a9e-a0ca-a67e2593b60b
name: Disabling Security Services via Registry
description: |
This query checks for processes modifying the registry to disable security features. This is a common technique used by threat actors for defence evasion.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
DeviceProcessEvents
| where InitiatingProcessCommandLine has_all(@'"reg"', 'add', @'"HKLM\SOFTWARE\Policies\', '/v','/t', 'REG_DWORD', '/d', '/f')
and InitiatingProcessCommandLine has_any('DisableRealtimeMonitoring', 'UseTPMKey', 'UseTPMKeyPIN', 'UseAdvancedStartup',
'EnableBDEWithNoTPM', 'RecoveryKeyMessageSource')
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountUpn
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Process
fieldMappings:
- identifier: ProcessId
columnName: ProcessId
- identifier: CommandLine
columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where InitiatingProcessCommandLine has_all(@'"reg"', 'add', @'"HKLM\SOFTWARE\Policies\', '/v','/t', 'REG_DWORD', '/d', '/f')
and InitiatingProcessCommandLine has_any('DisableRealtimeMonitoring', 'UseTPMKey', 'UseTPMKeyPIN', 'UseAdvancedStartup',
'EnableBDEWithNoTPM', 'RecoveryKeyMessageSource')
Stage 3: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 4: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
InitiatingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
DnsDomain | extend |
OracleDBAudit - Connection to database from unknown IP
#'Detects when user connects to a database from IP address which is not present in AllowList.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 80b1dd6d-1aea-471e-be7a-a4a0afdeec80
name: OracleDBAudit - Connection to database from unknown IP
description: |
'Detects when user connects to a database from IP address which is not present in AllowList.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let ip_allowlist = dynamic(['127.0.0.2']);
OracleDatabaseAuditEvent
| where isnotempty(SrcIpAddr)
| where DbAction =~ 'CONNECT'
| where SrcIpAddr !in (ip_allowlist)
| project SrcIpAddr, DstUserName
| extend AccountCustomEntity = DstUserName
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let ip_allowlist = dynamic(['127.0.0.2']);
Stage 1: source
OracleDatabaseAuditEvent
Stage 2: where
| where isnotempty(SrcIpAddr)
Stage 3: where
| where DbAction =~ 'CONNECT'
Stage 4: where
| where SrcIpAddr !in (ip_allowlist)
Stage 5: project
| project SrcIpAddr, DstUserName
Stage 6: extend
| extend AccountCustomEntity = DstUserName
Stage 7: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DbAction | eq |
| field:"DbAction" kind:eq value:"CONNECT" |
SrcIpAddr | is_not_null | field:"src_ip" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | eq | ip_allowlist | excludes:SrcIpAddr field:"SrcIpAddr" value:"ip_allowlist" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | project |
SrcIpAddr | project |
AccountCustomEntity | extend |
IPCustomEntity | extend |
PaloAlto - MAC address conflict
#'Detects several users with the same MAC address.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 976d2eee-51cb-11ec-bf63-0242ac130002
name: PaloAlto - MAC address conflict
description: |
'Detects several users with the same MAC address.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 2;
PaloAltoCDLEvent
| where EventResourceId =~ 'TRAFFIC'
| where isnotempty(DestinationMACAddress) and isnotempty(DstUsername)
| summarize UserSet = make_set(DstUsername) by DestinationMACAddress
| extend Users = array_length(UserSet)
| where Users >= threshold
| extend AccountCustomEntity = UserSet, IPCustomEntity = DestinationMACAddress
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 2;
Stage 1: source
PaloAltoCDLEvent
Stage 2: where
| where EventResourceId =~ 'TRAFFIC'
Stage 3: where
| where isnotempty(DestinationMACAddress) and isnotempty(DstUsername)
Stage 4: summarize
| summarize UserSet = make_set(DstUsername) by DestinationMACAddress
Stage 5: extend
| extend Users = array_length(UserSet)
Stage 6: where
| where Users >= threshold
Stage 7: extend
| extend AccountCustomEntity = UserSet, IPCustomEntity = DestinationMACAddress
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationMACAddress | is_not_null | field:"DestinationMACAddress" kind:is_not_null | |
DstUsername | is_not_null | field:"DstUsername" kind:is_not_null | |
EventResourceId | eq |
| field:"EventResourceId" kind:eq value:"TRAFFIC" |
Users | ge |
| field:"Users" kind:ge value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationMACAddress | summarize |
UserSet | summarize |
Users | extend |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Password spray attack against Microsoft Entra ID application
#Identifies evidence of password spray activity against Microsoft Entra ID applications by looking for failures from multiple accounts from the same IP address within a time window. If the number of accounts breaches the threshold just once, all failures from the IP address within the time range are bought into the result. Details on whether there were successful authentications by the IP address within the time window are also included. This can be an indicator that an attack was successful. The default failure acccount threshold is 5, Default time window for failures is 20m and default look back window is 1 day Note: Due to the number of possible accounts involved in a password spray it is not possible to map identities to a custom entity. References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
Rule Body
id: 48607a29-a26a-4abf-8078-a06dbdd174a4
name: Password spray attack against Microsoft Entra ID application
description: |
'Identifies evidence of password spray activity against Microsoft Entra ID applications by looking for failures from multiple accounts from the same IP address within a time window. If the number of accounts breaches the threshold just once, all failures from the IP address within the time range are bought into the result. Details on whether there were successful authentications by the IP address within the time window are also included.
This can be an indicator that an attack was successful.
The default failure acccount threshold is 5, Default time window for failures is 20m and default look back window is 1 day
Note: Due to the number of possible accounts involved in a password spray it is not possible to map identities to a custom entity.
References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let timeRange = 1d;
let lookBack = 7d;
let authenticationWindow = 20m;
let authenticationThreshold = 5;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let failureCodes = dynamic([50053, 50126, 50055]); // invalid password, account is locked - too many sign ins, expired password
let successCodes = dynamic([0, 50055, 50057, 50155, 50105, 50133, 50005, 50076, 50079, 50173, 50158, 50072, 50074, 53003, 53000, 53001, 50129]);
// Lookup up resolved identities from last 7 days
let aadFunc = (tableName:string){
let identityLookup = table(tableName)
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| where isnotempty(UserId)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName, Type;
// collect window threshold breaches
table(tableName)
| where TimeGenerated > ago(timeRange)
| where ResultType in(failureCodes)
| summarize FailedPrincipalCount = dcount(UserPrincipalName) by bin(TimeGenerated, authenticationWindow), IPAddress, AppDisplayName, Type
| where FailedPrincipalCount >= authenticationThreshold
| summarize WindowThresholdBreaches = count() by IPAddress, Type
| join kind= inner (
// where we breached a threshold, join the details back on all failure data
table(tableName)
| where TimeGenerated > ago(timeRange)
| where ResultType in(failureCodes)
| extend LocationDetails = todynamic(LocationDetails)
| extend FullLocation = strcat(LocationDetails.countryOrRegion,'|', LocationDetails.state, '|', LocationDetails.city)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), make_set(ClientAppUsed,20), make_set(FullLocation,20), FailureCount = count() by IPAddress, AppDisplayName, UserPrincipalName, UserDisplayName, Identity, UserId, Type
// lookup any unresolved identities
| extend UnresolvedUserId = iff(Identity matches regex isGUID, UserId, "")
| join kind= leftouter (
identityLookup
) on $left.UnresolvedUserId==$right.UserId
| extend UserDisplayName=iff(isempty(lu_UserDisplayName), UserDisplayName, lu_UserDisplayName)
| extend UserPrincipalName=iff(isempty(lu_UserPrincipalName), UserPrincipalName, lu_UserPrincipalName)
| summarize StartTime = min(StartTime), EndTime = max(EndTime), make_set(UserPrincipalName,20), make_set(UserDisplayName,20), make_set(set_ClientAppUsed,20), make_set(set_FullLocation,20), make_list(FailureCount,20) by IPAddress, AppDisplayName, Type
| extend FailedPrincipalCount = array_length(set_UserPrincipalName)
) on IPAddress
| project IPAddress, StartTime, EndTime, TargetedApplication=AppDisplayName, FailedPrincipalCount, UserPrincipalNames=set_UserPrincipalName, UserDisplayNames=set_UserDisplayName, ClientAppsUsed=set_set_ClientAppUsed, Locations=set_set_FullLocation, FailureCountByPrincipal=list_FailureCount, WindowThresholdBreaches, Type
| join kind= inner (
table(tableName) // get data on success vs. failure history for each IP
| where TimeGenerated > ago(timeRange)
| where ResultType in(successCodes) or ResultType in(failureCodes) // success or failure types
| summarize GlobalSuccessPrincipalCount = dcountif(UserPrincipalName, (ResultType in (successCodes))), ResultTypeSuccesses = make_set_if(ResultType, (ResultType in (successCodes))), GlobalFailPrincipalCount = dcountif(UserPrincipalName, (ResultType in (failureCodes))), ResultTypeFailures = make_set_if(ResultType, (ResultType in (failureCodes))) by IPAddress, Type
| where GlobalFailPrincipalCount > GlobalSuccessPrincipalCount // where the number of failed principals is greater than success - eliminates FPs from IPs who authenticate successfully alot and as a side effect have alot of failures
) on IPAddress
| project-away IPAddress1
| extend timestamp=StartTime
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.7
kind: Scheduled
Stages and Predicates
Parameters
let timeRange = 1d;
let lookBack = 7d;
let authenticationWindow = 20m;
let authenticationThreshold = 5;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let failureCodes = dynamic([50053, 50126, 50055]);
let successCodes = dynamic([0, 50055, 50057, 50155, 50105, 50133, 50005, 50076, 50079, 50173, 50158, 50072, 50074, 53003, 53000, 53001, 50129]);
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let Binding: aadFunc
let aadFunc = (tableName:string){
let identityLookup = table(tableName)
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| where isnotempty(UserId)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName, Type;
Stage 1: union
union of 2 branches
Stage 2: source
aadFunc(...)
Stage 3: source
aadFunc(...)
Qakbot Campaign Self Deletion
#This query was originally published in the threat analytics report, Qakbot blight lingers, seeds ransomware Qakbot is malware that steals login credentials from banking and financial services. It has been deployed against small businesses as well as major corporations. Some outbreaks have involved targeted ransomware campaigns that use a similar set of techniques. Links to related queries are listed under See also. The following query detects if an instance of Qakbot has attempted to overwrite its original binary. Reference - https://www.microsoft.com/security/blog/2017/11/06/mitigating-and-eliminating-info-stealing-qakbot-and-emotet-in-corporate-networks/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 47c02e21-3949-4e05-a28e-576cd75ff6f6
name: Qakbot Campaign Self Deletion
description: |
This query was originally published in the threat analytics report, Qakbot blight lingers, seeds ransomware
Qakbot is malware that steals login credentials from banking and financial services. It has been deployed against small businesses as well as major corporations. Some outbreaks have involved targeted ransomware campaigns that use a similar set of techniques. Links to related queries are listed under See also.
The following query detects if an instance of Qakbot has attempted to overwrite its original binary.
Reference - https://www.microsoft.com/security/blog/2017/11/06/mitigating-and-eliminating-info-stealing-qakbot-and-emotet-in-corporate-networks/
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1070
tags:
- Qakbot
- Ransomware
- Self Deletion
- Defense Evasion
query: |
DeviceProcessEvents
| where FileName =~ "ping.exe"
| where InitiatingProcessFileName =~ "cmd.exe"
| where (InitiatingProcessCommandLine has "calc.exe") and (InitiatingProcessCommandLine has "-n 6") and (InitiatingProcessCommandLine has "127.0.0.1")
| project TimeGenerated, ProcessCommandLine, InitiatingProcessCommandLine, InitiatingProcessParentFileName, DeviceId, DeviceName
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where FileName =~ "ping.exe"
Stage 3: where
| where InitiatingProcessFileName =~ "cmd.exe"
Stage 4: where
| where (InitiatingProcessCommandLine has "calc.exe") and (InitiatingProcessCommandLine has "-n 6") and (InitiatingProcessCommandLine has "127.0.0.1")
Stage 5: project
| project TimeGenerated, ProcessCommandLine, InitiatingProcessCommandLine, InitiatingProcessParentFileName, DeviceId, DeviceName
Stage 6: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 7: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | eq |
| field:"process_basename" kind:eq value:"ping.exe" |
InitiatingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match |
InitiatingProcessFileName | eq |
| field:"parent_process_basename" kind:eq value:"cmd.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | project |
DeviceName | project |
InitiatingProcessCommandLine | project |
InitiatingProcessParentFileName | project |
ProcessCommandLine | project |
TimeGenerated | project |
HostName | extend |
DnsDomain | extend |
Solorigate Defender Detections
#Surfaces any Defender Alert for Solorigate Events. In Microsoft Sentinel the SecurityAlerts table includes only the Device Name of the affected device, this query joins the DeviceInfo table to clearly connect other information such as Device group, ip, logged on users etc. This way, the Microsoft Sentinel user can have all the pertinent device info in one view for all the the Solarigate Defender alerts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceInfo action any: Device information |
Rule Body
id: e70fa6e0-796a-4e85-9420-98b17b0bb749
name: Solorigate Defender Detections
description: |
'Surfaces any Defender Alert for Solorigate Events. In Microsoft Sentinel the SecurityAlerts table includes only the Device Name of the affected device, this query joins the DeviceInfo table to clearly connect other information such as
Device group, ip, logged on users etc. This way, the Microsoft Sentinel user can have all the pertinent device info in one view for all the the Solarigate Defender alerts.'
severity: High
requiredDataConnectors:
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert (MDATP)
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceInfo
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1195
tags:
- Solorigate
- NOBELIUM
query: |
DeviceInfo
| extend DeviceName = tolower(DeviceName)
| join (SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where ThreatName has "Solorigate"
) on $left.DeviceName == $right.CompromisedEntity
| project TimeGenerated, DisplayName, ThreatName, CompromisedEntity, PublicIP, MachineGroup, AlertSeverity, Description, LoggedOnUsers, DeviceId, TenantId
| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
| project-away DomainIndex
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: CompromisedEntity
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: PublicIP
version: 1.0.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - 0-day Vulnerability" ]
Stages and Predicates
Stage 1: source
DeviceInfo
Stage 2: extend
| extend DeviceName = tolower(DeviceName)
Stage 3: join
| join (SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where ThreatName has "Solorigate"
) on $left.DeviceName == $right.CompromisedEntity
Stage 4: project
project AlertSeverity, CompromisedEntity, Description, DeviceId, DisplayName, LoggedOnUsers, MachineGroup, PublicIP, TenantId, ThreatName, TimeGenerated
Stage 5: extend
extend DomainIndex, HostName
Stage 6: extend
extend HostNameDomain
HostNameDomain =DomainIndex != -1substring(CompromisedEntity, (DomainIndex + 1))CompromisedEntityStage 7: project-away
project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ProviderName | eq |
| field:"ProviderName" kind:eq value:"MDATP" |
ThreatName | match |
| field:"ThreatName" kind:match value:"Solorigate" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertSeverity | project |
CompromisedEntity | project |
Description | project |
DeviceId | project |
DisplayName | project |
LoggedOnUsers | project |
MachineGroup | project |
PublicIP | project |
TenantId | project |
ThreatName | project |
TimeGenerated | project |
HostName | extend |
HostNameDomain | extend |
SpyCloud Enterprise Malware Detection
#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.
'This alert creates an incident when an malware record is detected in the SpyCloud watchlist data'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 7ba50f9e-2f94-462b-a54b-8642b8c041f5
name: SpyCloud Enterprise Malware Detection
description: |
'This alert creates an incident when an malware record is detected in the SpyCloud watchlist data'
severity: High
requiredDataConnectors: []
status: Available
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
tactics:
- CredentialAccess
relevantTechniques:
- T1555
query: |
SpyCloudBreachDataWatchlist_CL
| where Severity_s == '25'
| project TimeGenerated, Document_Id_g, Source_Id_s, SpyCloud_Publish_Date_t, Email_s, Domain_s, Password_s, Password_Plaintext_s, Username_s, Infected_Machine_Id_g, Infected_Path_s, Infected_Time_t, Target_Domain_s, Target_SubDomain_s, User_Hostname_s, User_OS_s, Target_URL_s,IP_Address_s
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 12h
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride: null
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Infected_Machine_Id_g
- identifier: DnsDomain
columnName: User_Hostname_s
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Email_s
- identifier: Name
columnName: Username_s
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Target_Domain_s
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Target_SubDomain_s
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP_Address_s
customDetails:
Document_Id: Document_Id_g
Password: Password_s
Password_Plaintext: Password_Plaintext_s
Infected_Path: Infected_Path_s
Infected_Time: Infected_Time_t
Domain: Domain_s
Source_Id: Source_Id_s
PublishDate: SpyCloud_Publish_Date_t
User_Host_Name: User_Hostname_s
sentinelEntitiesMappings: null
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
SpyCloudBreachDataWatchlist_CL
Stage 2: where
| where Severity_s == '25'
Stage 3: project
| project TimeGenerated, Document_Id_g, Source_Id_s, SpyCloud_Publish_Date_t, Email_s, Domain_s, Password_s, Password_Plaintext_s, Username_s, Infected_Machine_Id_g, Infected_Path_s, Infected_Time_t, Target_Domain_s, Target_SubDomain_s, User_Hostname_s, User_OS_s, Target_URL_s,IP_Address_s
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity_s | eq |
| field:"Severity_s" kind:eq value:"25" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Document_Id_g | project |
Domain_s | project |
Email_s | project |
IP_Address_s | project |
Infected_Machine_Id_g | project |
Infected_Path_s | project |
Infected_Time_t | project |
Password_Plaintext_s | project |
Password_s | project |
Source_Id_s | project |
SpyCloud_Publish_Date_t | project |
Target_Domain_s | project |
Target_SubDomain_s | project |
Target_URL_s | project |
TimeGenerated | project |
User_Hostname_s | project |
User_OS_s | project |
Username_s | project |
TI map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)
#Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: a4025a76-6490-4e6b-bb69-d02be4b03f07
name: TI map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)
description: |
Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- 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 AzureNetworkAnalytics_CL 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 AzureNetworkAnalytics_CL logs for NSG Flow information
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 (
AzureNetworkAnalytics_CL
| where TimeGenerated >= ago(dt_lookBack)
| extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
| extend PIPs = split(PublicIPs_s, '|', 0)
| extend PIP = tostring(PIPs[0])
)
on $left.TI_ipEntity == $right.PIP
// Filter out logs that occurred after the expiration of the corresponding indicator
| where AzureNetworkAnalytics_CL_TimeGenerated < ExpirationDateTime
// Filter out NSG Flow logs that are not allowed (FlowStatus_s == "A")
| where FlowStatus_s == "A"
// Group the results by IndicatorId and PIP (Public IP), and keep the log entry with the latest timestamp
| summarize AzureNetworkAnalytics_CL_TimeGenerated = arg_max(AzureNetworkAnalytics_CL_TimeGenerated, *) by IndicatorId, PIP
// Select the desired output fields
| project AzureNetworkAnalytics_CL_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
TI_ipEntity, Computer, FlowDirection_s, FlowStatus_s, FlowType_s, SrcPublicIPs_s, DestPublicIPs_s, PublicIPs_s, L7Protocol_s, DestPort_d, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
// Extract hostname and DNS domain from the Computer field
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
// Rename the timestamp field
| extend timestamp = AzureNetworkAnalytics_CL_TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: TI_ipEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.3
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 14 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 (
AzureNetworkAnalytics_CL
| where TimeGenerated >= ago(dt_lookBack)
| extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
| extend PIPs = split(PublicIPs_s, '|', 0)
| extend PIP = tostring(PIPs[0])
)
on $left.TI_ipEntity == $right.PIP
Stage 9: where
where AzureNetworkAnalytics_CL_TimeGenerated < ExpirationDateTime
Stage 10: where
where FlowStatus_s == "A"
Stage 11: summarize
summarize AzureNetworkAnalytics_CL_TimeGenerated by IndicatorId, PIP
Stage 12: project
project ActivityGroupNames, AzureNetworkAnalytics_CL_TimeGenerated, Computer, ConfidenceScore, Description, DestPort_d, DestPublicIPs_s, EmailSourceIpAddress, ExpirationDateTime, FlowDirection_s, FlowStatus_s, FlowType_s, IndicatorId, L7Protocol_s, NetworkDestinationIP, NetworkIP, NetworkSourceIP, PublicIPs_s, SrcPublicIPs_s, TI_ipEntity, ThreatType, Type, Url
Stage 13: extend
extend DnsDomain, HostName
Stage 14: 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 |
AzureNetworkAnalytics_CL_TimeGenerated | project |
Computer | project |
ConfidenceScore | project |
Description | project |
DestPort_d | project |
DestPublicIPs_s | project |
EmailSourceIpAddress | project |
ExpirationDateTime | project |
FlowDirection_s | project |
FlowStatus_s | project |
FlowType_s | project |
IndicatorId | project |
L7Protocol_s | project |
NetworkDestinationIP | project |
NetworkIP | project |
NetworkSourceIP | project |
PublicIPs_s | project |
SrcPublicIPs_s | project |
TI_ipEntity | project |
ThreatType | project |
Type | project |
Url | project |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
TI Map URL entity to Cloud App Events
#Identifies compromises and attacks and detect malicious activities in one's URL entity from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | CloudAppEvents action any: Cloud app activity |
Rule Body
id: 526df43b-f514-477c-af7a-c8d3586457fb
name: TI Map URL entity to Cloud App Events
description: |
'Identifies compromises and attacks and detect malicious activities in one's URL entity from TI'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- CloudAppEvents
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "url"
| extend Url = ObservableValue
| extend IndicatorId = tostring(split(Id, "--")[2])
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| join kind=innerunique (CloudAppEvents
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1, tostring(ActivityObjects))
| extend userPrincipalName = tostring(RawEventData.UserId)
| extend TargetResourceDisplayName = tostring(ActivityObjects[0].displayName)
| extend CloudApps_TimeGenerated = TimeGenerated) on Url
| where CloudApps_TimeGenerated < ValidUntil
| summarize CloudApps_TimeGenerated = argmax(CloudApps_TimeGenerated, *) by IndicatorId, Url
| extend
Description = column_ifexists("max_CloudApps_TimeGenerated_Description", ""),
ActivityGroupNames = column_ifexists("max_CloudApps_TimeGenerated_ActivityGroupNames", ""),
ThreatType = column_ifexists("max_CloudApps_TimeGenerated_ThreatType", ""),
ExpirationDateTime = column_ifexists("max_CloudApps_TimeGenerated_ExpirationDateTime", ""),
ConfidenceScore = column_ifexists("max_CloudApps_TimeGenerated_ConfidenceScore", ""),
IPAddress = column_ifexists("max_CloudApps_TimeGenerated_IPAddress", ""),
AccountObjectId = column_ifexists("max_CloudApps_TimeGenerated_AccountObjectId", ""),
AccountDisplayName = column_ifexists("max_CloudApps_TimeGenerated_AccountDisplayName", ""),
ObjectName = column_ifexists("max_CloudApps_TimeGenerated_ObjectName", ""),
Application = column_ifexists("max_CloudApps_TimeGenerated_Application", ""),
ApplicationID = column_ifexists("max_CloudApps_TimeGenerated_ApplicationId", ""),
userPrincipalName = column_ifexists("max_CloudApps_TimeGenerated_userPrincipalName", "")
| project CloudApps_TimeGenerated, Description, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, IPAddress, userPrincipalName, AccountObjectId, AccountDisplayName, ObjectName, Application, ApplicationID
entityMappings:
- entityType: Account
fieldMappings:
- identifier: ObjectGuid
columnName: AccountObjectId
- identifier: FullName
columnName: userPrincipalName
- identifier: DisplayName
columnName: AccountDisplayName
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: Application
- identifier: AppId
columnName: ApplicationID
version: 1.0.7
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType == "url"
Stage 4: extend
| extend Url = ObservableValue
Stage 5: extend
| extend IndicatorId = tostring(split(Id, "--")[2])
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId, ObservableValue
Stage 8: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 9: join
| join kind=innerunique (CloudAppEvents
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1, tostring(ActivityObjects))
| extend userPrincipalName = tostring(RawEventData.UserId)
| extend TargetResourceDisplayName = tostring(ActivityObjects[0].displayName)
| extend CloudApps_TimeGenerated = TimeGenerated) on Url
Stage 10: where
where CloudApps_TimeGenerated < ValidUntil
Stage 11: summarize
summarize CloudApps_TimeGenerated by IndicatorId, Url
Stage 12: extend
extend AccountDisplayName, AccountObjectId, ActivityGroupNames, Application, ApplicationID, ConfidenceScore, Description, ExpirationDateTime, IPAddress, ObjectName, ThreatType, userPrincipalName
Stage 13: project
project AccountDisplayName, AccountObjectId, Application, ApplicationID, CloudApps_TimeGenerated, ConfidenceScore, Description, ExpirationDateTime, IPAddress, IndicatorId, ObjectName, ThreatType, Url, userPrincipalName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"url" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountDisplayName | project |
AccountObjectId | project |
Application | project |
ApplicationID | project |
CloudApps_TimeGenerated | project |
ConfidenceScore | project |
Description | project |
ExpirationDateTime | project |
IPAddress | project |
IndicatorId | project |
ObjectName | project |
ThreatType | project |
Url | project |
userPrincipalName | project |