Detection rules › Kusto

Kusto rules: 16

AWSCloudTrail - Privilege escalation via SSM policy

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

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

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
Actioncontains
  • ssm:*
  • ssm:Create*
  • ssm:CreateAssociation
field:"Action" kind:contains
Conditionis_null
  • (no value, null check)
field:"Condition" kind:is_null
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • PutGroupPolicy corpus 12 (kusto 11, panther 1)
  • PutRolePolicy corpus 12 (kusto 11, panther 1)
  • PutUserPolicy corpus 12 (kusto 11, panther 1)
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend
Statementextend
Actionextend
Conditionextend
Effectextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

Azure Portal sign in from another Azure Tenant

#
Status
available
Severity
medium
Time window
1h
Group by
HomeTenantId, IPAddress, Location, ResourceTenantId, UserAgent, UserId, UserPrincipalName, ip_type
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 used in Stage 6

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 =
ifisnotnull(isipv4)"v4"
else"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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
FirstSeensummarize
HomeTenantIdsummarize
IPAddresssummarize
LastSeensummarize
Locationsummarize
ResourceTenantIdsummarize
UserAgentsummarize
UserIdsummarize
UserPrincipalNamesummarize
AccountNameextend
UPNSuffixextend

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.

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

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.

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

Changes to Application Logout URL

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

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
Actioncontains
  • Url
field:"Action" kind:contains value:"Url"
Categoryeq
  • ApplicationManagement corpus 13 (kusto 13)
field:"Category" kind:eq value:"ApplicationManagement"
OperationNamematch
  • Update Application transforms: term corpus 3 (kusto 3)
  • Update Service principal transforms: term corpus 2 (kusto 2)
field:"OperationName" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
TargetAppNameextend
mod_propsextend
Actionextend
UpdatedByextend
OldURLextend
NewURLextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

CiscoISE - ISE administrator password has been reset

#
Status
available
Severity
medium
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Detects when the ISE administrator password has been reset.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNameproject
SrcIpAddrproject
TimeGeneratedproject
AccountCustomEntityextend
IPCustomEntityextend

Detect Suspicious ncrypt.dll usage by process requesting Entra ID Nonce

#
Group by
DeviceId, InitiatingProcessId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

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 used in Stages 1, 5

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.

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.

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

'This query searches for events related to publicly available cloud bucket.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
RiskScorege
  • 3 transforms: number
field:"RiskScore" kind:ge value:"3"
index_nameeq
  • bucket
  • driller_bucket_object
field:"index_name" kind:eq
uidis_not_null
  • (no value, null check)
field:"uid" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
index_nameextend

GitHub Activites from a New Country

#
Status
available
Severity
medium
Time window
7d
Group by
Actor
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

FieldSource
Actorsummarize
timestampextend
AccountNameextend
AccountUPNSuffixextend

Illumio Firewall Tampering Analytic Rule

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

'Create Microsoft Sentinel Incident When Firewall Is Tampered With'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
event_typematch
  • tampering transforms: term
field:"event_type" kind:match value:"tampering"

Output Fields

These fields are emitted when the rule matches.

FieldSource
hostnameextend
ipaddressextend
ven_hrefextend

MFA Fatigue (OKTA)

#
Status
available
Severity
medium
Time window
1h
Group by
TimeGenerated, actor_alternateId_s, actor_displayName_s, authenticationContext_externalSessionId_s, outcome_result_s
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 =
iffailures =~ pushes and pushes > 1"Authentication attempts not successful because multiple pushes denied"
eliftotalattempts == 0"Multiple pushes sent and ignored"
elifsuccesses > 0 and pushes > 3"Multiple pushes sent, eventual successful authentication!"
else"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.

FieldKindValuesSearch
debugContext_debugData_factor_seq
  • OKTA_VERIFY_PUSH transforms: column_ifexists
field:"debugContext_debugData_factor_s" kind:eq value:"OKTA_VERIFY_PUSH"
eventType_seq
  • system.push.send_factor_verify_push
  • user.authentication.auth_via_mfa corpus 4 (elastic 2, kusto 2)
  • user.mfa.okta_verify.deny_push corpus 3 (elastic 2, kusto 1)
field:"okta::eventType" kind:eq
pushesgt
  • 10 transforms: number
field:"pushes" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
actor_alternateId_sproject
actor_displayName_sproject
authenticationContext_externalSessionId_sproject
failuresproject
findingproject
firsttimeproject
lasttimeproject
outcome_result_sproject
pushesproject
secondsproject
successesproject
totalattemptsproject
AccountNameextend
AccountUPNSuffixextend

Mimecast Data Leak Prevention - Notifications

#
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

Detects threat for data leak when action is notification

MITRE ATT&CK Coverage

TacticTechniques
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.

Netskope - Excessive Downloads Detection (Spike vs Baseline)

#
Status
available
Severity
medium
Time window
7d
Group by
CsUsername
Source
github.com/Azure/Azure-Sentinel

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

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 used in Stage 6

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationsUsedproject
BaselineDailyAvgMBproject
CurrentDownloadMBproject
DownloadCountproject
FilesDownloadedproject
SpikeMultiplierproject
TimeGeneratedproject
Userproject

PAC high severity

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

'High severity alerts found by Prancer.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CWE_IDproject
Collectionproject
Companyproject
Config_IDproject
Descriptionproject
Evidenceproject
MITREproject
Nameproject
Other_Dataproject
Request_Bodyproject
Request_Headerproject
Resourceproject
Response_Bodyproject
Response_Headerproject
Severityproject
Solutionproject
URLproject
WASC_IDproject

Palo Alto - possible internal to external port scanning

#
Status
available
Severity
low
Time window
1h
Group by
ApplicationProtocol, DestinationIP, DestinationPort, DeviceAction, DeviceName, DeviceProduct, DeviceVendor, Protocol, Reason, SourceIP, SourceUserID
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DeviceActionindeny, reset-bothexcludes:DeviceAction field:"DeviceAction" value:"deny" field:"DeviceAction" value:"reset-both"
DestinationPortin0, 389, 443, 53, 80, 8080, 880, 8888excludes:DestinationPort
DestinationPortge49512excludes:DestinationPort field:"DestinationPort" value:"49512"
DestinationPortle65535excludes:DestinationPort field:"DestinationPort" value:"65535"
DestinationIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DestinationIP
Reasonmatchaged-outexcludes:Reason field:"Reason" value:"aged-out"
Reasonmatchtcp-finexcludes:Reason field:"Reason" value:"tcp-fin"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationProtocolsummarize
DestinationPortsummarize
DeviceActionsummarize
DeviceNamesummarize
DeviceProductsummarize
DeviceVendorsummarize
EndTimeUtcsummarize
Protocolsummarize
Reasonsummarize
SourceIPsummarize
SourceUserIDsummarize
StartTimeUtcsummarize
totalcountsummarize

Ping Federate - Abnormal password reset attempts

#
Status
available
Severity
high
Time window
30m
Group by
DstUserName
Source
github.com/Azure/Azure-Sentinel

'Detects abnormal password reset attempts for user in short period of time.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNamesummarize
AccountCustomEntityextend

Preferred Networks Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Suspicious access of BEC related documents

#
Severity
medium
Time window
14d
Group by
TimeStamp, User
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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.

FieldKindValuesSearch
Anomaliesgt
  • 0 transforms: number
field:"Anomalies" kind:gt value:"0"
EventProductin
  • Azure File Storage
  • OneDrive
  • SharePoint
  • SharePoint 365
field:"EventProduct" kind:in
EventTypeeq
  • FileAccessed
field:"EventType" kind:eq value:"FileAccessed"
FilePathmatch
  • Bankuberweisung transforms: term
  • PO# transforms: term
  • Paiement transforms: term
  • Rechnung transforms: term
  • Zahlung transforms: term
  • account details transforms: term
  • bank account transforms: term
  • bank details transforms: term
  • bank statement transforms: term
  • closing transforms: term
  • deposit transforms: term
  • funds transforms: term
  • hacked transforms: term
  • invoice transforms: term
  • paycheck transforms: term
  • payment transforms: term
  • phishing transforms: term
  • purchase transforms: term
  • remittance transforms: term
  • transfer transforms: term
  • virement bancaire transforms: term
field:"FilePath" kind:match
OriginalEventne
  • FileSyncDownloadedFull
field:"OriginalEvent" kind:ne value:"FileSyncDownloadedFull"
Useris_not_null
  • (no value, null check)
field:"User" kind:is_not_null
Userne
  • app@sharepoint
field:"User" kind:ne value:"app@sharepoint"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Anomaliesproject
Baselineproject
CountOfDocsproject
Scoreproject
TimeStampproject
Userproject
IpAddrextend
Nameextend
UPNSuffixextend
NTDomainextend

TI Map IP entity to AWSCloudTrail

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

Identifies a match in AWSCloudTrail from any IP IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: where

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

Stage 9: 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.

FieldKindValuesSearch
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSCloudTrail_TimeGeneratedproject
ActivityGroupNamesproject
Confidenceproject
Descriptionproject
EventNameproject
EventTypeNameproject
Idproject
NetworkSourceIPproject
SourceIpAddressproject
TI_ipEntityproject
Urlproject
UserIdentityAccountIdproject
UserIdentityPrincipalidproject
UserIdentityUserNameproject
ValidUntilproject
timestampextend

TI Map URL Entity to AuditLogs

#
Severity
medium
Time window
14d
Group by
Id, ObservableValue, Url
Source
github.com/Azure/Azure-Sentinel

This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in AuditLogs.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

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 used in Stage 12

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 used in Stage 7

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Confidenceproject
Descriptionproject
Idproject
Identityproject
OperationNameproject
TargetResourceDisplayNameproject
TimeGeneratedproject
Typeproject
Urlproject
ValidUntilproject
userPrincipalNameproject
AccountNameextend
AccountUPNSuffixextend
DomainIndexextend
HostNameextend
HostNameDomainextend

Back to all Kusto rules