Detection rules › Kusto

Dataverse - New non-interactive identity granted access

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

Identifies API level access grants, either via the delegated permissions of a Microsoft Entra application or direct assignment within Dataverse as an application user.

MITRE ATT&CK coverage

Rule body kusto

id: 682e230c-e5da-4085-8666-701d1f1be7de
kind: Scheduled
name: Dataverse - New non-interactive identity granted access
description: Identifies API level access grants, either via the delegated permissions
  of a Microsoft Entra application or direct assignment within Dataverse as an application
  user.
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - LateralMovement
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T0859
  - T1078
query: |
  let dataverse_app_id = "00000007-0000-0000-c000-000000000000";
  let query_frequency = 1h;
  let azure_ad_changes = AuditLogs
      | where TimeGenerated >= ago(query_frequency)
      | where OperationName =~ 'Update application'
      | where TargetResources has dataverse_app_id
      | extend TargetAppName = tostring(TargetResources[0].displayName)
      | extend TargetAppId = tostring(TargetResources[0].id)
      | extend UserId = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
      | extend ClientIp = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
      | extend NewData = tostring(parse_json(tostring(parse_json(TargetResources)[0].modifiedProperties))[0].newValue)
      | where NewData has dataverse_app_id;
  let dataverse_changes = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where (Message == "Create" and EntityName == "systemuser" and parse_json(Fields)[0].Name == "applicationid")
      | extend TargetAppId = tostring(Fields[0].Value);
  union azure_ad_changes, dataverse_changes
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      TimeGenerated,
      UserId,
      ClientIp,
      TargetAppName,
      TargetAppId,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: TargetAppId
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - new non-interactive access granted
  alertDescriptionFormat: '{{UserId}} granted access to an Azure AD app {{{TargetAppName}}.
    Check to validate this access was authorized.'
version: 3.2.0

Stages and Predicates

Parameters

let dataverse_app_id = "00000007-0000-0000-c000-000000000000";
let query_frequency = 1h;

Let binding: azure_ad_changes

let azure_ad_changes = AuditLogs
    | where TimeGenerated >= ago(query_frequency)
    | where OperationName =~ 'Update application'
    | where TargetResources has dataverse_app_id
    | extend TargetAppName = tostring(TargetResources[0].displayName)
    | extend TargetAppId = tostring(TargetResources[0].id)
    | extend UserId = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend ClientIp = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
    | extend NewData = tostring(parse_json(tostring(parse_json(TargetResources)[0].modifiedProperties))[0].newValue)
    | where NewData has dataverse_app_id;

Derived from dataverse_app_id, query_frequency.

Let binding: dataverse_changes

let dataverse_changes = DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where (Message == "Create" and EntityName == "systemuser" and parse_json(Fields)[0].Name == "applicationid")
    | extend TargetAppId = tostring(Fields[0].Value);

Derived from query_frequency.

union (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: azure_ad_changes, dataverse_changes

Leg 1: azure_ad_changes

Leg 2: dataverse_changes

Applied to the combined result

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1]) | project
    TimeGenerated,
    UserId,
    ClientIp,
    TargetAppName,
    TargetAppId,
    InstanceUrl,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
EntityNameeq
  • systemuser transforms: cased corpus 3 (kusto 3)
Messageeq
  • Create transforms: cased corpus 2 (kusto 2)
Nameeq
  • applicationid transforms: cased
NewDatamatch
  • 00000007-0000-0000-c000-000000000000 transforms: term
OperationNameeq
  • Update application
TargetResourcesmatch
  • 00000007-0000-0000-c000-000000000000 transforms: term

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
InstanceUrlproject
TargetAppIdproject
TargetAppNameproject
TimeGeneratedproject
UPNSuffixproject
UserIdproject