Detection rules › Kusto

URL Added to Application from Unknown Domain

Severity
high
Time window
2h
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects a URL being added to an application where the domain is not one that is associated with the tenant. The query uses domains seen in sign in logs to determine if the domain is associated with the tenant. Applications associated with URLs not controlled by the organization can pose a security risk. Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes

MITRE ATT&CK coverage

Telemetry coverage

PlatformRecord / event type
AzureUpdate application

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 017e095a-94d8-430c-a047-e51a11fb737b
name: URL Added to Application from Unknown Domain
description: |
  'Detects a URL being added to an application where the domain is not one that is associated with the tenant.
    The query uses domains seen in sign in logs to determine if the domain is associated with the tenant.
    Applications associated with URLs not controlled by the organization can pose a security risk.
    Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let domains =
    SigninLogs
    | where ResultType == 0
    | extend domain = split(UserPrincipalName, "@")[1]
    | extend domain = tostring(split(UserPrincipalName, "@")[1])
    | summarize by tolower(tostring(domain));
    AuditLogs
    | where Category =~ "ApplicationManagement"
    | where Result =~ "success"
    | where OperationName =~ 'Update Application'
    | mv-expand TargetResources
    | mv-expand TargetResources.modifiedProperties
    | where TargetResources_modifiedProperties.displayName =~ "AppAddress"
    | extend Key = tostring(TargetResources_modifiedProperties.displayName)
    | extend NewValue = TargetResources_modifiedProperties.newValue
    | extend OldValue = TargetResources_modifiedProperties.oldValue
    | where isnotempty(Key) and isnotempty(NewValue)
    | project-reorder Key, NewValue, OldValue
    | extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
    | extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
    | extend AddedUrls = set_difference(NewUrls, OldUrls)
    | where array_length(AddedUrls) > 0
    | extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
    | 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 InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
    | extend AppDisplayName = tostring(TargetResources.displayName)
    | where isnotempty(AddedUrls)
    | mv-expand AddedUrls
    | extend AddedUrls = trim(@'"', tostring(AddedUrls))
    | extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
    | where isnotempty(Domain)
    | extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])
    | where Domain !in (domains)
    | project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: AddedUrls
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 1.1.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Let binding: domains used in Stage 20

let domains = SigninLogs
  | where ResultType == 0
  | extend domain = split(UserPrincipalName, "@")[1]
  | extend domain = tostring(split(UserPrincipalName, "@")[1])
  | summarize by tolower(tostring(domain));

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "ApplicationManagement"

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where OperationName =~ 'Update Application'

Stage 5: mv-expand

| mv-expand TargetResources

Stage 6: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 7: where

| where TargetResources_modifiedProperties.displayName =~ "AppAddress"

Stage 8: extend (3 consecutive steps)

| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue

Stage 9: where

| where isnotempty(Key) and isnotempty(NewValue)

Stage 10: project-reorder

| project-reorder Key, NewValue, OldValue

Stage 11: extend (3 consecutive steps)

| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)

Stage 12: where

| where array_length(AddedUrls) > 0

Stage 13: extend (8 consecutive steps)

| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| 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 InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AppDisplayName = tostring(TargetResources.displayName)

Stage 14: where

| where isnotempty(AddedUrls)

Stage 15: mv-expand

| mv-expand AddedUrls

Stage 16: extend

| extend AddedUrls = trim(@'"', tostring(AddedUrls))

Stage 17: extend

| extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))

Stage 18: where

| where isnotempty(Domain)

Stage 19: extend

| extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])

Stage 20: where

| where Domain !in (domains)

Stage 21: project-reorder

| project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress

Stage 22: extend

| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Domaineqdomainsexcludes:Domain field:"Domain" value:"domains"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Keyextend
NewValueextend
OldValueextend
NewUrlsextend
OldUrlsextend
AddedUrlsextend
UserAgentextend
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
InitiatedByextend
AppDisplayNameextend
Domainextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend