Detection rules › Kusto
Kusto rules: 0a
API - Invalid host access
#'42Crunch API protection against invalid host access'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 28500be7-cfcf-40e1-bad4-bc524e9283e2
name: API - Invalid host access
description: |
'42Crunch API protection against invalid host access'
severity: Low
requiredDataConnectors:
- connectorId: 42CrunchAPIProtection
dataTypes:
- FortyTwoCrunchAPIProtection
- connectorId: FortyTwoCrunchAPIProtection
dataTypes:
- FortyTwoCrunchAPIProtection
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: SingleAlert
status: Available
tactics:
- Reconnaissance
relevantTechniques:
- T1592
query: |
FortyTwoCrunchAPIProtection
| where TimeGenerated >= ago(5m)
| where ErrorMessage has "hostpath_mapping"
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, ResponseHeader, RequestHeader, Errors, EventType, Uuid
| sort by TimeGenerated desc
customDetails:
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InstanceName
version: 3.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
FortyTwoCrunchAPIProtection
Stage 2: where
| where TimeGenerated >= ago(5m)
Stage 3: where
| where ErrorMessage has "hostpath_mapping"
Stage 4: project-away
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, ResponseHeader, RequestHeader, Errors, EventType, Uuid
Stage 5: sort
| sort by TimeGenerated desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorMessage | match |
| field:"aws::errorMessage" kind:match value:"hostpath_mapping" |
Attempts to sign in to disabled accounts
#Identifies failed attempts to sign in to disabled accounts across multiple Azure Applications. Default threshold for Azure Applications attempted to sign in to is 3. References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes 50057 - User account is disabled. The account has been disabled by an administrator.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rule Body
id: 75ea5c39-93e5-489b-b1e1-68fa6c9d2d04
name: Attempts to sign in to disabled accounts
description: |
'Identifies failed attempts to sign in to disabled accounts across multiple Azure Applications.
Default threshold for Azure Applications attempted to sign in to is 3.
References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes
50057 - User account is disabled. The account has been disabled by an administrator.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let threshold = 3;
let aadFunc = (tableName:string){
table(tableName)
| where ResultType == "50057"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), applicationCount = dcount(AppDisplayName),
applicationSet = make_set(AppDisplayName), count() by UserPrincipalName, IPAddress, Type
| where applicationCount >= threshold
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 3;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let Binding: aadFunc
let aadFunc = (tableName:string){
table(tableName)
| where ResultType == "50057"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), applicationCount = dcount(AppDisplayName),
applicationSet = make_set(AppDisplayName), count() by UserPrincipalName, IPAddress, Type
| where applicationCount >= threshold
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
};
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt
Leg 1: aadSignin
Leg 2: aadNonInt
Authentication Methods Changed for Privileged Account
#Identifies authentication methods being changed for a privileged account. This could be an indication of an attacker adding an auth method to the account so they can have continued access. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
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)
- Azure AD External Guest User Invited (Splunk)
Rule Body
id: 694c91ee-d606-4ba9-928e-405a2dd0ff0f
name: Authentication Methods Changed for Privileged Account
description: |
'Identifies authentication methods being changed for a privileged account. This could be an indication of an attacker adding an auth method to the account so they can have continued access.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
queryFrequency: 2h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Persistence
relevantTechniques:
- T1098
tags:
- AADSecOpsGuide
query: |
let queryperiod = 14d;
let queryfrequency = 2h;
let security_info_actions = dynamic(["User registered security info", "User changed default security info", "User deleted security info", "Admin updated security info", "User reviewed security info", "Admin deleted security info", "Admin registered security info"]);
let VIPUsers = (
IdentityInfo
| where TimeGenerated > ago(queryperiod)
| mv-expand AssignedRoles
| where AssignedRoles contains 'Admin'
| summarize by AccountUPN);
AuditLogs
| where TimeGenerated > ago(queryfrequency)
| where Category =~ "UserManagement"
| where ActivityDisplayName in (security_info_actions)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
)
| where TargetUserPrincipalName in~ (VIPUsers)
// Uncomment the line below if you are experiencing high volumes of Target entities. If this is uncommented, the Target column will not be mapped to an entity.
//| summarize Start=min(TimeGenerated), End=max(TimeGenerated), Actions = make_set(ResultReason, MaxSize=8), Targets=make_set(Target, MaxSize=256) by Initiator, IP, Result
// Comment out this line below, if line above is used.
| summarize Start=min(TimeGenerated), End=max(TimeGenerated), Actions = make_set(ResultReason, MaxSize=8) by InitiatingAppName, InitiatingAppServicePrincipalId,
InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, TargetUserPrincipalName, Result
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1]),
TargetName = iff(tostring(TargetUserPrincipalName) has "[", "", tostring(split(TargetUserPrincipalName,'@',0)[0])), TargetUPNSuffix = iff(tostring(TargetUserPrincipalName) has "[", "", tostring(split(TargetUserPrincipalName,'@',1)[0]))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let queryperiod = 14d;
let queryfrequency = 2h;
Let Binding: security_info_actions
let security_info_actions = dynamic(["User registered security info", "User changed default security info", "User deleted security info", "Admin updated security info", "User reviewed security info", "Admin deleted security info", "Admin registered security info"]);
Let Binding: VIPUsers
let VIPUsers = (
IdentityInfo
| where TimeGenerated > ago(queryperiod)
| mv-expand AssignedRoles
| where AssignedRoles contains 'Admin'
| summarize by AccountUPN);
Stage 1: source
let VIPUsers
Stage 2: source
AuditLogs
Stage 3: where
where TimeGenerated > ago(7200s)
Stage 4: where
where Category =~ "UserManagement"
Stage 5: where
where ActivityDisplayName in ("Admin deleted security info", "Admin registered security info", "Admin updated security info", "User changed default security info", "User deleted security info", "User registered security info", "User reviewed security info")
Stage 6: extend (5 consecutive steps)
extend InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName
Stage 7: kusto:mv-apply
kusto:mv-apply TargetResource.type =~ "User"
Stage 8: where
where TargetUserPrincipalName =~ "VIPUsers"
Stage 9: summarize
summarize Actions, End, Start by InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, TargetUserPrincipalName, Result
Stage 10: extend
extend InitiatingAccountName, InitiatingAccountUPNSuffix, TargetName, TargetUPNSuffix
TargetName =TargetUserPrincipalName has "["""tostring(split(TargetUserPrincipalName, '@', 0)[0])TargetUPNSuffix =TargetUserPrincipalName has "["""tostring(split(TargetUserPrincipalName, '@', 1)[0])Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | in |
| field:"ActivityDisplayName" kind:in |
Category | eq |
| field:"Category" kind:eq value:"UserManagement" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"User" |
TargetUserPrincipalName | in |
| field:"TargetUserPrincipalName" kind:in value:"VIPUsers" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | summarize |
End | summarize |
InitiatingAadUserId | summarize |
InitiatingAppName | summarize |
InitiatingAppServicePrincipalId | summarize |
InitiatingIpAddress | summarize |
InitiatingUserPrincipalName | summarize |
Result | summarize |
Start | summarize |
TargetUserPrincipalName | summarize |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
TargetName | extend |
TargetUPNSuffix | extend |
AV detections related to Tarrask malware
#This query looks for Microsoft Defender AV detections related to Tarrask malware. 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 would allow the Microsoft Sentinel analyst to have more context related to the alert, if available. Reference: https://www.microsoft.com/security/blog/2022/04/12/tarrask-malware-uses-scheduled-tasks-for-defense-evasion/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceInfo action any: Device information |
Rule Body
id: 1785d372-b9fe-4283-96a6-3a1d83cabfd1
name: AV detections related to Tarrask malware
description: |
'This query looks for Microsoft Defender AV detections related to Tarrask malware. 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 would allow the Microsoft Sentinel analyst to have more context related to the alert, if available.
Reference: https://www.microsoft.com/security/blog/2022/04/12/tarrask-malware-uses-scheduled-tasks-for-defense-evasion/'
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1053
query: |
let Tarrask_threats = dynamic(["HackTool:Win64/Tarrask!MS", "HackTool:Win64/Ligolo!MSR", "Behavior:Win32/ScheduledTaskHide.A", "Tarrask"]);
DeviceInfo
| extend DeviceName = tolower(DeviceName)
| join kind=rightouter ( SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
| where ThreatName in~ (Tarrask_threats) or ThreatFamilyName in~ (Tarrask_threats)
| extend CompromisedEntity = tolower(CompromisedEntity)
) on $left.DeviceName == $right.CompromisedEntity
| summarize by DisplayName, ThreatName, ThreatFamilyName, PublicIP, AlertSeverity, Description, tostring(LoggedOnUsers), DeviceId, TenantId , bin(TimeGenerated, 1d), CompromisedEntity, tostring(LoggedOnUsers), ProductName, Entities
| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
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.4
kind: Scheduled
Stages and Predicates
Parameters
let Tarrask_threats = dynamic(["HackTool:Win64/Tarrask!MS", "HackTool:Win64/Ligolo!MSR", "Behavior:Win32/ScheduledTaskHide.A", "Tarrask"]);
Stage 1: source
DeviceInfo
Stage 2: extend
| extend DeviceName = tolower(DeviceName)
Stage 3: join
| join kind=rightouter ( SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
| where ThreatName in~ (Tarrask_threats) or ThreatFamilyName in~ (Tarrask_threats)
| extend CompromisedEntity = tolower(CompromisedEntity)
) on $left.DeviceName == $right.CompromisedEntity
Stage 4: summarize
summarize by DisplayName, ThreatName, ThreatFamilyName, PublicIP, AlertSeverity, Description, DeviceId, TenantId, CompromisedEntity, ProductName, Entities
Stage 5: extend
extend DomainIndex, HostName
Stage 6: extend
extend HostNameDomain
HostNameDomain =DomainIndex != -1substring(CompromisedEntity, (DomainIndex + 1))CompromisedEntityIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ProviderName | eq |
| field:"ProviderName" kind:eq value:"MDATP" |
ThreatFamilyName | in |
| field:"ThreatFamilyName" kind:in |
ThreatName | in |
| field:"ThreatName" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertSeverity | summarize |
CompromisedEntity | summarize |
Description | summarize |
DeviceId | summarize |
DisplayName | summarize |
Entities | summarize |
ProductName | summarize |
PublicIP | summarize |
TenantId | summarize |
ThreatFamilyName | summarize |
ThreatName | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation
#Identifies creation of IAM policies that provide broad CRUD permissions for DynamoDB, followed by policy attachment activity. This sequence can indicate an attempt to grant capabilities that support privilege escalation and should be validated against approved administrative changes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- ASL AWS Create Policy Version to allow all resources (Splunk)
- AWS IAM CompromisedKeyQuarantine Policy Attached to User (Elastic)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - CloudFormation policy created then used for privilege escalation (Kusto)
- AWSCloudTrail - Created CRUD S3 policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD KMS policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of DataPipeline policy and then privilege escalation (Kusto)
Rule Body
id: 6f675c17-7a61-440c-abd1-c73ef4d748ec
name: AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation
description: |
Identifies creation of IAM policies that provide broad CRUD permissions for DynamoDB, followed by policy attachment activity. This sequence can indicate an attempt to grant capabilities that support
privilege escalation and should be validated against approved administrative changes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
- Persistence
relevantTechniques:
- T1484
- T1098.003
query: |
let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;
// Creating Master table with all the events to use with materialize for better performance
let EventInfo = AWSCloudTrail
| where TimeGenerated >= ago(lookback)
| where EventName in (EventNameList) or EventName in (createPolicy)
| 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]), "");
//Checking for Policy creation event with Full Admin Privileges since lookback period.
let FullAdminPolicyEvents = materialize( EventInfo
| where TimeGenerated >= ago(lookback)
| where EventName in (createPolicy)
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
| mvexpand Statement
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
| extend Action = tostring(Action)
| where Effect =~ "Allow" and ((Action contains "dynamodb:Create" or Action contains "dynamodb:Put") and (Action contains "dynamodb:Describe" or Action contains "dynamodb:Get" or Action contains "dynamodb:Scan") and Action contains "dynamodb:Update" and Action contains "dynamodb:Delete") and Resource == "*" and Condition == ""
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn
| project-rename StartTime = TimeGenerated );
let PolicyAttach = materialize( EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName, UserIdentityType , RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, SourceIpAddress, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType", UserIdentityType, "SourceIpAddress", SourceIpAddress, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
| project EventSource, PolicyName, AttachEvent, AttachEventCount, RecipientAccountId, AccountName, AccountUPNSuffix
);
// Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
// These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
FullAdminPolicyEvents
| join kind=leftouter
(
PolicyAttach
)
on PolicyName
| project-away PolicyName1
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
PolicyName: PolicyName
EventName: EventName
RecipientAccountId: RecipientAccountId
UserIdentityArn: UserIdentityArn
alertDetailsOverride:
alertDisplayNameFormat: 'AWS DynamoDB privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} from {{SourceIpAddress}}.'
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;
let EventInfo and let FullAdminPolicyEvents are inlined into the numbered stages below.
Let Binding: PolicyAttach
let PolicyAttach = materialize( EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName, UserIdentityType , RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, SourceIpAddress, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType", UserIdentityType, "SourceIpAddress", SourceIpAddress, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
| project EventSource, PolicyName, AttachEvent, AttachEventCount, RecipientAccountId, AccountName, AccountUPNSuffix
);
Stages 1 to 14 define let FullAdminPolicyEvents as the rule's main pipeline source. Stages 15 to 17 use it.
Stage 1: source
AWSCloudTrail
Stage 2: where
| where TimeGenerated >= ago(lookback)
Stage 3: where
| where EventName in (EventNameList) or EventName in (createPolicy)
Stage 4: 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 5: where
| where TimeGenerated >= ago(lookback)
Stage 6: where
| where EventName in (createPolicy)
Stage 7: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 8: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 9: mv-expand
| mvexpand Statement
Stage 10: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
Stage 11: extend
| extend Action = tostring(Action)
Stage 12: where
| where Effect =~ "Allow" and ((Action contains "dynamodb:Create" or Action contains "dynamodb:Put") and (Action contains "dynamodb:Describe" or Action contains "dynamodb:Get" or Action contains "dynamodb:Scan") and Action contains "dynamodb:Update" and Action contains "dynamodb:Delete") and Resource == "*" and Condition == ""
Stage 13: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn
Stage 14: project-rename
| project-rename StartTime = TimeGenerated
Stage 15: join
FullAdminPolicyEvents
| join kind=leftouter
(
PolicyAttach
)
on PolicyName
Stage 16: project-away
project-away PolicyName1
Stage 17: summarize aggregation inside the join branch
summarize by EventSource, EventName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, SourceIpAddress, PolicyName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"ErrorCode" kind:is_null | |
ErrorMessage | is_null | field:"ErrorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | summarize |
AccountUPNSuffix | summarize |
EventName | summarize |
EventSource | summarize |
PolicyName | summarize |
RecipientAccountId | summarize |
SourceIpAddress | summarize |
UserIdentityArn | summarize |
UserIdentityType | summarize |
BloodHound Attack Path Finding - Add Owner to Tier Zero Object via MS Graph App 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.
BloodHound Attack Path Finding - Add Owner to Tier Zero Object via MS Graph App Role
Rule Body
id: 58c70f97-543b-4116-8624-12ee7ad8a13d
name: BloodHound Attack Path Finding - Add Owner to Tier Zero Object via MS Graph
App Role
description: BloodHound Attack Path Finding - Add Owner to Tier Zero Object via MS
Graph App Role
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureT0MGAddOwner\""
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 == "AzureT0MGAddOwner"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0MGAddOwner" |
BloodHound Attack Path Finding - Tier Zero Group Control via MS Graph App 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.
BloodHound Attack Path Finding - Tier Zero Group Control via MS Graph App Role
Rule Body
id: 70e09635-9787-41af-9973-87d22ca15292
name: BloodHound Attack Path Finding - Tier Zero Group Control via MS Graph App Role
description: BloodHound Attack Path Finding - Tier Zero Group Control via MS Graph
App Role
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureT0MGAddMember\""
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 == "AzureT0MGAddMember"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0MGAddMember" |
Cisco SEG - DLP policy violation
#'Detects DLP policy violation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: df5c34dd-e1e6-4e07-90b1-4309ebfe754c
name: Cisco SEG - DLP policy violation
description: |
'Detects DLP policy violation.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1030
query: |
CiscoSEGEvent
| where NetworkDirection =~ 'Outgoing'
| where tostring(AdditionalFields) has 'ESADLPVerdict'
| extend dlp_verdict = extract(@'ESADLPVerdict":"(NOT_EVALUATED|NO TRIGGER|VIOLATION|NO VIOLATION)"', 1, tostring(AdditionalFields))
| where dlp_verdict =~ 'VIOLATION'
| extend AccountCustomEntity = SrcUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
CiscoSEGEvent
Stage 2: where
| where NetworkDirection =~ 'Outgoing'
Stage 3: where
| where tostring(AdditionalFields) has 'ESADLPVerdict'
Stage 4: extend
| extend dlp_verdict = extract(@'ESADLPVerdict":"(NOT_EVALUATED|NO TRIGGER|VIOLATION|NO VIOLATION)"', 1, tostring(AdditionalFields))
Stage 5: where
| where dlp_verdict =~ 'VIOLATION'
Stage 6: extend
| extend AccountCustomEntity = SrcUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalFields | match |
| field:"AdditionalFields" kind:match value:"ESADLPVerdict" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"Outgoing" |
dlp_verdict | eq |
| field:"dlp_verdict" kind:eq value:"VIOLATION" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
dlp_verdict | extend |
AccountCustomEntity | extend |
Cisco Umbrella - URI contains IP address
#'Malware can use IP address to communicate with C2.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Rule Body
id: ee1818ec-5f65-4991-b711-bcf2ab7e36c3
name: Cisco Umbrella - URI contains IP address
description: |
'Malware can use IP address to communicate with C2.'
severity: Medium
requiredDataConnectors:
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
query: |
let lbtime = 10m;
Cisco_Umbrella
| where TimeGenerated > ago(lbtime)
| where EventType == 'proxylogs'
| where DvcAction =~ 'Allowed'
| where UrlOriginal matches regex @'\Ahttp:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*'
| project TimeGenerated, SrcIpAddr, Identities
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Identities
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let lbtime = 10m;
Stage 1: source
Cisco_Umbrella
Stage 2: where
| where TimeGenerated > ago(lbtime)
Stage 3: where
| where EventType == 'proxylogs'
Stage 4: where
| where DvcAction =~ 'Allowed'
Stage 5: where
| where UrlOriginal matches regex @'\Ahttp:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*'
Stage 6: project
| project TimeGenerated, SrcIpAddr, Identities
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq value:"Allowed" |
EventType | eq |
| field:"EventType" kind:eq value:"proxylogs" |
UrlOriginal | regex_match |
| field:"UrlOriginal" kind:regex_match value:"\Ahttp:\/\/\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}.*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Identities | project |
SrcIpAddr | project |
TimeGenerated | project |
Detect Possible Teams BEC Attack by High Teams Recipients
#An external sender suddenly increasing the amount of internal users they are sending messages to, can indicate that external user being compromised and used for BEC Attacks. In these kind of attacks compromised accounts are used to send phishing links or attachments to users in business relationships.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | MessageEvents action any: Teams message processed |
Rule Body
// Possible BEC detection by high teams recipients
let increase_percentage = 200;
let base = (
MessageEvents
| where TimeGenerated > ago(14d)
// Focus on chat messsages
| where ThreadType == "chat"
// Only return external users sending messages
| join kind=leftanti (
IdentityInfo
| where TimeGenerated > ago(14d)
| distinct AccountObjectId
) on $left.SenderObjectId == $right.AccountObjectId
// Make a set of all the chats they are posting to, for every day
| summarize ChatSet = make_set(ThreadId) by SenderEmailAddress, bin(TimeGenerated, 1d)
// Count the amount of chats they posted to for each day
| extend ChatAmmount = array_length(ChatSet)
);
// Get the average of send chats per external user per day
let averageBySender = (
base
| summarize AverageChatsBySender = avg(ChatAmmount) by SenderEmailAddress
);
// Check if the sender dubbled their chats to internal users compared to their baseline
base
| where TimeGenerated > ago(1d)
| join kind=inner averageBySender on SenderEmailAddress
| where ChatAmmount > AverageChatsBySender * (increase_percentage / 100)
Stages and Predicates
Parameters
let increase_percentage = 200;
let base is inlined into the numbered stages below.
Let Binding: averageBySender
let averageBySender = (
base
| summarize AverageChatsBySender = avg(ChatAmmount) by SenderEmailAddress
);
Stage 1: source
let base
Stage 2: source
let averageBySender
Stage 3: source
MessageEvents
Stage 4: where
where TimeGenerated > ago(1209600s)
Stage 5: where
where ThreadType == "chat"
Stage 6: join (negated)
join kind=leftanti (IdentityInfo) on $left.SenderObjectId, $right.AccountObjectId
Stage 7: summarize
summarize ChatSet by SenderEmailAddress
Stage 8: extend
extend ChatAmmount
Stage 9: where
where TimeGenerated > ago(86400s)
Stage 10: join
join kind=inner (averageBySender) on SenderEmailAddress
Stage 11: where
where /* macro: (ChatAmmount > (AverageChatsBySender * (200 / 100))) */
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ThreadType | eq |
| field:"ThreadType" kind:eq value:"chat" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ChatSet | summarize |
SenderEmailAddress | summarize |
ChatAmmount | extend |
Elevation of Privilege attempt detected
#Identifies Elevation of Privilege attempt Azure Firewall IDPS logs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Rule Body
id: 7a0f78b3-9a55-4ad0-a56d-b6616fdbff6a
name: Elevation of Privilege attempt detected
description: |
Identifies Elevation of Privilege attempt Azure Firewall IDPS logs.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureFirewall
dataTypes:
- AZFWIdpsSignature
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
let TimeWindow = 90d; // How far back to look
let HitThreshold = 10; // Minimum hits to alert per SourceIp + Category
let MinSeverity = 1; // Set Minimum Severity
// Feature flags for optional filters
let EnableCategoryFilter = true; // Filter 1
let EnableDescriptionFilter = false; // Filter 2
let EnableActionFilter = false; // Filter 3
// Filter 1: Categories of interest
let CategoriesOfInterest = dynamic([
"Attempted User Privilege Gain",
"Unsuccessful User Privilege Gain",
"Successful User Privilege Gain",
"Attempted Administrator Privilege Gain",
"Successful Administrator Privilege Gain"
]);
// Filter 2: Descriptions of interest
let DescriptionsOfInterest = dynamic([
"attempted-user",
"unsuccessful-user",
"successful-user",
"attempted-admin",
"successful-admin"
]);
// Filter 3: Action match
let MatchActions = dynamic(["Deny", "alert"]);
AZFWIdpsSignature
| where TimeGenerated >= ago(TimeWindow)
| where Severity >= MinSeverity
// Filter 1: Category filter (optional)
| where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))
// Filter 2: Description filter (optional)
| where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))
// Filter 3: Action filter (optional)
| where (EnableActionFilter == false) or (Action in~ (MatchActions))
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
TotalHits = count(),
MaxSeverity = max(Severity),
Actions = make_set(Action, 5),
Signatures = make_set(SignatureId, 20),
Description = make_set(substring(tostring(Description), 0, 120), 3)
by SourceIp, ThreatCategory = Category
| where TotalHits >= HitThreshold
| project
StartTime,
EndTime,
SourceIp,
ThreatCategory,
TotalHits,
MaxSeverity,
Actions,
Signatures,
Description
| order by MaxSeverity desc, TotalHits desc
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let TimeWindow = 90d;
let HitThreshold = 10;
let MinSeverity = 1;
let EnableCategoryFilter = true;
let EnableDescriptionFilter = false;
let EnableActionFilter = false;
let MatchActions = dynamic(["Deny", "alert"]);
Let Binding: CategoriesOfInterest
let CategoriesOfInterest = dynamic([
"Attempted User Privilege Gain",
"Unsuccessful User Privilege Gain",
"Successful User Privilege Gain",
"Attempted Administrator Privilege Gain",
"Successful Administrator Privilege Gain"
]);
Let Binding: DescriptionsOfInterest
let DescriptionsOfInterest = dynamic([
"attempted-user",
"unsuccessful-user",
"successful-user",
"attempted-admin",
"successful-admin"
]);
Stage 1: source
AZFWIdpsSignature
Stage 2: where
| where TimeGenerated >= ago(TimeWindow)
Stage 3: where
| where Severity >= MinSeverity
Stage 4: where
| where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))
Stage 5: where
| where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))
Stage 6: where
| where (EnableActionFilter == false) or (Action in~ (MatchActions))
Stage 7: summarize
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
TotalHits = count(),
MaxSeverity = max(Severity),
Actions = make_set(Action, 5),
Signatures = make_set(SignatureId, 20),
Description = make_set(substring(tostring(Description), 0, 120), 3)
by SourceIp, ThreatCategory = Category
Stage 8: where
| where TotalHits >= HitThreshold
Stage 9: project
| project
StartTime,
EndTime,
SourceIp,
ThreatCategory,
TotalHits,
MaxSeverity,
Actions,
Signatures,
Description
Stage 10: sort
| order by MaxSeverity desc, TotalHits desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | match |
| field:"Category" kind:match |
Severity | ge |
| field:"Severity" kind:ge value:"1" |
TotalHits | ge |
| field:"TotalHits" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | project |
Description | project |
EndTime | project |
MaxSeverity | project |
Signatures | project |
SourceIp | project |
StartTime | project |
ThreatCategory | project |
TotalHits | project |
Infoblox - TI - InfobloxCDC Match Found - Lookalike Domains
#'InfobloxCDC Lookalike Domain match found in your Infoblox TIDE Threat Intelligence. Customize query count, scheduling, responses and more. Modify data sources, types and threat properties as desired. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxCDC.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 568730be-b39d-45e3-a392-941e00837d52
name: Infoblox - TI - InfobloxCDC Match Found - Lookalike Domains
description: |
'InfobloxCDC Lookalike Domain match found in your Infoblox TIDE Threat Intelligence. Customize query count, scheduling, responses and more. Modify data sources, types and threat properties as desired. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxCDC**](https://aka.ms/sentinel-InfobloxCloudDataConnector-parser).'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1498
- T1565
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let TI = ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
| where Description == "Infoblox - HOST - Policy"
| where Tags has_cs "Property: Policy_LookalikeDomains"
| where isnotempty(DomainName)
;
let Data = InfobloxCDC
| extend HitTime = TimeGenerated
| where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(DestinationDnsDomain)
//Remove trailing period at end of domain
| extend DestinationDnsDomain = trim_end(@"\.$", DestinationDnsDomain)
;
TI | join kind=innerunique Data on $left.DomainName == $right.DestinationDnsDomain
| where HitTime >= TimeGenerated and HitTime < ExpirationDateTime
| project LatestIndicatorTime, HitTime, DeviceEventClassID, ThreatLevel, ThreatLevel_Score, ThreatConfidence, DestinationDnsDomain, InfobloxB1FeedName, ThreatClass, ThreatProperty, InfobloxB1PolicyAction, DeviceAction, InfobloxB1PolicyName, SourceIP, DeviceName, SourceMACAddress, SourceUserName, InfobloxB1SrcOSVersion, InfobloxB1ConnectionType, InfobloxB1Network, AdditionalExtensionsParsedNested,
AdditionalInformation, Description, ThreatType, TrafficLightProtocolLevel, Type, ConfidenceScore, ExpirationDateTime, SourceSystem, Action, IndicatorId, ExternalIndicatorId, Tags
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
- identifier: OSVersion
columnName: InfobloxB1SrcOSVersion
- identifier: FullName
columnName: SourceUserName
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DestinationDnsDomain
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: ThreatProperty
- identifier: Category
columnName: ThreatClass
customDetails:
SourceMACAddress: SourceMACAddress
InfobloxB1FeedName: InfobloxB1FeedName
InfobloxB1Network: InfobloxB1Network
InfobloxB1Action: InfobloxB1PolicyAction
InfobloxB1PolicyName: InfobloxB1PolicyName
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let TI is inlined into the numbered stages below.
Let Binding: Data
let Data = InfobloxCDC
| extend HitTime = TimeGenerated
| where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(DestinationDnsDomain)
| extend DestinationDnsDomain = trim_end(@"\.$", DestinationDnsDomain);
Stages 1 to 7 define let TI as the rule's main pipeline source. Stages 8 to 10 use it.
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 3: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 4: where
| where Active == true and ExpirationDateTime > now()
Stage 5: where
| where Description == "Infoblox - HOST - Policy"
Stage 6: where
| where Tags has_cs "Property: Policy_LookalikeDomains"
Stage 7: where
| where isnotempty(DomainName)
Stage 8: join
TI
| join kind=innerunique Data on $left.DomainName == $right.DestinationDnsDomain
Stage 9: where
where HitTime >= TimeGenerated and HitTime < ExpirationDateTime
Stage 10: project
project Action, AdditionalExtensionsParsedNested, AdditionalInformation, ConfidenceScore, Description, DestinationDnsDomain, DeviceAction, DeviceEventClassID, DeviceName, ExpirationDateTime, ExternalIndicatorId, HitTime, IndicatorId, InfobloxB1ConnectionType, InfobloxB1FeedName, InfobloxB1Network, InfobloxB1PolicyAction, InfobloxB1PolicyName, InfobloxB1SrcOSVersion, LatestIndicatorTime, SourceIP, SourceMACAddress, SourceSystem, SourceUserName, Tags, ThreatClass, ThreatConfidence, ThreatLevel, ThreatLevel_Score, ThreatProperty, ThreatType, TrafficLightProtocolLevel, Type
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
Description | eq |
| field:"Description" kind:eq value:"Infoblox - HOST - Policy" |
DestinationDnsDomain | is_not_null | field:"DestinationDnsDomain" kind:is_not_null | |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
Tags | match |
| field:"Tags" kind:match value:"Property: Policy_LookalikeDomains" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
AdditionalExtensionsParsedNested | project |
AdditionalInformation | project |
ConfidenceScore | project |
Description | project |
DestinationDnsDomain | project |
DeviceAction | project |
DeviceEventClassID | project |
DeviceName | project |
ExpirationDateTime | project |
ExternalIndicatorId | project |
HitTime | project |
IndicatorId | project |
InfobloxB1ConnectionType | project |
InfobloxB1FeedName | project |
InfobloxB1Network | project |
InfobloxB1PolicyAction | project |
InfobloxB1PolicyName | project |
InfobloxB1SrcOSVersion | project |
LatestIndicatorTime | project |
SourceIP | project |
SourceMACAddress | project |
SourceSystem | project |
SourceUserName | project |
Tags | project |
ThreatClass | project |
ThreatConfidence | project |
ThreatLevel | project |
ThreatLevel_Score | project |
ThreatProperty | project |
ThreatType | project |
TrafficLightProtocolLevel | project |
Type | project |
Jira - New site admin user
#'Detects new site admin user.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 6bf42891-b54d-4b4e-8533-babc5b3ea4c5
name: Jira - New site admin user
description: |
'Detects new site admin user.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: JiraAuditAPI
dataTypes:
- JiraAudit
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let priv_users = JiraAudit
| where TimeGenerated > ago(14d)
| where EventMessage =~ 'User added to group'
| where ObjectItemName =~ 'site-admins'
| extend user = todynamic(AssociatedItems)[0]['name']
| summarize makeset(user);
JiraAudit
| where EventMessage =~ "User's password changed"
| extend user = todynamic(AssociatedItems)[0]['name']
| where user in (priv_users)
| extend AccountCustomEntity = user
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: priv_users
let priv_users = JiraAudit
| where TimeGenerated > ago(14d)
| where EventMessage =~ 'User added to group'
| where ObjectItemName =~ 'site-admins'
| extend user = todynamic(AssociatedItems)[0]['name']
| summarize makeset(user);
Stage 1: source
JiraAudit
Stage 2: where
| where EventMessage =~ "User's password changed"
Stage 3: extend
| extend user = todynamic(AssociatedItems)[0]['name']
Stage 4: where
| where user in (priv_users)
Stage 5: extend
| extend AccountCustomEntity = user
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | eq |
| field:"EventMessage" kind:eq value:"User's password changed" |
user | in |
| field:"user" kind:in value:"priv_users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
user | extend |
AccountCustomEntity | extend |
Jira - New user created
#'Detects when new user was created.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: 8c90f30f-c612-407c-91a0-c6a6b41ac199
name: Jira - New user created
description: |
'Detects when new user was created.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: JiraAuditAPI
dataTypes:
- JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1078
query: |
JiraAudit
| where EventMessage =~ 'User created'
| where ObjectItemTypeName =~ 'USER'
| project EventCreationTime, UserName, SrcIpAddr, ObjectItemName, AssociatedItems
| extend AccountCustomEntity = ObjectItemName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
JiraAudit
Stage 2: where
| where EventMessage =~ 'User created'
Stage 3: where
| where ObjectItemTypeName =~ 'USER'
Stage 4: project
| project EventCreationTime, UserName, SrcIpAddr, ObjectItemName, AssociatedItems
Stage 5: extend
| extend AccountCustomEntity = ObjectItemName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | eq |
| field:"EventMessage" kind:eq value:"User created" |
ObjectItemTypeName | eq |
| field:"ObjectItemTypeName" kind:eq value:"USER" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AssociatedItems | project |
EventCreationTime | project |
ObjectItemName | project |
SrcIpAddr | project |
UserName | project |
AccountCustomEntity | extend |
PaloAlto - Dropping or denying session with traffic
#'Detects dropping or denying session with traffic.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: ba663b74-51f4-11ec-bf63-0242ac130002
name: PaloAlto - Dropping or denying session with traffic
description: |
'Detects dropping or denying session with traffic.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 100;
PaloAltoCDLEvent
| where EventResourceId =~ 'TRAFFIC'
| where EventResult has_any ("deny", "drop", "reject")
| where tolong(DstBytes) > 0
| where tolong(NetworkPackets) > 0
| summarize count() by SrcIpAddr, DstUsername, bin(TimeGenerated, 10m)
| where count_ > threshold
| extend AccountCustomEntity = DstUsername, IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
Stage 1: source
PaloAltoCDLEvent
Stage 2: where
| where EventResourceId =~ 'TRAFFIC'
Stage 3: where
| where EventResult has_any ("deny", "drop", "reject")
Stage 4: where
| where tolong(DstBytes) > 0
Stage 5: where
| where tolong(NetworkPackets) > 0
Stage 6: summarize
| summarize count() by SrcIpAddr, DstUsername, bin(TimeGenerated, 10m)
Stage 7: where
| where count_ > threshold
Stage 8: extend
| extend AccountCustomEntity = DstUsername, IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstBytes | gt |
| field:"DstBytes" kind:gt value:"0" |
EventResourceId | eq |
| field:"EventResourceId" kind:eq value:"TRAFFIC" |
EventResult | match |
| field:"EventResult" kind:match |
NetworkPackets | gt |
| field:"NetworkPackets" kind:gt value:"0" |
count_ | gt |
| field:"count_" kind:gt value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUsername | summarize |
SrcIpAddr | summarize |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Process Tree Analysis
#Below queries perform process tree analysis on MDE/MDATP, Azure Sentinel (Sysmon), and Splunk (Sysmon) and displays anomalous trees. All queries run smoothly even in the large environments. Detailed explanation is here
References
Telemetry Coverage
Rule Body
let timeframe = 48h;
// Define of which processes you want to generate process tree
let _selected_processes = dynamic(["winword.exe","excel.exe","powerpnt.exe","acrord32.exe", "FoxitPhantomPDF.exe","MicrosoftPdfReader.exe","SumatraPDF.exe", "onenote.exe"]);
// First, generate the process tree and store it in the cache.
// Renaming fields accordingly to generate a tree up to 7th level
// In each step, project only the required fields to optimize resource usage
let _process_tree_data= materialize
( DeviceProcessEvents
| where Timestamp > ago(timeframe)
| where InitiatingProcessFileName in~ (_selected_processes )
| project DeviceId,DeviceName,
InitiatingProcessG3ParentFileName=FileName,InitiatingProcessG3ParentSHA1=SHA1,InitiatingProcessG3ParentId=ProcessId, InitiatingProcessG3ParentCommandLine=ProcessCommandLine,InitiatingProcessG3ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG4ParentFileName=InitiatingProcessFileName,InitiatingProcessG4ParentSHA1=InitiatingProcessSHA1,InitiatingProcessG4ParentId=InitiatingProcessId,InitiatingProcessG4ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG4ParentCreationTime=todatetime(InitiatingProcessCreationTime)
// Start iteration
// 1st iteration of join. From now on, query all processes, rename fields, and join accordingly
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessG2ParentFileName=FileName,InitiatingProcessG2ParentFolderPath=FolderPath,InitiatingProcessG2ParentSHA1=SHA1, InitiatingProcessG2ParentId=ProcessId, InitiatingProcessG2ParentCommandLine=ProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG3ParentFileName=InitiatingProcessFileName,InitiatingProcessG3ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG3ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG3ParentId=InitiatingProcessId, InitiatingProcessG3ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG3ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime
// 2nd iteration of join.
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessG1ParentFileName=FileName,InitiatingProcessG1ParentFolderPath=FolderPath,InitiatingProcessG1ParentSHA1=SHA1, InitiatingProcessG1ParentId=ProcessId, InitiatingProcessG1ParentCommandLine=ProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(ProcessCreationTime),
InitiatingProcessG2ParentFileName=InitiatingProcessFileName,InitiatingProcessG2ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG2ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG2ParentId=InitiatingProcessId, InitiatingProcessG2ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG2ParentFileName , InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime
// 3rd iteration of join.
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessParentFileName=FileName,InitiatingProcessParentFolderPath=FolderPath,InitiatingProcessParentSHA1=SHA1, InitiatingProcessParentId=ProcessId, InitiatingProcessParentCommandLine=ProcessCommandLine, InitiatingProcessParentCreationTime=ProcessCreationTime,
InitiatingProcessG1ParentFileName=InitiatingProcessFileName,InitiatingProcessG1ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG1ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG1ParentId=InitiatingProcessId, InitiatingProcessG1ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(InitiatingProcessCreationTime)
)
on DeviceId , InitiatingProcessG1ParentFileName , InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime
// 4th iteration of join
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project DeviceId, InitiatingProcessFileName=FileName,InitiatingProcessSHA1=SHA1, InitiatingProcessId=ProcessId, InitiatingProcessCommandLine=ProcessCommandLine, InitiatingProcessCreationTime=ProcessCreationTime,
InitiatingProcessParentFileName=InitiatingProcessFileName,InitiatingProcessParentSHA1=InitiatingProcessSHA1, InitiatingProcessParentId=InitiatingProcessId, InitiatingProcessParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessParentCreationTime=InitiatingProcessCreationTime
)
on DeviceId , InitiatingProcessParentFileName , InitiatingProcessParentId, InitiatingProcessParentCreationTime
// 5th iteration of join
| join kind=leftouter (
DeviceProcessEvents
| where Timestamp > ago(timeframe)
| project Timestamp, DeviceId, FileName,SHA1, ProcessId, ProcessCommandLine, ProcessCreationTime,
InitiatingProcessFileName,InitiatingProcessSHA1, InitiatingProcessId, InitiatingProcessCommandLine, InitiatingProcessCreationTime
)
on DeviceId , InitiatingProcessFileName , InitiatingProcessId, InitiatingProcessCreationTime
);
// Use the cached results and find the rare patterns based on process names.
_process_tree_data
|summarize count() by FileName,InitiatingProcessFileName,InitiatingProcessParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG3ParentFileName,InitiatingProcessG4ParentFileName
| where count_ < 10 // If the count of a pattern is less than 10, it is anomalous. Threshold can be changed.
// Now, join the anomalous patterns with the original results to get the details.
| join kind=inner _process_tree_data on FileName,InitiatingProcessFileName,InitiatingProcessParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG3ParentFileName,InitiatingProcessG4ParentFileName
// Now, join the anomalous patterns with the original results to get the details.
|project Timestamp=case(isnotempty(Timestamp),Timestamp,isnotempty(InitiatingProcessParentCreationTime),InitiatingProcessParentCreationTime,isnotempty(InitiatingProcessG1ParentCreationTime),InitiatingProcessG1ParentCreationTime,
isnotempty(InitiatingProcessG2ParentCreationTime),InitiatingProcessG2ParentCreationTime,isnotempty(InitiatingProcessG3ParentCreationTime),InitiatingProcessG3ParentCreationTime,InitiatingProcessG4ParentCreationTime),
count_ , DeviceId, DeviceName,
InitiatingProcessG4ParentFileName,InitiatingProcessG3ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessParentFileName,InitiatingProcessFileName,FileName,
InitiatingProcessG4ParentCommandLine, InitiatingProcessG3ParentCommandLine, InitiatingProcessG2ParentCommandLine, InitiatingProcessG1ParentCommandLine, InitiatingProcessCommandLine, ProcessCommandLine,
InitiatingProcessG4ParentId, InitiatingProcessG4ParentCreationTime,
InitiatingProcessG3ParentId, InitiatingProcessG3ParentFolderPath ,InitiatingProcessG3ParentSHA1, InitiatingProcessG3ParentCreationTime,
InitiatingProcessG2ParentId,InitiatingProcessG2ParentFolderPath,InitiatingProcessG2ParentSHA1, InitiatingProcessG2ParentCreationTime,
InitiatingProcessG1ParentId,InitiatingProcessG1ParentFolderPath,InitiatingProcessG1ParentSHA1, InitiatingProcessG1ParentCreationTime,
InitiatingProcessParentId, InitiatingProcessParentFolderPath,InitiatingProcessParentSHA1, InitiatingProcessParentCommandLine ,InitiatingProcessParentCreationTime,
InitiatingProcessId, InitiatingProcessSHA1, InitiatingProcessCreationTime,
ProcessId, SHA1, ProcessCreationTime
| order by Timestamp, DeviceName, InitiatingProcessG4ParentCreationTime , InitiatingProcessG3ParentCreationTime , InitiatingProcessG2ParentCreationTime , InitiatingProcessG1ParentCreationTime , InitiatingProcessCreationTime
let _timeframe = 1d;
// define of which processes you want to generate process tree
let _selected_processes = dynamic(["winword.exe","excel.exe","powerpnt.exe","acrord32.exe", "FoxitPhantomPDF.exe","MicrosoftPdfReader.exe","SumatraPDF.exe", "onenote.exe"]);
// Sysmon logs are not parsed automatically, below function parses the Sysmon EventID=1 logs.
let parse_sysmon_1 = (T:(TimeGenerated:datetime,EventID:int, Source:string,RenderedDescription:string, EventData:string))
{
T
| where TimeGenerated > ago(_timeframe)
| where Source == "Microsoft-Windows-Sysmon" and EventID == 1
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""), TechniqueName = column_ifexists("TechniqueName", "")
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
};
// First, generate the process tree and store it in the cache
let _process_tree_data = materialize (
// First, get only the processes created by selected process
Event
| invoke parse_sysmon_1() //parsing the sysmon logs
// Get only the required fields. Rrenaming fields accordingly to generate a tree up to 5th level
| project Computer,G2_ParentProcessId=tostring(ProcessId),G2_ParentProcess=tostring(OriginalFileName),G2_ParentProcessGuid=tostring(ProcessGuid),G2_ParentProcessCommandLine=tostring(CommandLine),
G3_ParentProcessId=tostring(ParentProcessId),G3_ParentProcess=tostring(parse_path(tostring(ParentImage)).Filename),G3_ParentProcessGuid=tostring(ParentProcessGuid),G3_ParentProcessCommandLine=tostring(ParentCommandLine)
| where G3_ParentProcess in~ (_selected_processes)
// Start iteration
// First iteration of join. From now on, query all processes, rename fields, and join accordingly
| join kind=leftouter
(
Event
| invoke parse_sysmon_1()
| project G1_ParentProcessId=tostring(ProcessId),G1_ParentProcess=tostring(OriginalFileName),G1_ParentProcessGuid=tostring(ProcessGuid),G1_ParentProcessCommandLine=tostring(CommandLine),
G2_ParentProcessId=tostring(ParentProcessId),G2_ParentProcess=tostring(parse_path(tostring(ParentImage)).Filename),G2_ParentProcessGuid=tostring(ParentProcessGuid),G2_ParentProcessCommandLine=tostring(ParentCommandLine)
) on G2_ParentProcessGuid
// Second iteration of join.
| join kind=leftouter
(
Event
| invoke parse_sysmon_1()
| project ParentProcessId=tostring(ProcessId),ParentProcess=tostring(OriginalFileName),ParentProcessGuid=tostring(ProcessGuid),ParentProcessCommandLine=tostring(CommandLine),
G1_ParentProcessId=tostring(ParentProcessId),G1_ParentProcess=tostring(parse_path(tostring(ParentImage)).Filename),G1_ParentProcessGuid=tostring(ParentProcessGuid),G1_ParentProcessCommandLine=tostring(ParentCommandLine)
) on G1_ParentProcessGuid
// Third iteration of join.
| join kind=leftouter
(
Event
| invoke parse_sysmon_1()
| project ProcessId=tostring(ProcessId),Process=tostring(OriginalFileName),ProcessGuid=tostring(ProcessGuid),ProcessCommandLine=tostring(CommandLine),
ParentProcessId=tostring(ParentProcessId),ParentProcess=tostring(parse_path(tostring(ParentImage)).Filename),ParentProcessGuid=tostring(ParentProcessGuid),ParentProcessCommandLine=tostring(ParentCommandLine)
) on ParentProcessGuid
);
// Use the cached results and find the rare patterns based on process names.
_process_tree_data
| summarize count() by Process, ParentProcess, G1_ParentProcess, G2_ParentProcess, G3_ParentProcess
| where count_ < 10 // If the count of a pattern is less than 10, it is anomalous. Threshold can be changed.
// Now, join the anomalous patterns with the original results to get the details.
| join kind=inner _process_tree_data on Process, ParentProcess, G1_ParentProcess, G2_ParentProcess, G3_ParentProcess
| project-reorder Computer, count_, G3_ParentProcess, G2_ParentProcess, G1_ParentProcess, ParentProcess, Process,
G3_ParentProcessCommandLine, G2_ParentProcessCommandLine, G1_ParentProcessCommandLine, ParentProcessCommandLine, ProcessCommandLine,
G3_ParentProcessId, G2_ParentProcessId, G1_ParentProcessId, ParentProcessId, ProcessId
Stages and Predicates
Parameters
let timeframe = 48h;
let _selected_processes = dynamic(["winword.exe","excel.exe","powerpnt.exe","acrord32.exe", "FoxitPhantomPDF.exe","MicrosoftPdfReader.exe","SumatraPDF.exe", "onenote.exe"]);
let _process_tree_data is inlined into the numbered stages below.
Stage 1: source
let _process_tree_data
Stage 2: source
DeviceProcessEvents
Stage 3: where
where Timestamp > ago(172800s)
Stage 4: where
where InitiatingProcessFileName in~ ("FoxitPhantomPDF.exe", "MicrosoftPdfReader.exe", "SumatraPDF.exe", "acrord32.exe", "excel.exe", "onenote.exe", "powerpnt.exe", "winword.exe")
Stage 5: project
project DeviceId, DeviceName, InitiatingProcessG3ParentCommandLine, InitiatingProcessG3ParentCreationTime, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentSHA1, InitiatingProcessG4ParentCommandLine, InitiatingProcessG4ParentCreationTime, InitiatingProcessG4ParentFileName, InitiatingProcessG4ParentId, InitiatingProcessG4ParentSHA1
Stage 6: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime
Stage 7: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG2ParentFileName, InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime
Stage 8: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessG1ParentFileName, InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime
Stage 9: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessParentFileName, InitiatingProcessParentId, InitiatingProcessParentCreationTime
Stage 10: join
join kind=leftouter (DeviceProcessEvents) on DeviceId, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessCreationTime
Stage 11: summarize
summarize by FileName, InitiatingProcessFileName, InitiatingProcessParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG3ParentFileName, InitiatingProcessG4ParentFileName
Stage 12: where
where count_ < 10
Stage 13: join
join kind=inner (_process_tree_data) on FileName, InitiatingProcessFileName, InitiatingProcessParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG3ParentFileName, InitiatingProcessG4ParentFileName
Stage 14: project
project DeviceId, DeviceName, FileName, InitiatingProcessCommandLine, InitiatingProcessCreationTime, InitiatingProcessFileName, InitiatingProcessG1ParentCommandLine, InitiatingProcessG1ParentCreationTime, InitiatingProcessG1ParentFileName, InitiatingProcessG1ParentFolderPath, InitiatingProcessG1ParentId, InitiatingProcessG1ParentSHA1, InitiatingProcessG2ParentCommandLine, InitiatingProcessG2ParentCreationTime, InitiatingProcessG2ParentFileName, InitiatingProcessG2ParentFolderPath, InitiatingProcessG2ParentId, InitiatingProcessG2ParentSHA1, InitiatingProcessG3ParentCommandLine, InitiatingProcessG3ParentCreationTime, InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentFolderPath, InitiatingProcessG3ParentId, InitiatingProcessG3ParentSHA1, InitiatingProcessG4ParentCommandLine, InitiatingProcessG4ParentCreationTime, InitiatingProcessG4ParentFileName, InitiatingProcessG4ParentId, InitiatingProcessId, InitiatingProcessParentCommandLine, InitiatingProcessParentCreationTime, InitiatingProcessParentFileName, InitiatingProcessParentFolderPath, InitiatingProcessParentId, InitiatingProcessParentSHA1, InitiatingProcessSHA1, ProcessCommandLine, ProcessCreationTime, ProcessId, SHA1, Timestamp, count_
Stage 15: sort
sort by DeviceName, InitiatingProcessCreationTime, InitiatingProcessG1ParentCreationTime, InitiatingProcessG2ParentCreationTime, InitiatingProcessG3ParentCreationTime, InitiatingProcessG4ParentCreationTime, Timestamp
Stage 16: source
let _process_tree_data
Stage 17: source
Event
Stage 18: where
where TimeGenerated > ago(86400s)
Stage 19: where
where EventID == 1 and Source == "Microsoft-Windows-Sysmon"
Stage 20: extend
extend RenderedDescription
Stage 21: extend
extend EventData
Stage 22: mv-expand
mv-expand EventData
Stage 23: evaluate
evaluate
Stage 24: extend
extend Key, Value
Stage 25: evaluate
evaluate
Stage 26: extend
extend RuleName, TechniqueId, TechniqueName
Stage 27: parse
parse
Stage 28: project
project Computer, G2_ParentProcess, G2_ParentProcessCommandLine, G2_ParentProcessGuid, G2_ParentProcessId, G3_ParentProcess, G3_ParentProcessCommandLine, G3_ParentProcessGuid, G3_ParentProcessId
Stage 29: where
where G3_ParentProcess in~ ("FoxitPhantomPDF.exe", "MicrosoftPdfReader.exe", "SumatraPDF.exe", "acrord32.exe", "excel.exe", "onenote.exe", "powerpnt.exe", "winword.exe")
Stage 30: join
join kind=leftouter (Event) on G2_ParentProcessGuid
Stage 31: join
join kind=leftouter (Event) on G1_ParentProcessGuid
Stage 32: join
join kind=leftouter (Event) on ParentProcessGuid
Stage 33: summarize
summarize by Process, ParentProcess, G1_ParentProcess, G2_ParentProcess, G3_ParentProcess
Stage 34: where
where count_ < 10
Stage 35: join
join kind=inner (_process_tree_data) on Process, ParentProcess, G1_ParentProcess, G2_ParentProcess, G3_ParentProcess
Stage 36: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"1" |
G3_ParentProcess | in |
| field:"G3_ParentProcess" kind:in |
InitiatingProcessFileName | in |
| field:"InitiatingProcessFileName" kind:in |
Source | eq |
| field:"Source" kind:eq value:"Microsoft-Windows-Sysmon" |
count_ | lt |
| field:"count_" kind:lt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | project |
DeviceName | project |
FileName | project |
InitiatingProcessCommandLine | project |
InitiatingProcessCreationTime | project |
InitiatingProcessFileName | project |
InitiatingProcessG1ParentCommandLine | project |
InitiatingProcessG1ParentCreationTime | project |
InitiatingProcessG1ParentFileName | project |
InitiatingProcessG1ParentFolderPath | project |
InitiatingProcessG1ParentId | project |
InitiatingProcessG1ParentSHA1 | project |
InitiatingProcessG2ParentCommandLine | project |
InitiatingProcessG2ParentCreationTime | project |
InitiatingProcessG2ParentFileName | project |
InitiatingProcessG2ParentFolderPath | project |
InitiatingProcessG2ParentId | project |
InitiatingProcessG2ParentSHA1 | project |
InitiatingProcessG3ParentCommandLine | project |
InitiatingProcessG3ParentCreationTime | project |
InitiatingProcessG3ParentFileName | project |
InitiatingProcessG3ParentFolderPath | project |
InitiatingProcessG3ParentId | project |
InitiatingProcessG3ParentSHA1 | project |
InitiatingProcessG4ParentCommandLine | project |
InitiatingProcessG4ParentCreationTime | project |
InitiatingProcessG4ParentFileName | project |
InitiatingProcessG4ParentId | project |
InitiatingProcessId | project |
InitiatingProcessParentCommandLine | project |
InitiatingProcessParentCreationTime | project |
InitiatingProcessParentFileName | project |
InitiatingProcessParentFolderPath | project |
InitiatingProcessParentId | project |
InitiatingProcessParentSHA1 | project |
InitiatingProcessSHA1 | project |
ProcessCommandLine | project |
ProcessCreationTime | project |
ProcessId | project |
SHA1 | project |
Timestamp | project |
count_ | project |
Tailscale: Device started advertising subnet routes
#Identifies when a tailnet device begins advertising subnet routes (subnet-router capability) not present in the previous snapshot. Unexpected advertisement may indicate a compromised node expanding reachable surface area or an unsanctioned admin change.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Lateral Movement |
Rule Body
id: c2b3d4e5-2345-6789-01ab-cdef12345002
name: "Tailscale: Device started advertising subnet routes"
description: |
Identifies when a tailnet device begins advertising subnet routes (subnet-router capability) not present in the previous snapshot. Unexpected advertisement may indicate a compromised node expanding reachable surface area or an unsanctioned admin change.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
- Persistence
relevantTechniques:
- T1021
- T1556
query: |
let recent =
Tailscale_Devices_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0;
let baseline =
Tailscale_Devices_CL
| where TimeGenerated between (ago(1d + 1h) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0
| distinct DeviceId;
recent
| join kind=leftanti baseline on DeviceId
| project TimeGenerated, DeviceId, DeviceName, Hostname, User, AdvertisedRoutes, EnabledRoutes, LastSeen
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
let recent is inlined into the numbered stages below.
Let Binding: baseline
let baseline = Tailscale_Devices_CL
| where TimeGenerated between (ago(1d + 1h) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0
| distinct DeviceId;
Stages 1 to 4 define let recent as the rule's main pipeline source. Stages 5 to 6 use it.
Stage 1: source
Tailscale_Devices_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by DeviceId
Stage 4: where
| where array_length(AdvertisedRoutes) > 0
Stage 5: join (negated)
join kind=leftanti (baseline) on DeviceId
Stage 6: project
project AdvertisedRoutes, DeviceId, DeviceName, EnabledRoutes, Hostname, LastSeen, TimeGenerated, User
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AdvertisedRoutes | gt | 0 | excludes:AdvertisedRoutes field:"AdvertisedRoutes" value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AdvertisedRoutes | project |
DeviceId | project |
DeviceName | project |
EnabledRoutes | project |
Hostname | project |
LastSeen | project |
TimeGenerated | project |
User | project |
UniFi Site Manager: New Device Adopted
#Identifies when a new device is adopted into the UniFi network. While often legitimate, unexpected adoptions may indicate unauthorized hardware additions.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Rule Body
id: e69be544-9476-35bb-2533-fa8c650dcd46
name: "UniFi Site Manager: New Device Adopted"
description: |
Identifies when a new device is adopted into the UniFi network. While often legitimate, unexpected adoptions may indicate unauthorized hardware additions.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Devices_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1200
- T1133
subTechniques: []
query: |
// UniFi New Device Adopted Detection
Unifi_SiteManager_Devices_CL
| where TimeGenerated > ago(1h)
| where isnotempty(AdoptionTime)
| extend AdoptionDateTime = todatetime(AdoptionTime)
| where AdoptionDateTime > ago(1h)
| summarize arg_max(TimeGenerated, *) by Id
| extend
DeviceName = coalesce(Name, "Unnamed"),
DeviceId = Id,
Model = Model,
IPAddress = Ip,
MACAddress = Mac,
ProductLine = ProductLine,
FirmwareVersion = Version,
Shortname = Shortname
| extend HostName = DeviceName
| project
TimeGenerated,
AdoptionTime = AdoptionDateTime,
DeviceName,
DeviceId,
Model,
IPAddress,
MACAddress,
ProductLine,
Status = Status,
FirmwareVersion,
Shortname,
HostName
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT24H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
Unifi_SiteManager_Devices_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where isnotempty(AdoptionTime)
Stage 4: extend
| extend AdoptionDateTime = todatetime(AdoptionTime)
Stage 5: where
| where AdoptionDateTime > ago(1h)
Stage 6: summarize
| summarize arg_max(TimeGenerated, *) by Id
Stage 7: extend
| extend
DeviceName = coalesce(Name, "Unnamed"),
DeviceId = Id,
Model = Model,
IPAddress = Ip,
MACAddress = Mac,
ProductLine = ProductLine,
FirmwareVersion = Version,
Shortname = Shortname
Stage 8: extend
| extend HostName = DeviceName
Stage 9: project
| project
TimeGenerated,
AdoptionTime = AdoptionDateTime,
DeviceName,
DeviceId,
Model,
IPAddress,
MACAddress,
ProductLine,
Status = Status,
FirmwareVersion,
Shortname,
HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdoptionTime | is_not_null | field:"AdoptionTime" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AdoptionTime | project |
DeviceId | project |
DeviceName | project |
FirmwareVersion | project |
HostName | project |
IPAddress | project |
MACAddress | project |
Model | project |
ProductLine | project |
Shortname | project |
Status | project |
TimeGenerated | project |