Detection rules › Kusto
Kusto rules: 16
AWSCloudTrail - Privilege escalation via SSM policy
#Detects inline IAM policy updates that grant broad AWS Systems Manager permissions. This can enable command execution and control actions across managed instances, representing a potential privilege escalation route.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
Rule Body
id: c668c09f-5a49-43f9-b249-6b89a31ec8fb
name: AWSCloudTrail - Privilege escalation via SSM policy
description: |
Detects inline IAM policy updates that grant broad AWS Systems Manager permissions. This can enable command
execution and control actions across managed instances, representing a potential privilege escalation route.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1098.003
query: |
AWSCloudTrail
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| 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 "ssm:*" or Action contains "ssm:Create*" or Action contains "ssm:CreateAssociation") and Resource == "*" and isempty(Condition)
| 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]), "")
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
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
UserIdentityArn: UserIdentityArn
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS SSM privilege escalation policy update by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline SSM escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 4: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 5: mv-expand
| mvexpand Statement
Stage 6: 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 7: extend
| extend Action = tostring(Action)
Stage 8: where
| where Effect =~ "Allow" and (Action contains "ssm:*" or Action contains "ssm:Create*" or Action contains "ssm:CreateAssociation") and Resource == "*" and isempty(Condition)
Stage 9: 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 10: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Condition | is_null | field:"Condition" kind:is_null | |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::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 |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Azure Portal sign in from another Azure Tenant
#This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant, and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look to pivot to other tenants leveraging cross-tenant delegated access in this manner.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- 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 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: 87210ca1-49a4-4a7d-bb4a-4988752f978c
name: Azure Portal sign in from another Azure Tenant
description: |
'This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant, and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look to pivot to other tenants leveraging cross-tenant delegated access in this manner.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1199
query: |
// Get details of current Azure Ranges (note this URL updates regularly so will need to be manually updated over time)
// You may find the name of the new JSON here: https://www.microsoft.com/download/details.aspx?id=56519
// On the downloads page, click the 'details' button, and then replace just the filename in the URL below
let azure_ranges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
| mv-expand values
| mv-expand values.properties.addressPrefixes
| mv-expand values_properties_addressPrefixes
| summarize by tostring(values_properties_addressPrefixes)
| extend isipv4 = parse_ipv4(values_properties_addressPrefixes)
| extend isipv6 = parse_ipv6(values_properties_addressPrefixes)
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
| summarize make_list(values_properties_addressPrefixes) by ip_type
;
SigninLogs
// Limiting to Azure Portal really reduces false positives and helps focus on potential admin activity
| where ResultType == 0
| where AppDisplayName =~ "Azure Portal"
| extend isipv4 = parse_ipv4(IPAddress)
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
// Only get logons where the IP address is in an Azure range
| join kind=fullouter (azure_ranges) on ip_type
| extend ipv6_match = ipv6_is_in_any_range(IPAddress, list_values_properties_addressPrefixes)
| extend ipv4_match = ipv4_is_in_any_range(IPAddress, list_values_properties_addressPrefixes)
| where ipv4_match or ipv6_match
// Limit to where the user is external to the tenant
| where HomeTenantId != ResourceTenantId
// Further limit it to just access to the current tenant (you can drop this if you wanted to look elsewhere as well but it helps reduce FPs)
| where ResourceTenantId == AADTenantId
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), make_set(ResourceDisplayName) by UserPrincipalName, IPAddress, UserAgent, Location, HomeTenantId, ResourceTenantId, UserId
| extend AccountName = split(UserPrincipalName, "@")[0]
| extend UPNSuffix = split(UserPrincipalName, "@")[1]
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: UserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
alertDetailsOverride:
alertDisplayNameFormat: Azure Portal sign in by {{UserPrincipalName}} from another Azure Tenant with IP Address {{IPAddress}}
alertDescriptionFormat: |
This query looks for successful sign in attempts to the Azure Portal where the user who is signing in from another Azure tenant,
and the IP address the login attempt is from is an Azure IP. A threat actor who compromises an Azure tenant may look
to pivot to other tenants leveraging cross-tenant delegated access in this manner.
In this instance {{UserPrincipalName}} logged in at {{FirstSeen}} from IP Address {{IPAddress}}.
version: 2.0.3
kind: Scheduled
Stages and Predicates
Let Binding: azure_ranges
let azure_ranges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
| mv-expand values
| mv-expand values.properties.addressPrefixes
| mv-expand values_properties_addressPrefixes
| summarize by tostring(values_properties_addressPrefixes)
| extend isipv4 = parse_ipv4(values_properties_addressPrefixes)
| extend isipv6 = parse_ipv6(values_properties_addressPrefixes)
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
| summarize make_list(values_properties_addressPrefixes) by ip_type;
Stage 1: source
SigninLogs
Stage 2: where
| where ResultType == 0
Stage 3: where
| where AppDisplayName =~ "Azure Portal"
Stage 4: extend
| extend isipv4 = parse_ipv4(IPAddress)
Stage 5: extend
| extend ip_type = case(isnotnull(isipv4), "v4", "v6")
ip_type =isnotnull(isipv4)"v4""v6"Stage 6: join
join kind=fullouter (azure_ranges) on ip_type
Stage 7: extend
extend ipv6_match
Stage 8: extend
extend ipv4_match
Stage 9: where
where (ipv4_match == true or ipv6_match == true)
Stage 10: where
where HomeTenantId != ResourceTenantId
Stage 11: where
where ResourceTenantId =~ AADTenantId
Stage 12: summarize
summarize FirstSeen, LastSeen by UserPrincipalName, IPAddress, UserAgent, Location, HomeTenantId, ResourceTenantId, UserId
Stage 13: extend
extend AccountName
Stage 14: extend
extend UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppDisplayName | eq |
| field:"Application" kind:eq value:"Azure Portal" |
ResultType | eq |
| field:"ResultType" kind:eq value:"0" |
ipv4_match | eq |
| field:"ipv4_match" kind:eq value:"true" |
ipv6_match | eq |
| field:"ipv6_match" kind:eq value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FirstSeen | summarize |
HomeTenantId | summarize |
IPAddress | summarize |
LastSeen | summarize |
Location | summarize |
ResourceTenantId | summarize |
UserAgent | summarize |
UserId | summarize |
UserPrincipalName | summarize |
AccountName | extend |
UPNSuffix | extend |
BloodHound Attack Path Finding - Avere Contributor Role on Tier Zero Virtual Machine
#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 - Avere Contributor Role on Tier Zero Virtual Machine
Rule Body
id: 9bd607bf-a64c-4917-913e-e9a33f4ea28e
name: BloodHound Attack Path Finding - Avere Contributor Role on Tier Zero Virtual
Machine
description: BloodHound Attack Path Finding - Avere Contributor Role on Tier Zero
Virtual Machine
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: 'BHEAttackPathsData_CL
| where Finding == "AzureT0AvereContributor"'
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 == "AzureT0AvereContributor"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0AvereContributor" |
Changes to Application Logout URL
#Detects changes to an applications sign out URL. Look for any modifications to a sign out URL. Blank entries or entries to non-existent locations would stop a user from terminating a session. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#logout-url-modified-or-removed
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule Body
id: 492fbe35-cbac-4a8c-9059-826782e6915a
name: Changes to Application Logout URL
description: |
'Detects changes to an applications sign out URL.
Look for any modifications to a sign out URL. Blank entries or entries to non-existent locations would stop a user from terminating a session.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#logout-url-modified-or-removed'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
AuditLogs
| where Category =~ "ApplicationManagement"
| where OperationName has_any ("Update Application", "Update Service principal")
| 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(InitiatedBy.user.ipAddress)
| extend TargetAppName = tostring(TargetResources[0].displayName)
| extend mod_props = TargetResources[0].modifiedProperties
| mv-expand mod_props
| extend Action = tostring(mod_props.displayName)
| where Action contains "Url"
| extend UpdatedBy = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
| extend OldURL = tostring(mod_props.oldValue)
| extend NewURL = tostring(mod_props.newValue)
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| project-reorder TimeGenerated, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingAadUserId, InitiatingUserPrincipalName, InitiatingIPAddress, UpdatedBy
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: URL
fieldMappings:
- identifier: Url
columnName: OldURL
- entityType: URL
fieldMappings:
- identifier: Url
columnName: NewURL
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIPAddress
version: 1.1.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "ApplicationManagement"
Stage 3: where
| where OperationName has_any ("Update Application", "Update Service principal")
Stage 4: extend (7 consecutive steps)
| 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(InitiatedBy.user.ipAddress)
| extend TargetAppName = tostring(TargetResources[0].displayName)
| extend mod_props = TargetResources[0].modifiedProperties
Stage 5: mv-expand
| mv-expand mod_props
Stage 6: extend
| extend Action = tostring(mod_props.displayName)
Stage 7: where
| where Action contains "Url"
Stage 8: extend (4 consecutive steps)
| extend UpdatedBy = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
| extend OldURL = tostring(mod_props.oldValue)
| extend NewURL = tostring(mod_props.newValue)
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Stage 9: project-reorder
| project-reorder TimeGenerated, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingAadUserId, InitiatingUserPrincipalName, InitiatingIPAddress, UpdatedBy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains value:"Url" |
Category | eq |
| field:"Category" kind:eq value:"ApplicationManagement" |
OperationName | match |
| field:"OperationName" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIPAddress | extend |
TargetAppName | extend |
mod_props | extend |
Action | extend |
UpdatedBy | extend |
OldURL | extend |
NewURL | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
CiscoISE - ISE administrator password has been reset
#'Detects when the ISE administrator password has been reset.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Rule Body
id: e63b4d90-d0a8-4609-b187-babfcc7f86d7
name: CiscoISE - ISE administrator password has been reset
description: |
'Detects when the ISE administrator password has been reset.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
query: |
let lbtime = 5m;
CiscoISEEvent
| where TimeGenerated > ago(lbtime)
| where EventId == '58019'
| project TimeGenerated, 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.3
kind: Scheduled
Stages and Predicates
Parameters
let lbtime = 5m;
Stage 1: source
CiscoISEEvent
Stage 2: where
| where TimeGenerated > ago(lbtime)
Stage 3: where
| where EventId == '58019'
Stage 4: project
| project TimeGenerated, SrcIpAddr, DstUserName
Stage 5: extend
| extend AccountCustomEntity = DstUserName
Stage 6: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventId | eq |
| field:"EventId" kind:eq value:"58019" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | project |
SrcIpAddr | project |
TimeGenerated | project |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Detect Suspicious ncrypt.dll usage by process requesting Entra ID Nonce
#This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and requesting an Entra ID Nonce. More information on the attack scenario this is detection is applicable for can be found in the references.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
References
Telemetry Coverage
Rule Body
let cli_tools = dynamic(["powershell", "python"]);
// Get all possible nonce requests
let nonce_requests = (
DeviceNetworkEvents
| where Timestamp > ago(1h)
| where ActionType startswith "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceTimestamp = Timestamp
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where Timestamp > ago(1h)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where Timestamp between (todatetime(NonceTimestamp - 10m) .. todatetime(NonceTimestamp + 10m))
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where (
// Flag CLI tools
InitiatingProcessFileName has_any (cli_tools) or
// Flag unknown processes
GlobalPrevalence < 250
)
let cli_tools = dynamic(["powershell", "python"]);
// Get all possible nonce requests
let nonce_requests = (
DeviceNetworkEvents
| where TimeGenerated > ago(1h)
| where ActionType startswith "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceTimestamp = TimeGenerated
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where TimeGenerated > ago(1h)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where TimeGenerated between (todatetime(NonceTimestamp - 10m) .. todatetime(NonceTimestamp + 10m))
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where (
// Flag CLI tools
InitiatingProcessFileName has_any (cli_tools) or
// Flag unknown processes
GlobalPrevalence < 250
)
Stages and Predicates
Parameters
let cli_tools = dynamic(["powershell", "python"]);
Let Binding: nonce_requests
let nonce_requests = (
DeviceNetworkEvents
| where Timestamp > ago(1h)
| where ActionType startswith "ConnectionSuccess"
| where RemoteUrl =~ "login.microsoftonline.com"
| project-rename NonceTimestamp = Timestamp
);
Stage 1: source
let nonce_requests
Stage 2: source
DeviceEvents
Stage 3: where
where Timestamp > ago(3600s)
Stage 4: where
where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
Stage 5: join
join kind=inner (nonce_requests) on InitiatingProcessId, DeviceId
Stage 6: where
where /* macro: (Timestamp between (todatetime((NonceTimestamp - 10m)) .. todatetime((NonceTimestamp + 10m)))) */
Stage 7: invoke
invoke
Stage 8: where
where (GlobalPrevalence < 250 or (InitiatingProcessFileName has "powershell" or InitiatingProcessFileName has "python"))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | starts_with |
| field:"ActionType" kind:starts_with |
FileName | eq |
| field:"FileName" kind:eq value:"ncrypt.dll" |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"250" |
InitiatingProcessFileName | match |
| field:"InitiatingProcessFileName" kind:match |
RemoteUrl | eq |
| field:"RemoteUrl" kind:eq value:"login.microsoftonline.com" |
Flare cloud bucket results
#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 query searches for events related to publicly available cloud bucket.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 9cb7c337-f172-4af6-b0e8-b6b7552d762d
name: Flare cloud bucket results
description: |
'This query searches for events related to publicly available cloud bucket.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Flare
dataTypes:
- FireworkV2_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1593
query: |
FireworkV2_CL
| where notempty(uid) and RiskScore >= 3
| extend index_name = split(uid, "/")[0]
| where index_name == "driller_bucket_object" or index_name == "bucket"
version: 3.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
FireworkV2_CL
Stage 2: where
| where notempty(uid) and RiskScore >= 3
Stage 3: extend
| extend index_name = split(uid, "/")[0]
Stage 4: where
| where index_name == "driller_bucket_object" or index_name == "bucket"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RiskScore | ge |
| field:"RiskScore" kind:ge value:"3" |
index_name | eq |
| field:"index_name" kind:eq |
uid | is_not_null | field:"uid" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
index_name | extend |
GitHub Activites from a New Country
#'Detect activities from a location that was not recently or was never visited by the user or by any user in your organization.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: f041e01d-840d-43da-95c8-4188f6cef546
name: GitHub Activites from a New Country
description: |
'Detect activities from a location that was not recently or was never visited by the user or by any user in your organization.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let LearningPeriod = 7d;
let RunTime = 1h;
let StartTime = 1h;
let EndRunTime = StartTime - RunTime;
let EndLearningTime = StartTime + LearningPeriod;
let GitHubCountryCodeLogs = (GitHubAuditData
| where Country != "");
GitHubCountryCodeLogs
| where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))
| summarize makeset(Country) by Actor
| join kind=innerunique (
GitHubCountryCodeLogs
| where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
| distinct Country, Actor, TimeGenerated
) on Actor
| where set_Country !contains Country
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Actor
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let LearningPeriod = 7d;
let RunTime = 1h;
let StartTime = 1h;
let EndRunTime = StartTime - RunTime;
let EndLearningTime = StartTime + LearningPeriod;
let GitHubCountryCodeLogs is inlined into the numbered stages below.
Stages 1 to 2 define let GitHubCountryCodeLogs as the rule's main pipeline source. Stages 3 to 8 use it.
Stage 1: source
GitHubAuditData
Stage 2: where
| where Country != ""
Stage 3: where
GitHubCountryCodeLogs
| where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))
Stage 4: summarize
| summarize makeset(Country) by Actor
Stage 5: join
| join kind=innerunique (
GitHubCountryCodeLogs
| where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
| distinct Country, Actor, TimeGenerated
) on Actor
Stage 6: where
where set_Country !contains Country
Stage 7: extend
extend timestamp
Stage 8: extend
extend AccountName, AccountUPNSuffix
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actor | summarize |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Illumio Firewall Tampering Analytic Rule
#'Create Microsoft Sentinel Incident When Firewall Is Tampered With'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: e9e4e466-3970-4165-bc8d-7721c6ef34a6
name: Illumio Firewall Tampering Analytic Rule
description: |
'Create Microsoft Sentinel Incident When Firewall Is Tampered With'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: IllumioSaaSDataConnector
dataTypes:
- Illumio_Auditable_Events_CL
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 60m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
Illumio_Auditable_Events_CL
| union IllumioSyslogAuditEvents
| where event_type has 'tampering'
| extend ipaddress = action.src_ip,
hostname = created_by.agent.hostname,
ven_href = created_by.ven.href
| project-away resource_changes, action, version
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: hostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipaddress
alertDetailsOverride:
alertDisplayNameFormat: |
Illumio Firewall Tamper Incident for {{hostname}}
alertDescriptionFormat: |
Illumio Firewall Tamper Incident for {{hostname}} generated at {{TimeGenerated}}
version: 1.0.7
kind: Scheduled
Stages and Predicates
Stage 1: source
Illumio_Auditable_Events_CL
Stage 2: union
union of 1 branches
Stage 3: source
IllumioSyslogAuditEvents
Stage 4: where
where event_type has "tampering"
Stage 5: extend
extend hostname, ipaddress, ven_href
Stage 6: project-away
project-away action, resource_changes, version
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
event_type | match |
| field:"event_type" kind:match value:"tampering" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
hostname | extend |
ipaddress | extend |
ven_href | extend |
MFA Fatigue (OKTA)
#MFA fatigue attack is a cybersecurity threat where attackers exploit user exhaustion from multi-factor authentication prompts to trick them into approving multi-factor authentication prompts, potentially compromising their own security. The query identifies MFA fatigue attempts in Okta by correlating repeated Okta Verify push requests, denied pushes, and eventual successful MFA authentication within the same session. Ref: https://www.okta.com/blog/identity-security/mfa-fatigue-growing-security-concern/.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- Okta AiTM Phishing Attempt Blocked by FastPass (Panther)
- Okta Authentication Failed During MFA Challenge (Splunk)
- Okta Fast Pass phishing Detection (Kusto)
- Okta FastPass Phishing Detection (Sigma)
- Okta FastPass Phishing Detection (Elastic)
- Okta MFA Bruteforce Attack (YARA-L)
- Okta MFA Exhaustion Hunt (Splunk)
Rule Body
id: c2697b81-7fe9-4f57-ba1d-de46c6f91f9c
name: MFA Fatigue (OKTA)
description: |
MFA fatigue attack is a cybersecurity threat where attackers exploit user exhaustion from multi-factor authentication prompts to trick them into approving multi-factor authentication prompts, potentially compromising their own security. The query identifies MFA fatigue attempts in Okta by correlating repeated Okta Verify push requests, denied pushes, and eventual successful MFA authentication within the same session.
Ref: https://www.okta.com/blog/identity-security/mfa-fatigue-growing-security-concern/.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: OktaSSO
dataTypes:
- Okta_CL
- connectorId: OktaSSOv2
dataTypes:
- OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1621
query: |
// Adjust threshold for MFA pushes to reduce noise
let PushThreshold = 10;
OktaSSO
| where ((eventType_s =="user.authentication.auth_via_mfa" and column_ifexists('debugContext_debugData_factor_s', '') == "OKTA_VERIFY_PUSH") or eventType_s == "system.push.send_factor_verify_push" or eventType_s == "user.mfa.okta_verify.deny_push")
| summarize IPAddress = make_set(client_ipAddress_s,100), City = make_set(client_geographicalContext_city_s,100),
successes = countif(eventType_s == "user.authentication.auth_via_mfa"),
denies = countif(eventType_s == "user.mfa.okta_verify.deny_push"),
pushes = countif(eventType_s == "system.push.send_factor_verify_push") by TimeGenerated, authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s
| summarize lasttime = max(TimeGenerated), firsttime = min(TimeGenerated),
successes = sum(successes), failures = sum(denies), pushes = sum(pushes) by authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s
| extend seconds = lasttime - firsttime
| where pushes > PushThreshold
| extend totalattempts = successes + failures
| extend finding = case(
failures == pushes and pushes > 1, "Authentication attempts not successful because multiple pushes denied",
totalattempts == 0, "Multiple pushes sent and ignored",
successes > 0 and pushes > 3, "Multiple pushes sent, eventual successful authentication!",
"Normal authentication pattern")
| project lasttime, firsttime, seconds, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, pushes, successes, failures, totalattempts, finding, outcome_result_s
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
alertDetailsOverride:
alertDisplayNameFormat: 'MFA fatigue suspected for user {{actor_displayName_s}} ({{actor_alternateId_s}})'
alertDescriptionFormat: 'Okta session {{authenticationContext_externalSessionId_s}} recorded {{pushes}} MFA pushes, over {{seconds}} seconds.'
customDetails:
SessionId: authenticationContext_externalSessionId_s
Pushes: pushes
Successes: successes
Denies: failures
TotalAttempts: totalattempts
Duration: seconds
version: 1.1.3
kind: Scheduled
Stages and Predicates
Parameters
let PushThreshold = 10;
Stage 1: source
OktaSSO
Stage 2: where
| where ((eventType_s =="user.authentication.auth_via_mfa" and column_ifexists('debugContext_debugData_factor_s', '') == "OKTA_VERIFY_PUSH") or eventType_s == "system.push.send_factor_verify_push" or eventType_s == "user.mfa.okta_verify.deny_push")
Stage 3: summarize
| summarize IPAddress = make_set(client_ipAddress_s,100), City = make_set(client_geographicalContext_city_s,100),
successes = countif(eventType_s == "user.authentication.auth_via_mfa"),
denies = countif(eventType_s == "user.mfa.okta_verify.deny_push"),
pushes = countif(eventType_s == "system.push.send_factor_verify_push") by TimeGenerated, authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s
Stage 4: summarize
| summarize lasttime = max(TimeGenerated), firsttime = min(TimeGenerated),
successes = sum(successes), failures = sum(denies), pushes = sum(pushes) by authenticationContext_externalSessionId_s, actor_alternateId_s,actor_displayName_s, outcome_result_s
Stage 5: extend
| extend seconds = lasttime - firsttime
Stage 6: where
| where pushes > PushThreshold
Stage 7: extend
| extend totalattempts = successes + failures
Stage 8: extend
| extend finding = case(
failures == pushes and pushes > 1, "Authentication attempts not successful because multiple pushes denied",
totalattempts == 0, "Multiple pushes sent and ignored",
successes > 0 and pushes > 3, "Multiple pushes sent, eventual successful authentication!",
"Normal authentication pattern")
finding =failures =~ pushes and pushes > 1"Authentication attempts not successful because multiple pushes denied"totalattempts == 0"Multiple pushes sent and ignored"successes > 0 and pushes > 3"Multiple pushes sent, eventual successful authentication!""Normal authentication pattern"Stage 9: project
| project lasttime, firsttime, seconds, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, pushes, successes, failures, totalattempts, finding, outcome_result_s
Stage 10: extend
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
debugContext_debugData_factor_s | eq |
| field:"debugContext_debugData_factor_s" kind:eq value:"OKTA_VERIFY_PUSH" |
eventType_s | eq |
| field:"okta::eventType" kind:eq |
pushes | gt |
| field:"pushes" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
actor_alternateId_s | project |
actor_displayName_s | project |
authenticationContext_externalSessionId_s | project |
failures | project |
finding | project |
firsttime | project |
lasttime | project |
outcome_result_s | project |
pushes | project |
seconds | project |
successes | project |
totalattempts | project |
AccountName | extend |
AccountUPNSuffix | extend |
Mimecast Data Leak Prevention - Notifications
#Detects threat for data leak when action is notification
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 1818aeaa-4cc8-426b-ba54-539de896d299
name: Mimecast Data Leak Prevention - Notifications
description: Detects threat for data leak when action is notification
severity: High
requiredDataConnectors:
- connectorId: MimecastSIEMAPI
dataTypes:
- MimecastDLP_CL
enabled: true
query: MimecastDLP_CL| where action_s == "notification";
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- Exfiltration
relevantTechniques:
- T1030
alertRuleTemplateName:
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1d
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: MailMessage
fieldMappings:
- identifier: Sender
columnName: senderAddress_s
- identifier: Recipient
columnName: recipientAddress_s
- identifier: DeliveryAction
columnName: action_s
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
MimecastDLP_CL
Stage 2: where
| where action_s == "notification";
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action_s | eq |
| field:"action_s" kind:eq value:"notification" |
Netskope - Excessive Downloads Detection (Spike vs Baseline)
#Detects users with excessive download activity compared to their 7-day baseline. Triggers when current download volume exceeds 3x the average.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Rule Body
id: dd0ebd84-ffbe-45df-848b-0615ac446b04
name: Netskope - Excessive Downloads Detection (Spike vs Baseline)
description: |
Detects users with excessive download activity compared to their 7-day baseline. Triggers when current download volume exceeds 3x the average.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: NetskopeWebTxConnector
dataTypes:
- NetskopeWebTransactions_CL
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- Collection
relevantTechniques:
- T1530
- T1074
query: |
let lookbackPeriod = 7d;
let currentPeriod = 1h;
let threshold = 3;
let baseline = NetskopeWebTransactions_CL
| where TimeGenerated between (ago(lookbackPeriod) .. ago(currentPeriod))
| where isnotempty(CsUsername)
| where XCsAppActivity =~ 'Download' or ScBytes > 0
| summarize
BaselineAvgBytes = avg(ScBytes),
BaselineTotalBytes = sum(ScBytes),
BaselineCount = count()
by CsUsername
| extend BaselineDailyAvg = BaselineTotalBytes / 7;
let current = NetskopeWebTransactions_CL
| where TimeGenerated > ago(currentPeriod)
| where isnotempty(CsUsername)
| where XCsAppActivity =~ 'Download' or ScBytes > 0
| summarize
CurrentTotalBytes = sum(ScBytes),
CurrentCount = count(),
Apps = make_set(XCsApp),
Files = make_set(XCsAppObjectName)
by CsUsername;
current
| join kind=inner baseline on CsUsername
| where CurrentTotalBytes > (BaselineDailyAvg * threshold)
| extend
SpikeMultiplier = round(CurrentTotalBytes / BaselineDailyAvg, 2),
CurrentTotalMB = round(CurrentTotalBytes / 1048576.0, 2),
BaselineDailyMB = round(BaselineDailyAvg / 1048576.0, 2)
| project
TimeGenerated = now(),
User = CsUsername,
CurrentDownloadMB = CurrentTotalMB,
BaselineDailyAvgMB = BaselineDailyMB,
SpikeMultiplier,
DownloadCount = CurrentCount,
ApplicationsUsed = Apps,
FilesDownloaded = Files
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: User
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookbackPeriod = 7d;
let currentPeriod = 1h;
let threshold = 3;
let current is inlined into the numbered stages below.
Let Binding: baseline
let baseline = NetskopeWebTransactions_CL
| where TimeGenerated between (ago(lookbackPeriod) .. ago(currentPeriod))
| where isnotempty(CsUsername)
| where XCsAppActivity =~ 'Download' or ScBytes > 0
| summarize
BaselineAvgBytes = avg(ScBytes),
BaselineTotalBytes = sum(ScBytes),
BaselineCount = count()
by CsUsername
| extend BaselineDailyAvg = BaselineTotalBytes / 7;
Stages 1 to 5 define let current as the rule's main pipeline source. Stages 6 to 9 use it.
Stage 1: source
NetskopeWebTransactions_CL
Stage 2: where
| where TimeGenerated > ago(currentPeriod)
Stage 3: where
| where isnotempty(CsUsername)
Stage 4: where
| where XCsAppActivity =~ 'Download' or ScBytes > 0
Stage 5: summarize
| summarize
CurrentTotalBytes = sum(ScBytes),
CurrentCount = count(),
Apps = make_set(XCsApp),
Files = make_set(XCsAppObjectName)
by CsUsername
Stage 6: join
current
| join kind=inner baseline on CsUsername
Stage 7: where
where CurrentTotalBytes > (BaselineDailyAvg * 3)
Stage 8: extend
extend BaselineDailyMB, CurrentTotalMB, SpikeMultiplier
Stage 9: project
project ApplicationsUsed, BaselineDailyAvgMB, CurrentDownloadMB, DownloadCount, FilesDownloaded, SpikeMultiplier, TimeGenerated, User
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CsUsername | is_not_null | field:"CsUsername" kind:is_not_null | |
ScBytes | gt |
| field:"ScBytes" kind:gt value:"0" |
XCsAppActivity | eq |
| field:"XCsAppActivity" kind:eq value:"Download" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ApplicationsUsed | project |
BaselineDailyAvgMB | project |
CurrentDownloadMB | project |
DownloadCount | project |
FilesDownloaded | project |
SpikeMultiplier | project |
TimeGenerated | project |
User | project |
PAC high severity
#'High severity alerts found by Prancer.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 7caa1c03-d20b-42f2-ac95-5232f6e570da
name: PAC high severity
description: |
'High severity alerts found by Prancer.'
severity: High
requiredDataConnectors:
- connectorId: PrancerLogData
dataTypes:
- prancer_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: SingleAlert
status: Available
tactics:
- Reconnaissance
relevantTechniques:
- T1595
query: |
union prancer_CL
| where data_data_resourceID_s contains 'Microsoft.Compute' and severity_s == 'High'
| project
Name = name_s,
Severity = severity_s,
Description = data_alert_description_s,
URL = data_alert_url_s,
Resource = data_data_resourceID_s,
Config_ID = data_data_configId_s,
Collection = data_data_applicationName_s,
MITRE = data_alert_mitreId_s,
Company = companyName_s,
Solution = data_alert_solution_s,
WASC_ID = data_alert_wascid_s,
CWE_ID = data_alert_cweid_s,
Evidence = data_alert_evidence_s,
Request_Header = data_data_requestHeader_s,
Request_Body = data_data_requestBody_s,
Response_Header = data_data_responseHeader_s,
Response_Body = data_data_responseBody_s,
Other_Data = data_alert_other_s
entityMappings:
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: Resource
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
alertDetailsOverride:
alertDisplayNameFormat: "{{data_data_message_s}}"
alertDescriptionFormat: "{{data_data_description_s}}"
alertSeverityColumnName: "{{data_data_severity_s}}"
alertDynamicProperties:
- alertProperty: RemediationSteps
value: data_data_remediation_description_s
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: union
union of 1 branches
Stage 2: source
prancer_CL
Stage 3: where
where data_data_resourceID_s contains "Microsoft.Compute" and severity_s == "High"
Stage 4: project
project CWE_ID, Collection, Company, Config_ID, Description, Evidence, MITRE, Name, Other_Data, Request_Body, Request_Header, Resource, Response_Body, Response_Header, Severity, Solution, URL, WASC_ID
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
data_data_resourceID_s | contains |
| field:"data_data_resourceID_s" kind:contains value:"Microsoft.Compute" |
severity_s | eq |
| field:"okta::severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CWE_ID | project |
Collection | project |
Company | project |
Config_ID | project |
Description | project |
Evidence | project |
MITRE | project |
Name | project |
Other_Data | project |
Request_Body | project |
Request_Header | project |
Resource | project |
Response_Body | project |
Response_Header | project |
Severity | project |
Solution | project |
URL | project |
WASC_ID | project |
Palo Alto - possible internal to external port scanning
#'Identifies a list of internal Source IPs (10.x.x.x Hosts) that have triggered 10 or more non-graceful tcp server resets from one or more Destination IPs which results in an "ApplicationProtocol = incomplete" designation. The server resets coupled with an "Incomplete" ApplicationProtocol designation can be an indication of internal to external port scanning or probing attack. References: https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClUvCAK and https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClTaCAK'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 5b72f527-e3f6-4a00-9908-8e4fee14da9f
name: Palo Alto - possible internal to external port scanning
description: |
'Identifies a list of internal Source IPs (10.x.x.x Hosts) that have triggered 10 or more non-graceful tcp server resets from one or more Destination IPs which results in an "ApplicationProtocol = incomplete" designation. The server resets coupled with an "Incomplete" ApplicationProtocol designation can be an indication of internal to external port scanning or probing attack.
References: https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClUvCAK and
https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClTaCAK'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1046
query: |
CommonSecurityLog
| where isnotempty(DestinationPort) and DeviceAction !in ("reset-both", "deny")
// filter out common usage ports. Add ports that are legitimate for your environment
| where DestinationPort !in ("443", "53", "389", "80", "0", "880", "8888", "8080")
| where ApplicationProtocol == "incomplete"
// filter out IANA ephemeral or negotiated ports as per https://en.wikipedia.org/wiki/Ephemeral_port
| where DestinationPort !between (toint(49512) .. toint(65535))
| where Computer != ""
| where ipv4_is_private(DestinationIP) == false
| extend Reason = coalesce(
column_ifexists("Reason", ""),
extract("reason=(.+?)(;|$)", 1, AdditionalExtensions),
""
)
// Filter out any graceful reset reasons of AGED OUT which occurs when a TCP session closes with a FIN due to aging out.
| where Reason !has "aged-out"
// Filter out any TCP FIN which occurs when a TCP FIN is used to gracefully close half or both sides of a connection.
| where Reason !has "tcp-fin"
// Uncomment one of the following where clauses to trigger on specific TCP reset reasons
// See Palo Alto article for details - https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClUvCAK
// TCP RST-server - Occurs when the server sends a TCP reset to the client
// | where AdditionalExtensions has "reason=tcp-rst-from-server"
// TCP RST-client - Occurs when the client sends a TCP reset to the server
// | where AdditionalExtensions has "reason=tcp-rst-from-client"
// Already performed
//| extend reason = tostring(split(AdditionalExtensions, ";")[3])
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), count() by DeviceName, SourceUserID, SourceIP, ApplicationProtocol, Reason, DestinationPort, Protocol, DeviceVendor, DeviceProduct, DeviceAction, DestinationIP
| where count_ >= 10
| summarize StartTimeUtc = min(StartTimeUtc), EndTimeUtc = max(EndTimeUtc), makeset(DestinationIP), totalcount = sum(count_) by DeviceName, SourceUserID, SourceIP, ApplicationProtocol, Reason, DestinationPort, Protocol, DeviceVendor, DeviceProduct, DeviceAction
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SourceUserID
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.0.8
kind: Scheduled
Stages and Predicates
Stage 1: source
CommonSecurityLog
Stage 2: where
| where isnotempty(DestinationPort) and DeviceAction !in ("reset-both", "deny")
Stage 3: where
| where DestinationPort !in ("443", "53", "389", "80", "0", "880", "8888", "8080")
Stage 4: where
| where ApplicationProtocol == "incomplete"
Stage 5: where
| where DestinationPort !between (toint(49512) .. toint(65535))
Stage 6: where
| where Computer != ""
Stage 7: where
| where ipv4_is_private(DestinationIP) == false
Stage 8: extend
| extend Reason = coalesce(
column_ifexists("Reason", ""),
extract("reason=(.+?)(;|$)", 1, AdditionalExtensions),
""
)
Stage 9: where
| where Reason !has "aged-out"
Stage 10: where
| where Reason !has "tcp-fin"
Stage 11: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), count() by DeviceName, SourceUserID, SourceIP, ApplicationProtocol, Reason, DestinationPort, Protocol, DeviceVendor, DeviceProduct, DeviceAction, DestinationIP
Stage 12: where
| where count_ >= 10
Stage 13: summarize
| summarize StartTimeUtc = min(StartTimeUtc), EndTimeUtc = max(EndTimeUtc), makeset(DestinationIP), totalcount = sum(count_) by DeviceName, SourceUserID, SourceIP, ApplicationProtocol, Reason, DestinationPort, Protocol, DeviceVendor, DeviceProduct, DeviceAction
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ApplicationProtocol | eq |
| field:"ApplicationProtocol" kind:eq value:"incomplete" |
DestinationPort | is_not_null | field:"DestinationPort" kind:is_not_null | |
count_ | ge |
| field:"count_" kind:ge value:"10" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DeviceAction | in | deny, reset-both | excludes:DeviceAction field:"DeviceAction" value:"deny" field:"DeviceAction" value:"reset-both" |
DestinationPort | in | 0, 389, 443, 53, 80, 8080, 880, 8888 | excludes:DestinationPort |
DestinationPort | ge | 49512 | excludes:DestinationPort field:"DestinationPort" value:"49512" |
DestinationPort | le | 65535 | excludes:DestinationPort field:"DestinationPort" value:"65535" |
DestinationIP | 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:DestinationIP |
Reason | match | aged-out | excludes:Reason field:"Reason" value:"aged-out" |
Reason | match | tcp-fin | excludes:Reason field:"Reason" value:"tcp-fin" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ApplicationProtocol | summarize |
DestinationPort | summarize |
DeviceAction | summarize |
DeviceName | summarize |
DeviceProduct | summarize |
DeviceVendor | summarize |
EndTimeUtc | summarize |
Protocol | summarize |
Reason | summarize |
SourceIP | summarize |
SourceUserID | summarize |
StartTimeUtc | summarize |
totalcount | summarize |
Ping Federate - Abnormal password reset attempts
#'Detects abnormal password reset attempts for user in short period of time.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: e45a7334-2cb4-4690-8156-f02cac73d584
name: Ping Federate - Abnormal password reset attempts
description: |
'Detects abnormal password reset attempts for user in short period of time.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let threshold = 10;
PingFederateEvent
| where EventType =~ 'PWD_RESET_REQUEST'
| summarize count() by DstUserName, bin(TimeGenerated, 30m)
| where count_ > threshold
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
PingFederateEvent
Stage 2: where
| where EventType =~ 'PWD_RESET_REQUEST'
Stage 3: summarize
| summarize count() by DstUserName, bin(TimeGenerated, 30m)
Stage 4: where
| where count_ > threshold
Stage 5: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"PWD_RESET_REQUEST" |
count_ | gt |
| field:"count_" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | summarize |
AccountCustomEntity | extend |
Preferred Networks Deleted
#Detects when a preferred network is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: e06b913f-7698-4b2c-96e6-d47a9f4f5de8
name: Preferred Networks Deleted
description: Detects when a preferred network is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 32800\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 32800
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"32800" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Suspicious access of BEC related documents
#This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry Coverage
Rule Body
id: cd8d946d-10a4-40a9-bac1-6d0a6c847d65
name: Suspicious access of BEC related documents
description: |
'This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks.
The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1530
eventGroupingSettings:
aggregationKind: SingleAlert
query: |
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
// Adjust this threshold based on your environment
let sensitivity = 2.5;
let Events = materialize(imFileEvent
| where TimeGenerated between(startofday(ago(14d))..endofday(ago(0d)))
| where User !~ "app@sharepoint"
| where EventType =~ "FileAccessed"
| extend OriginalEvent = column_ifexists("EventOriginalType","Unknown")
| where OriginalEvent !~ "FileSyncDownloadedFull"
| where EventProduct in ("SharePoint 365", "Azure File Storage", "OneDrive" , "SharePoint")
| where FilePath has_any(BEC_Keywords)
| extend _AuthDetails = column_ifexists("AuthorizationDetails", "None")
| extend SPuser = case(gettype(_AuthDetails) == "array", tostring(todynamic(_AuthDetails)[0].principals[0].id), "Unknown")
| extend User = case(isnotempty(User), User, SPuser)
| where isnotempty(User));
Events
| summarize dcount(FileName) by User, bin(startofday(TimeGenerated), 1d)
| summarize CountOfDocs = make_list(dcount_FileName, 10000), TimeStamp = make_list(TimeGenerated, 10000) by User
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
| where Anomalies > 0
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, User
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, User
| extend IpAddr = column_ifexists("IpAddr", SrcIpAddr)
| extend Name = iif(User contains "@", split(User, "@")[0], split(User, "\\")[1])
| extend UPNSuffix = iif(User contains "@", split(User, "@")[1], "")
| extend NTDomain = iif(User contains "@", split(User, "\\")[0], "")
| project-reorder TimeGenerated, User, EventType, EventResult, EventProduct, FilePath, HttpUserAgent, IpAddr, CountOfDocs, Baseline, Score
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: User
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddr
- entityType: File
fieldMappings:
- identifier: Name
columnName: FilePath
customDetails:
Type: EventType
Result: EventResult
Product: EventProduct
UserAgent: HttpUserAgent
alertDetailsOverride:
alertDisplayNameFormat: Suspicious access of {{CountOfDocs}} BEC related documents by {{User}}
alertDescriptionFormat: |
This query looks for users (in this case {{User}}) with suspicious spikes in the number of files accessed (in this case {{CountOfDocs}} events) that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let sensitivity = 2.5;
let Events is inlined into the numbered stages below.
Let Binding: BEC_Keywords
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
Stage 1: source
let Events
Stage 2: source
imFileEvent
Stage 3: where
where TimeGenerated between (ago(1209600s) .. now())
Stage 4: where
where User !~ "app@sharepoint"
Stage 5: where
where EventType =~ "FileAccessed"
Stage 6: extend
extend OriginalEvent
Stage 7: where
where OriginalEvent !~ "FileSyncDownloadedFull"
Stage 8: where
where EventProduct in ("Azure File Storage", "OneDrive", "SharePoint", "SharePoint 365")
Stage 9: where
where (FilePath has "invoice" or FilePath has "payment" or FilePath has "paycheck" or FilePath has "transfer" or FilePath has "bank statement" or FilePath has "bank details" or FilePath has "closing" or FilePath has "funds" or FilePath has "bank account" or FilePath has "account details" or FilePath has "remittance" or FilePath has "purchase" or FilePath has "deposit" or FilePath has "PO#" or FilePath has "Zahlung" or FilePath has "Rechnung" or FilePath has "Paiement" or FilePath has "virement bancaire" or FilePath has "Bankuberweisung" or FilePath has "hacked" or FilePath has "phishing")
Stage 10: extend (3 consecutive steps)
extend SPuser, User, _AuthDetails
Stage 11: where
where isnotempty(User)
Stage 12: summarize
summarize by User
Stage 13: summarize
summarize CountOfDocs, TimeStamp by User
Stage 14: extend
extend Anomalies, Baseline, Score
Stage 15: mv-expand
mv-expand CountOfDocs
Stage 16: where
where Anomalies > 0
Stage 17: project
project Anomalies, Baseline, CountOfDocs, Score, TimeStamp, User
Stage 18: join
join kind=inner (Events) on TimeStamp, User
Stage 19: extend (4 consecutive steps)
extend IpAddr, NTDomain, Name, UPNSuffix
Stage 20: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Anomalies | gt |
| field:"Anomalies" kind:gt value:"0" |
EventProduct | in |
| field:"EventProduct" kind:in |
EventType | eq |
| field:"EventType" kind:eq value:"FileAccessed" |
FilePath | match |
| field:"FilePath" kind:match |
OriginalEvent | ne |
| field:"OriginalEvent" kind:ne value:"FileSyncDownloadedFull" |
User | is_not_null | field:"User" kind:is_not_null | |
User | ne |
| field:"User" kind:ne value:"app@sharepoint" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Anomalies | project |
Baseline | project |
CountOfDocs | project |
Score | project |
TimeStamp | project |
User | project |
IpAddr | extend |
Name | extend |
UPNSuffix | extend |
NTDomain | extend |
TI Map IP entity to AWSCloudTrail
#Identifies a match in AWSCloudTrail from any IP IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 69f55be4-1b13-42d0-b975-a1e59c996dd2
name: TI Map IP entity to AWSCloudTrail
description: |
Identifies a match in AWSCloudTrail from any IP IOC from TI
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h; // Look back 1 hour for AWSCloudTrail logs
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
// Filter out indicators without relevant IP address fields
| where TimeGenerated >= ago(ioc_lookBack)
// Select the IP entity based on availability of different IP fields
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
// Exclude local addresses using the ipv4_is_private operator and filtering out specific address prefixes
| 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 Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Perform a join between IP indicators and AWSCloudTrail logs to identify potential malicious activity
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
// 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 (
AWSCloudTrail
| where TimeGenerated >= ago(dt_lookBack)
| extend AWSCloudTrail_TimeGenerated = TimeGenerated // Rename time column for clarity
)
on $left.TI_ipEntity == $right.SourceIpAddress
// Filter out logs that occurred after the expiration of the corresponding indicator
| where AWSCloudTrail_TimeGenerated < ValidUntil
// Group the results by IndicatorId and SourceIpAddress, and keep the log entry with the latest timestamp
| summarize AWSCloudTrail_TimeGenerated = arg_max(AWSCloudTrail_TimeGenerated, *) by Id, SourceIpAddress
// Select the desired output fields
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project AWSCloudTrail_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
TI_ipEntity, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName, SourceIpAddress,
NetworkSourceIP, Url
// Rename the timestamp field
| extend timestamp = AWSCloudTrail_TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: ObjectGuid
columnName: UserIdentityUserName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.7
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 10 define let IP_Indicators as the rule's main pipeline source. Stages 11 to 18 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 4: extend
| extend NetworkSourceIP = toupper(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend (3 consecutive steps)
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
TI_ipEntity =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 8: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 9: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 10: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 11: project-reorder
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 12: join
| join kind=innerunique (
AWSCloudTrail
| where TimeGenerated >= ago(dt_lookBack)
| extend AWSCloudTrail_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.SourceIpAddress
Stage 13: where
where AWSCloudTrail_TimeGenerated < ValidUntil
Stage 14: summarize
summarize AWSCloudTrail_TimeGenerated by Id, SourceIpAddress
Stage 15: extend
extend Description
Stage 16: extend
extend ActivityGroupNames
Stage 17: project
project AWSCloudTrail_TimeGenerated, ActivityGroupNames, Confidence, Description, EventName, EventTypeName, Id, NetworkSourceIP, SourceIpAddress, TI_ipEntity, Url, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName, ValidUntil
Stage 18: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
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 |
|---|---|
AWSCloudTrail_TimeGenerated | project |
ActivityGroupNames | project |
Confidence | project |
Description | project |
EventName | project |
EventTypeName | project |
Id | project |
NetworkSourceIP | project |
SourceIpAddress | project |
TI_ipEntity | project |
Url | project |
UserIdentityAccountId | project |
UserIdentityPrincipalid | project |
UserIdentityUserName | project |
ValidUntil | project |
timestamp | extend |
TI Map URL Entity to AuditLogs
#This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in AuditLogs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rule Body
id: 9991c277-e0a1-4079-8c40-fbfca2705615
name: TI Map URL Entity to AuditLogs
description: |
'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in AuditLogs.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- 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;
let AuditEvents = materialize(AuditLogs
| where TimeGenerated >= ago(dt_lookBack)
// Extract the URL that is contained within the JSON data
| extend Url = tolower(extract("(http[s]?://[^\\s\\\"\\\\]+)", 1, tostring(TargetResources)))
| extend userPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend TargetResourceDisplayName = tostring(TargetResources[0].displayName));
let AuditUrls = AuditEvents | distinct Url = tolower(Url) | summarize make_list(Url);
ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "url"
| extend Url = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| where tolower(Url) in (AuditUrls)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend Description = tostring(parse_json(Data).description)
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (AuditEvents) on Url
| where TimeGenerated < ValidUntil
| summarize TimeGenerated = arg_max(TimeGenerated, *) by Id, Url
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project TimeGenerated, Description, ActivityGroupNames, Id, Type, ValidUntil, Confidence,
OperationName, Identity, userPrincipalName, TargetResourceDisplayName, Url
| extend AccountName = tostring(split(userPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(userPrincipalName, "@")[1])
| extend HostName = tostring(split(TargetResourceDisplayName, ".")[0]), DomainIndex = toint(indexof(TargetResourceDisplayName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(TargetResourceDisplayName, DomainIndex + 1), TargetResourceDisplayName)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: userPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: TargetResourceDisplayName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.2.11
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: AuditEvents
let AuditEvents = materialize(AuditLogs
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = tolower(extract("(http[s]?://[^\\s\\\"\\\\]+)", 1, tostring(TargetResources)))
| extend userPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend TargetResourceDisplayName = tostring(TargetResources[0].displayName));
Let Binding: AuditUrls
let AuditUrls = AuditEvents | distinct Url = tolower(Url) | summarize make_list(Url);
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 = tolower(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: where
| where tolower(Url) in (AuditUrls)
Stage 8: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 9: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 10: extend
| extend Description = tostring(parse_json(Data).description)
Stage 11: where
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
Stage 12: join
| join kind=innerunique (AuditEvents) on Url
Stage 13: where
where TimeGenerated < ValidUntil
Stage 14: summarize
summarize TimeGenerated by Id, Url
Stage 15: extend
extend Description
Stage 16: extend
extend ActivityGroupNames
Stage 17: project
project ActivityGroupNames, Confidence, Description, Id, Identity, OperationName, TargetResourceDisplayName, TimeGenerated, Type, Url, ValidUntil, userPrincipalName
Stage 18: extend (3 consecutive steps)
extend AccountName, AccountUPNSuffix, DomainIndex, HostName, HostNameDomain
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" |
Url | in |
| field:"Url" kind:in value:"AuditUrls" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
Confidence | project |
Description | project |
Id | project |
Identity | project |
OperationName | project |
TargetResourceDisplayName | project |
TimeGenerated | project |
Type | project |
Url | project |
ValidUntil | project |
userPrincipalName | project |
AccountName | extend |
AccountUPNSuffix | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |