Query Details

Copilot Agents User Access

Query

# Copilot - Agents - User Access

![KQL](https://img.shields.io/badge/language-KQL-blue.svg)
![Status: Testing](https://img.shields.io/badge/status-testing-blue.svg)

## Query Information

### Description

Retrieve Copilot - Agent - User Access setting changes

User access allows control of how members of your organization access and install agents. The setting has three options:

- All users - This option is the default. It means that all users in the organization can access agents, subject to the existing app policies and user assignments.
- No users - This option means that no users in the organization can access agents.
- Specific users/groups - This option lets you select specific users or groups in your organization to have access to agents. While some users in your organization might have permission to install and use agents from the Agent Registry list, only the users or groups you select in this setting can use agents.

#### References

- [Agent settings in Microsoft 365 admin center](https://learn.microsoft.com/en-us/microsoft-365/admin/manage/agent-settings?view=o365-worldwide)

### Author

- **Alex Verboon**

## Defender XDR

```kql
// User Access
CloudAppEvents
| where Application == "Microsoft 365"
| where ActionType == "UpdateTenantSettings"
| extend UserAccessSetting = tostring(parse_json(tostring(RawEventData.Resource)).Property)
| where UserAccessSetting == "EnableCopilotExtensibility"
| extend ForAllUsers = tostring(RawEventData.ForAllUsers)
| extend NewValue = tostring(parse_json(tostring(parse_json(tostring(RawEventData.Resource)).NewValue)))
| extend OriginalValue = tostring(parse_json(tostring(parse_json(tostring(RawEventData.Resource)).OriginalValue)))
| extend RemovedIdentities = parse_json(RawEventData.RemovedIdentities)
| extend AddedIdentities = parse_json(RawEventData.AddedIdentities)
| extend Configuration = "Users who can access Agents"
| extend ConfigurationState = case(
    // No Users: was All Users, now explicitly disabled
    ForAllUsers == "true"  and NewValue == "false", "No Users",
    // All Users: enabled for everyone
    ForAllUsers == "true"  and array_length(AddedIdentities) == 0 and NewValue == "true",  "All Users",
    // Specific Users or Groups: scoped to identities
    ForAllUsers == "false" and array_length(AddedIdentities) > 0,   "Specific Users or Groups",
    ForAllUsers == "false" and array_length(RemovedIdentities) > 0, "Specific Users or Groups",
    "Unknown"
)
| project TimeGenerated, UserAccessSetting, Configuration,ConfigurationState, ForAllUsers, AddedIdentities, RemovedIdentities, OriginalValue, NewValue,AccountDisplayName
| sort by TimeGenerated
```

Query with enriched added and removed user entities

```kql
// User Access
CloudAppEvents
| where Application == "Microsoft 365"
| where ActionType == "UpdateTenantSettings"
| extend UserAccessSetting = tostring(parse_json(tostring(RawEventData.Resource)).Property)
| where UserAccessSetting == "EnableCopilotExtensibility"
| extend ForAllUsers = tostring(RawEventData.ForAllUsers)
| extend NewValue = tostring(parse_json(tostring(parse_json(tostring(RawEventData.Resource)).NewValue)))
| extend OriginalValue = tostring(parse_json(tostring(parse_json(tostring(RawEventData.Resource)).OriginalValue)))
| extend RemovedIdentities = parse_json(RawEventData.RemovedIdentities)
| extend AddedIdentities = parse_json(RawEventData.AddedIdentities)
| extend Configuration = "Users who can access Agents"
| extend ConfigurationState = case(
    ForAllUsers == "true"  and NewValue == "false", "No Users",
    ForAllUsers == "true"  and array_length(AddedIdentities) == 0 and NewValue == "true", "All Users",
    ForAllUsers == "false" and array_length(AddedIdentities) > 0,   "Specific Users or Groups",
    ForAllUsers == "false" and array_length(RemovedIdentities) > 0, "Specific Users or Groups",
    "Unknown"
)
// Pad empty arrays with a placeholder so mv-expand never drops the row
| extend AddedIdentities   = iff(array_length(AddedIdentities)   == 0, dynamic([null]), AddedIdentities)
| extend RemovedIdentities = iff(array_length(RemovedIdentities) == 0, dynamic([null]), RemovedIdentities)
// Enrich AddedIdentities
| mv-expand AddedIdentity = AddedIdentities
| extend AddedObjectId = tostring(AddedIdentity)
| join kind=leftouter (
    IdentityInfo
    | where TimeGenerated > ago(21d)
    | summarize arg_max(TimeGenerated, *) by AccountObjectId
    | project AccountObjectId, AddedDisplayName = AccountDisplayName
) on $left.AddedObjectId == $right.AccountObjectId
// Only pack non-placeholder rows
| extend AddedIdentitiesDetails = iff(isnotempty(AddedObjectId), pack("ObjectId", AddedObjectId, "DisplayName", AddedDisplayName), dynamic(null))
| summarize
    AddedIdentitiesDetails = make_list(AddedIdentitiesDetails),
    arg_max(TimeGenerated, *)
    by TimeGenerated, UserAccessSetting, Configuration, ConfigurationState, ForAllUsers,
       RemovedIdentities = tostring(RemovedIdentities),
       OriginalValue, NewValue, AccountDisplayName
// Enrich RemovedIdentities
| mv-expand RemovedIdentity = parse_json(RemovedIdentities)
| extend RemovedObjectId = tostring(RemovedIdentity)
| join kind=leftouter (
    IdentityInfo
    | where TimeGenerated > ago(21d)
    | summarize arg_max(TimeGenerated, *) by AccountObjectId
    | project AccountObjectId, RemovedDisplayName = AccountDisplayName
) on $left.RemovedObjectId == $right.AccountObjectId
// Only pack non-placeholder rows
| extend RemovedIdentitiesDetails = iff(isnotempty(RemovedObjectId), pack("ObjectId", RemovedObjectId, "DisplayName", RemovedDisplayName), dynamic(null))
| summarize
    RemovedIdentitiesDetails = make_list(RemovedIdentitiesDetails),
    AddedIdentitiesDetails   = any(AddedIdentitiesDetails)
    by TimeGenerated, UserAccessSetting, Configuration, ConfigurationState, ForAllUsers, OriginalValue, NewValue, AccountDisplayName
| project TimeGenerated, AccountDisplayName, UserAccessSetting, Configuration, ConfigurationState,
    AddedIdentitiesDetails, RemovedIdentitiesDetails
| sort by TimeGenerated
```


Explanation

This KQL (Kusto Query Language) query is designed to track changes in user access settings for Copilot agents within a Microsoft 365 environment. Here's a simplified explanation of what the query does:

  1. Data Source: The query pulls data from the CloudAppEvents table, specifically looking at events related to the "Microsoft 365" application where tenant settings have been updated.

  2. Focus: It focuses on changes to the "EnableCopilotExtensibility" setting, which controls who in the organization can access and use Copilot agents.

  3. User Access Settings:

    • All Users: All users in the organization can access agents.
    • No Users: No users in the organization can access agents.
    • Specific Users/Groups: Only selected users or groups can access agents.
  4. Data Processing:

    • The query extracts and processes information about the current and previous settings, as well as any users or groups added or removed from access.
    • It categorizes the configuration state into "No Users," "All Users," or "Specific Users or Groups" based on the changes detected.
  5. Enrichment:

    • The query enriches the data by joining with the IdentityInfo table to get display names for added or removed users/groups.
    • It ensures that even if no users are added or removed, the data structure remains consistent.
  6. Output:

    • The final output includes the time of the change, the account display name of the person who made the change, the user access setting, the configuration state, and details of any added or removed identities.
    • The results are sorted by the time the changes were made.

In summary, this query helps administrators monitor and understand changes to who can access Copilot agents in their organization, providing insights into how access policies are being modified over time.

Details

Alex Verboon profile picture

Alex Verboon

Released: April 20, 2026

Tables

CloudAppEventsIdentityInfo

Keywords

CopilotAgentsUserAccessMicrosoftIdentityConfigurationAccountDisplayName

Operators

//|where==extendtostring()parse_json()array_length()case()andprojectsort byiff()dynamic()mv-expandjoin kind=leftouter>ago()summarizearg_max()bypack()make_list()any()isnotempty()==on$left.$right.

Actions