共用方式為


教學課程:使用來自 Microsoft Entra 的數據,在 Azure 數據總管中建立自定義報表

在本教學課程中,您將瞭解如何使用來自 Microsoft Entra ID 和 Microsoft Entra ID Governance Services 的數據,在 Azure 數據總管 中建立自定義報表。

本教學課程補充了報告選項,例如 封存和使用 Azure 監視器及權限管理的報告,其著重於將稽核記錄匯出至 Azure 監視器以進行保留和分析。 相較之下,將Microsoft Entra ID 數據匯出至 Azure 數據總管,可讓您彈性地在 Microsoft Entra 物件上建立自定義報表,包括歷程記錄和已刪除的物件。

使用 Azure 數據總管也可以從其他來源進行數據聚合,並具備大規模的延展性、彈性架構及數據保留策略。 當您需要保留使用者存取數據多年、執行臨機作調查,或對存取數據執行自定義查詢時,Azure 數據總管特別有用。

本教學課程說明如何顯示從 Microsoft Entra 匯出的設定、使用者和訪問許可權,以及從其他來源導出的數據,例如在自己的 SQL 資料庫中具有訪問許可權的應用程式。 然後,您可以使用 Azure 數據總管中的 Kusto 查詢語言 (KQL),根據組織的需求來建置自定義報表。

在本教學課程中,您會:

  • 在 Azure 訂用帳戶中設定 Azure 數據總管,或建立免費的叢集。
  • 使用 PowerShell 腳本和 Microsoft Graph,從 Microsoft Entra ID 擷取數據。
  • 建立數據表,並將數據從 Microsoft Entra ID 匯入 Azure 數據總管。
  • 從 Microsoft Entra ID Governance 擷取數據。
  • 建立數據表,並將數據從 Microsoft Entra ID Governance 匯入 Azure 數據總管。
  • 使用 KQL 建置自訂查詢。
  • 查詢 Azure 監視器中的數據。

在本教學課程結束時,您將能夠創建使用者訪問權限和許可權的自訂檢視。 這些檢視會透過Microsoft支援的工具跨越多個應用程式。 您也可以從非Microsoft資料庫或應用程式帶入數據,以報告這些訪問許可權和許可權。

必要條件

如果您不熟悉 Azure 數據總管,而且想要瞭解本教學課程所示範的案例,您可以取得 免費的 Azure 數據總管叢集。 針對生產環境支援的 Azure 數據總管服務等級協定使用,您需要 Azure 訂用帳戶來裝載完整的 Azure 數據總管叢集。

判斷您想要在報告中包括的資料。 本教學課程中的腳本會提供來自 Microsoft Entra 的使用者、群組和應用程式的特定數據範例。 這些範例說明您可以使用此方法產生的報表類型,但您的特定報告需求可能會有所不同,而且需要不同或其他數據。 您可以從這些對象開始,並在一段時間內引進更多種類的Microsoft Entra 物件。

  • 本教學課程說明如何以已登入的使用者身分,從 Microsoft Entra 擷取數據。 若要這樣做,請確定您具有必要的角色指派。 您需要具有適當許可權的角色,才能匯出您想要使用的Microsoft Entra 資料類型:

    • 使用者資料:全域系統管理員、特殊權限角色系統管理員、使用者系統管理員
    • 群組數據:全域管理員、特殊許可權角色管理員、群組管理員
    • 應用程式和應用程式角色指派:全域管理員、特殊許可權角色管理員、應用程式管理員、雲端應用程式管理員
  • Microsoft Graph PowerShell 需要同意透過 Microsoft Graph 擷取Microsoft Entra 物件。 本教學課程中的範例需要委派 User.Read.AllGroup.Read.AllApplication.Read.AllDirectory.Read.All 許可權。 如果您打算在沒有登入用戶的情況下使用自動化來擷取數據,請改為同意對應的應用程式許可權。 如需詳細資訊,請參閱 Microsoft Graph 權限參考

    如果您尚未Microsoft Graph PowerShell 同意這些許可權,您必須是全域管理員,才能執行此同意作業。

  • 本教學課程不會說明自定義安全性屬性。 根據預設,全域管理員和其他系統管理角色,不具備從 Microsoft Entra 使用者讀取自訂安全性屬性的權限。 如果您打算擷取自定義安全性屬性,您可能需要更多角色和許可權。

  • 在安裝 Microsoft Graph PowerShell 的電腦上,確定您具有文件系統目錄的寫入許可權。 此目錄是您安裝必要的 Microsoft Graph PowerShell 模組,以及導出Microsoft Entra 數據的儲存位置。

  • 如果您也想要將該數據併入 Azure 數據總管,請確定您有權從Microsoft Entra 以外的其他數據源擷取數據。

設定 Azure 資料總管

如果您先前尚未使用 Azure 數據總管,您必須先加以設定。 您可以建立沒有 Azure 訂用帳戶或信用卡 的免費叢集。 或者,您可以建立需要 Azure 訂用帳戶的完整叢集。 若要開始使用,請參閱 快速入門:建立 Azure 數據總管叢集和資料庫

使用 PowerShell 擷取Microsoft Entra ID 數據

在本節中,您會 安裝 Microsoft Graph PowerShell 模組。 在 PowerShell 中,您可 連線到 Microsoft Graph,以擷取 Microsoft Entra ID 數據。

第一次貴組織針對此案例使用這些模組時,您必須具備全域管理員角色,才能允許 Microsoft Graph PowerShell 授與同意,以在租使用者中使用。 後續的互動可以使用低權限的角色。

  1. 開啟 PowerShell。

  2. 如果您尚未安裝所有 Microsoft Graph PowerShell 模組,請安裝必要的Microsoft Graph 模組。 本教學課程的本節需要下列課程模組:Microsoft.Graph.AuthenticationMicrosoft.Graph.UsersMicrosoft.Graph.GroupsMicrosoft.Graph.ApplicationsMicrosoft.Graph.DirectoryObjects。 如果您已安裝這些模組,請略過此步驟。

       $modules = @('Microsoft.Graph.Users', 'Microsoft.Graph.Groups', 'Microsoft.Graph.Applications', 'Microsoft.Graph.DirectoryObjects') 
       foreach ($module in $modules) { 
       Install-Module -Name $module -Scope CurrentUser -AllowClobber -Force
       } 
    
  3. 將模組匯入目前的 PowerShell 工作階段:

      $modules = @('Microsoft.Graph.Users', 'Microsoft.Graph.Groups', 'Microsoft.Graph.Applications', 'Microsoft.Graph.DirectoryObjects') 
      foreach ($module in $modules) { 
      Import-Module -Name $module 
      } 
    
  4. 聯機至 Microsoft Graph。 本教學課程的本節說明讀取使用者、群組和應用程式,因此需要 User.Read.AllGroup.Read.AllApplication.Read.AllDirectory.Read.All 許可權範圍。 如需權限的詳細資訊,請參閱 Microsoft Graph 許可權參考

      Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All", "Application.Read.All", "Directory.Read.All" -ContextScope Process -NoWelcome
    

    此命令會提示您使用 Microsoft Entra 認證登入。 登入之後,如果您是第一次連線,或需要新的許可權,您可能需要同意所需的許可權。

使用 PowerShell 查詢來擷取 Microsoft Entra ID 資料以生成自定義報表

下列查詢會使用PowerShell從 Microsoft Graph 擷取Microsoft Entra ID 數據,並將數據匯出至 JSON 檔案。 您會在本教學課程的 稍後一節中,將這些檔案匯入 Azure 資料總管中,

產生具有此資料類型之報表的案例包括:

  • 稽核員想要查看報告,其中列出由成員部門組織的10個群組群組成員。
  • 稽核員想要查看在兩個日期之間可存取應用程式之所有用戶的報告。

您也可以從Microsoft Entra 以外的來源將數據帶入 Azure 數據總管。 這項功能的案例可能是:

  • 系統管理員想要檢視從 Microsoft Entra ID 新增至應用程式的所有使用者,以及其在應用程式本身存放庫中的訪問許可權,例如 SQL 資料庫。

這些類型的報表不會內建於Microsoft Entra ID 中。 不過,您可以在 Azure 數據總管中使用自訂查詢,從 Microsoft Entra ID 擷取數據,並結合數據,以自行建立這些報表。 本教程將在 從其他來源導入數據 一節中稍後討論這個過程。

在本教學課程中,您會從下列區域擷取Microsoft Entra ID 數據:

  • 使用者資訊,例如顯示名稱、UPN 和工作詳細資料
  • 群組資訊,包括其成員資格
  • 應用程式角色的應用程式和任務分配

此數據集可讓您針對接收應用程式存取權的人員,以及其應用程式角色資訊和相關聯的時間範圍,執行一組廣泛的查詢。 請記住,這些是範例查詢,您的數據和特定需求可能會與此處顯示的內容不同。

注意

較大型的使用者可能會遇到節流現象和 429 錯誤碼,這是由 Microsoft Graph 模組所處理的。 Azure 數據總管也可能限制檔案上傳大小。

在這些 PowerShell 腳本中,您會將選取的屬性從 Microsoft Entra 物件導出至 JSON 檔案。 來自這些匯出屬性的數據可用來在 Azure 數據總管中產生自定義報告。

下列特定屬性會包含在這些範例中,因為我們使用此數據來說明您可以在 Azure 數據總管中建立的報表類型。 因為您的特定報告需求可能會與本教學課程所顯示的內容不同,因此您應該在這些腳本中包含您想要檢視報表的特定屬性。 不過,您可以遵循顯示的相同模式來協助建置腳本。

選取快照日期

硬編碼的 快照日期 可識別 JSON 檔案中的資料,並帶有特定日期。 您可以使用它來追蹤 Azure 數據總管中一段時間的類似數據集。 快照集日期也有助於比較兩個快照日期之間的資料變更。

$SnapshotDate = Get-Date -AsUTC -Format "yyyy-MM-dd"

取得 Microsoft Entra 用戶數據

此腳本會將選取的屬性從 Microsoft Entra 使用者物件匯出至 JSON 檔案。 您將在本教學課程的 稍後一節中,將此檔案和其他 JSON 檔案的其他數據匯入至 Azure 數據總管

  function Export-EntraUsersToJson { 

  # Define a hash table for property mappings 
   $propertyMappings = @{ 
    "Id" = "ObjectID" 
    "DisplayName" = "DisplayName" 
    "UserPrincipalName" = "UserPrincipalName" 
    "EmployeeId" = "EmployeeId" 
    "UserType" = "UserType" 
    "CreatedDateTime" = "CreatedDateTime" 
    "JobTitle" = "JobTitle" 
    "Department" = "Department" 
    "AccountEnabled" = "AccountEnabled" 

   # Add custom properties as needed 
    "custom_extension" = "CustomExtension" 
   } 
  # Retrieve users with specified properties and create custom objects directly 
   $users = Get-MgUser -Select ($propertyMappings.Keys) -All | ForEach-Object { 
      $userObject = @{} 
      foreach ($key in $propertyMappings.Keys) { 
        if ($key -eq "CreatedDateTime") { 
          # Convert date string directly to DateTime and format it 
          $date = [datetime]::Parse($_.$key) 
          $userObject[$propertyMappings[$key]] = $date.ToString("yyyy-MM-dd") 
        } else { 
          $userObject[$propertyMappings[$key]] = $_.$key 
        } 
      } 
      # Additional properties or transformations 
      $userObject["SnapshotDate"] = $SnapshotDate
      [pscustomobject]$userObject 
    } 
    # Convert the user data to JSON and save it to a file 
    $users | ConvertTo-Json -Depth 2 | Set-Content ".\EntraUsers.json" 
  } 
  # Execute the function 
  Export-EntraUsersToJson 

取得群組數據

產生 JSON 檔案,其中包含用來在 Azure 數據總管中建立自定義檢視的組名和標識碼。 此範例包含所有群組,但您可以視需要包含其他篩選。 如果您要篩選只包含特定群組,您可能會想要在腳本中包含邏輯來檢查巢狀群組。

    # Get all groups and select Id and DisplayName 
    $groups = Get-MgGroup -All | Select-Object Id,DisplayName 
    # Export the groups to a JSON file 
    $groups | ConvertTo-Json | Set-Content ".\EntraGroups.json" 

取得群組成員資格數據

產生具有群組成員資格的 JSON 檔案,用來在 Azure 數據總管中建立自定義檢視。 此範例包含所有群組,但您可以視需要包含其他篩選。

    # Retrieve all groups from Microsoft Entra ID 
    $groups = Get-MgGroup -All 
    # Initialize an array to store results 
    $results = @() 
    # Iterate over each group 
    foreach ($group in $groups) { 
      # Extract the group ID 
      $groupId = $group.Id 
      # Get members of the current group and select their IDs 
      $members = Get-MgGroupMember -GroupId $groupId | Select-Object -ExpandProperty Id 
      # Add a custom object with group ID and member IDs to the results array 
      $results += [PSCustomObject]@{ 
        GroupId = $groupId 
        Members = $members 
        SnapshotDate = $SnapshotDate
      } 
      # Pause for a short time to avoid rate limits 
      Start-Sleep -Milliseconds 200 
    } 
    # Convert the results array to JSON format and save it to a file 
    $results | ConvertTo-Json | Set-Content "EntraGroupMembership.json" 

取得應用程式和服務主體數據

產生 JSON 檔案,其中包含租使用者中的所有應用程式和對應的服務主體。 您將在本教學課程 稍後章節中,將此數據匯入 Azure 數據總管,以便根據此數據產生與應用程式相關的自定義報告。

    # Fetch applications and their corresponding service principals, and then export to JSON 
    Get-MgApplication -All | ForEach-Object { 
      $app = $_ 
      $sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" 
      [pscustomobject]@{ 
        Name        = $app.DisplayName 
        ApplicationId   = $app.AppId 
        ServicePrincipalId = $sp.Id 
        SnapshotDate = $SnapshotDate
      } 
    } | ConvertTo-Json -Depth 10 | Set-Content "Applications.json" 

取得應用程式角色數據

Microsoft Entra 中,為企業應用程式產生所有應用程式角色的 JSON 檔案。 將這項數據匯入 Azure 資料總管之後,您將使用它來產生包含使用者應用程式角色指派的報告。

    # Get a list of all applications, and handle pagination manually if necessary 
    $apps = Get-MgApplication -All 
    # Loop through each application to gather the desired information 
    $results = foreach ($app in $apps) { 
      # Get the service principal for the application by using its app ID 
      $spFilter = "appId eq '$($app.AppId)'" 
      $sp = Get-MgServicePrincipal -Filter $spFilter | Select-Object -First 1 
      # Process app roles, if any, for the application 
      $appRoles = if ($app.AppRoles) { 
        $app.AppRoles | Where-Object { $_.AllowedMemberTypes -contains "User" } | 
        Select-Object Id, Value, DisplayName 
      } 
      # Construct a custom object with application and service principal details 
      [PSCustomObject]@{ 
        ApplicationId    = $app.AppId 
        DisplayName     = $app.DisplayName 
        ServicePrincipalId = $sp.Id 
        AppRoles      = $appRoles 
        SnapshotDate = $SnapshotDate
      } 
    } 
    # Export the results to a JSON file 
    $results | ConvertTo-Json -Depth 4 | Out-File 'AppRoles.json' 

取得應用程式角色指派數據

產生租戶中所有使用者的應用程式角色指派的 JSON 檔案:

    $users = Get-MgUser -All 
    $result = @() 
    foreach ($user in $users) { 
      Get-MgUserAppRoleAssignment -UserId $user.Id | ForEach-Object { 
        # Use the same date formatting approach 
        $createdDateTime = $_.CreatedDateTime -replace "\\/Date\((\d+)\)\\/", '$1' 
        # Convert the milliseconds timestamp to a readable date format if necessary 
        $result += [PSCustomObject]@{ 
          AppRoleId      = $_.AppRoleId 
          CreatedDateTime   = $createdDateTime 
          PrincipalDisplayName = $_.PrincipalDisplayName 
          PrincipalId     = $_.PrincipalId 
          ResourceDisplayName = $_.ResourceDisplayName 
          ResourceId      = $_.ResourceId 
          SnapshotDate     = $SnapshotDate
        } 
      } 
    } 
    $result | ConvertTo-Json -Depth 10 | Out-File "AppRoleAssignments.json" 

建立數據表,並將 JSON 檔案與數據從 Microsoft Entra ID 匯入 Azure 數據總管

在本節中,您會將新建立Microsoft Entra ID 服務的 JSON 檔案匯入為 Azure 數據總管中的數據表,以供進一步分析。 在第一次透過 Azure 資料總管 Web UI 匯入時,您會根據 Web UI 從每個 JSON 檔案建議的架構來建立資料表。

  1. 登入 Azure 數據總管 Web UI

  2. 移至您在 Azure 數據總管叢集中設定的資料庫,或本教學課程稍早的免費叢集。

  3. 在左側功能表上,選取 [查詢]

  4. 請針對每個匯出的 JSON 檔案遵循下列步驟,將匯出的數據以新數據表的形式放入該 Azure 數據總管資料庫:

    1. 以滑鼠右鍵按下您要內嵌資料的資料庫名稱。 然後選擇 [取得資料

      查詢索引標籤的螢幕快照,其中包含資料庫的快捷方式功能表,以及醒目提示數據的命令。

    2. 從可用的清單中選取資料來源。 在本教學課程中,您會從本機檔案擷取數據,因此請選取 [本機檔案

    3. 選取 [+ 新增數據表,並根據您要匯入的 JSON 檔名輸入數據表名稱。 例如,如果您要匯入 EntraUsers.json,請將資料表命名 EntraUsers。 第一次匯入之後,數據表已經存在,而且您可以選取該數據表做為後續匯入的目標數據表。

    4. 選取 [瀏覽檔案],選取 JSON 檔案,然後選取 [下一步]

    5. Azure 數據總管會自動偵測架構,並在 [檢查] 索引卷標上提供預覽。選取 [完成] 以建立數據表並從該檔案匯入數據。 擷取資料之後,請選取 [關閉]

    6. 針對您在上一節中產生的每個 JSON 檔案重複上述步驟。

在這些步驟結束時,您會在資料庫中擁有數據表 EntraUsersEntraGroupsEntraGroupMembershipApplicationsAppRolesAppRoleAssignments

使用 PowerShell 擷取 Microsoft Entra ID 治理資料

在本節中,您會使用 PowerShell 從 Microsoft Entra ID Governance Services 擷取數據。 如果您沒有Microsoft Entra ID Governance、Microsoft Entra ID P2 或 Microsoft Entra Suite,請在 使用 Azure 數據總管建置自定義報表一節中繼續。

針對下列步驟,您可能需要 安裝 Microsoft Graph PowerShell 模組 以提取 Microsoft Entra ID Governance 數據。 第一次貴組織針對此案例使用這些模組時,您必須具備全域管理員角色,才能允許 Microsoft Graph PowerShell 授與同意,以在租使用者中使用。 後續互動可以使用權限較低的角色。

  1. 開啟 PowerShell。

  2. 如果您尚未安裝所有 Microsoft Graph PowerShell 模組,請安裝必要的Microsoft Graph 模組。 本教學課的本部分需要下列模組:Microsoft.Graph.Identity.Governance。 如果您已安裝模組,請跳至下一個步驟。

       $modules = @('Microsoft.Graph.Identity.Governance')
       foreach ($module in $modules) {
       Install-Module -Name $module -Scope CurrentUser -AllowClobber -Force
       }
    
  3. 將模組匯入目前的 PowerShell 工作階段:

      $modules = @('Microsoft.Graph.Identity.Governance')
      foreach ($module in $modules) {
      Import-Module -Name $module
      } 
    
  4. 聯機至 Microsoft Graph。 本教學課程的這一節說明如何從權利管理和存取權檢閱擷取數據,因此需要 AccessReview.Read.AllEntitlementManagement.Read.All 許可權範圍。 對於其他報告使用案例,例如 Privileged Identity Management (PIM) 或生命週期工作流程,請使用必要的許可權更新 Scopes 參數。 如需權限的詳細資訊,請參閱 Microsoft Graph 許可權參考

      Connect-MgGraph -Scopes "AccessReview.Read.All, EntitlementManagement.Read.All" -ContextScope Process -NoWelcome
    

    此命令會提示您使用 Microsoft Entra 認證登入。 登入之後,如果您是第一次連線,或需要新的許可權,您可能需要同意所需的許可權。

用來擷取自定義報表Microsoft Entra ID Governance 數據的 PowerShell 查詢

下列查詢會使用PowerShell從 Microsoft Graph 擷取Microsoft Entra ID Governance 數據,並將數據匯出至 JSON 檔案。 您會在本教學課程的 稍後一節中,將這些檔案匯入 Azure 資料總管中,

產生具有此資料類型之報表的案例包括:

  • 歷史存取權審查報告。
  • 透過使用者權限管理報告作業分派。

取得存取審核排程定義資料

使用存取權檢閱定義名稱和標識符來產生 JSON 檔案,以用來在 Azure 數據總管中建立自定義檢視。 此範例包含所有存取權檢閱,但您可以視需要包含其他篩選。 如需詳細資訊,請參閱 使用篩選查詢參數

   $allsched = Get-MgIdentityGovernanceAccessReviewDefinition -All
   $definitions = @()
   # Iterate over each definition
   foreach ($definition in $allsched) {
      $definitions += [PSCustomObject]@{
         Id = $definition.Id
         DisplayName = $definition.DisplayName
         SnapshotDate = $SnapshotDate
      }
   }
   $definitions | ConvertTo-Json -Depth 10 | Set-Content "EntraAccessReviewDefinitions.json"

取得存取權檢閱的實例數據

若要使用 PowerShell 將所有存取權檢閱定義、實例和決策匯出為結構化資料夾格式,您可以使用 Microsoft Graph API。 此方法可確保您的數據會以階層方式組織,並與指定的資料夾結構對齊。

開始之前,請注意下列幾點:

  • 請確定您有存取 Microsoft Graph 中檢閱數據的必要許可權。
  • 視數據量而定,腳本的運行時間可能會有所不同。 視需要監視程式並調整參數。

若要取得實例數據:

  1. 下載 Export_Access_Reviews.ps1 文稿,並將其儲存在本機。

  2. 在 [檔案總管] 中,解除封鎖腳本,讓您可以在PowerShell中執行腳本。

  3. 執行下列命令。 輸出會將所有數據放入三個子資料夾中:ReviewInstancesReviewInstanceDecisionItemsReviewInstanceContactedReviewers

     .\ExportAccessReviews.ps1 -InstanceStartDate "11/15/2024" -InstanceEndDate "12/15/2024" -ExportFolder "C:\AccessReviewsExport\11_15_to_12_15" 
    

取得權限管理的存取套件資料

產生 JSON 檔案,其中包含用來在 Azure 數據總管中建立自定義檢視的存取套件名稱和標識符。 此範例包含所有存取套件,但您可以視需要包含其他篩選。

   $accesspackages1 = Get-MgEntitlementManagementAccessPackage -All
   $accesspackages2 = @()
   # Iterate over each access package
   foreach ($accesspackage in $accesspackages1) {
      $accesspackages2 += [PSCustomObject]@{
         Id = $accesspackage.Id
         DisplayName = $accesspackage.DisplayName
         SnapshotDate = $SnapshotDate
      }
   }
   $accesspackages2 | ConvertTo-Json -Depth 10 | Set-Content "EntraAccessPackages.json"

取得權限管理的存取套件指派資料

產生具有指派的 JSON 檔案,以存取用來在 Azure 數據總管中建立自定義檢視的套件。 此範例包含所有已提交的作業,但您可以視需要進行其他篩選。

   $apassignments1 = Get-MgEntitlementManagementAssignment -ExpandProperty target,accessPackage -filter "state eq 'Delivered'" -all
   $apassignments2 = @()
   # Iterate over each access package assignment
   foreach ($assignment in $apassignments1) {
      $apassignments2 += [PSCustomObject]@{
         Id = $assignment.Id
         ScheduleStartDateTime = $assignment.Schedule.StartDateTime -replace "\\/Date\((\d+)\)\\/", '$1' 
         AccessPackageId = $assignment.AccessPackage.Id
         AccessPackageDisplayName = $assignment.AccessPackage.DisplayName
         TargetId = $assignment.Target.Id
         TargetDisplayName = $assignment.Target.DisplayName
         TargetEmail = $assignment.Target.Email
         TargetObjectId = $assignment.Target.ObjectId
         TargetPrincipalName = $assignment.Target.PrincipalName
         TargetSubjectType = $assignment.Target.SubjectType
         SnapshotDate = $SnapshotDate
      }
   }
   $apassignments2 | ConvertTo-Json -Depth 10 | Set-Content "EntraAccessPackageAssignments.json"

建立數據表,並將 JSON 檔案與數據從 Microsoft Entra ID Governance 匯入 Azure 數據總管

在本節中,您會將新建立Microsoft Entra ID Governance 服務的 JSON 檔案匯入 Azure 數據總管,以進行進一步分析。 這些檔案會聯結您已針對 Microsoft Entra ID 服務匯入的數據。 在第一次透過 Azure 資料總管 Web UI 匯入時,您會根據 Web UI 從每個 JSON 檔案建議的架構來建立資料表。

  1. 登入 Azure 數據總管 Web UI

  2. 在 Azure 數據總管叢集或免費叢集中,移至保存您Microsoft Entra ID 數據的資料庫。

  3. 在左側功能表上,選取 [查詢]

  4. 請針對每個匯出的 JSON 檔案遵循下列步驟,將匯出的數據以新數據表的形式放入該 Azure 數據總管資料庫:

    1. 以滑鼠右鍵按下您要內嵌資料之資料庫的資料庫名稱。 然後選擇 [取得資料

      查詢索引標籤的螢幕快照,其中包含資料庫的快捷方式功能表,以及醒目提示數據的命令。

    2. 從可用的清單中選取資料來源。 在本教學課程中,您會從本機檔案擷取數據,因此請選取 [本機檔案

    3. 選取 [+ 新增數據表,並根據您要匯入的 JSON 檔名輸入數據表名稱。 第一次匯入之後,數據表已經存在,而且您可以選取該數據表做為後續匯入的目標數據表。

    4. 選取 [瀏覽檔案],選取 JSON 檔案,然後選取 [下一步]。

    5. Azure 數據總管會自動偵測架構,並在 [檢查] 索引卷標上提供預覽。選取 [完成] 以建立數據表並從該檔案匯入數據。 擷取資料之後,請選取 [關閉]。

    6. 針對您在上一節中產生的每個 JSON 檔案,針對每個資料夾重複上述步驟。

    7. 如果資料夾中有許多檔案,您可以使用 lightingest 在建立資料表之後匯入其餘檔案。

在這些步驟結束時,除了您稍早建立的數據表之外,您還有數據表 EntraAccessReviewDefinitionsEntraAccessPackagesEntraAccessPackageAssignmentsReviewInstancesReviewInstanceDecisionItemsReviewInstanceContactedReviewers

使用 Azure 資料總管建置自定義報告

有了 Azure 數據總管中現在可用的數據,您就可以根據業務需求開始建立自定義報表:

  1. 登入 Azure 數據總管 Web UI

  2. 在左側功能表上,選取 [查詢]

下列查詢提供一般報告的範例,但您可以自訂這些報告以符合您的需求,並建立其他報告。

您也可以在 Excel 檢視報表,方法是選取 [匯出] 標籤,然後選取 [在 Excel 中開啟]。

範例:針對特定快照日期產生直接指派和群組指派的應用程式角色指派

此報告提供對誰在何時擁有何種存取目標應用程式權限的檢視。 您可以將其用於安全性稽核、合規性驗證,以及了解組織內的存取模式。

以下查詢針對 Microsoft Entra ID 中的特定應用程式,並對角色指派情況進行截至某一日期的分析。 查詢會擷取直接和群組型角色指派。 它會將此數據與 EntraUsers 數據表的使用者詳細數據合併,以及來自 AppRoles 數據表的角色資訊。 在查詢中,將 targetSnapshotDate 設定為您載入數據時所使用的 snapshotDate 值。

/// Define constants 
let targetServicePrincipalId = "<your service principal-id>"; // Target service principal ID 
let targetSnapshotDate = datetime("2024-01-13"); // Target snapshot date for the data 

// Extract role assignments for the target service principal and snapshot date 
let roleAssignments = AppRoleAssignments 
    | where ResourceId == targetServicePrincipalId and startofday(SnapshotDate) == targetSnapshotDate 
    | extend AppRoleIdStr = tostring(AppRoleId); // Convert AppRoleId to a string for easier comparison 

// Prepare user data from the EntraUsers table 
let users = EntraUsers 
    | project ObjectID, UserPrincipalName, DisplayName, ObjectIDStr = tostring(ObjectID); // Include ObjectID as string for joining 

// Prepare role data from the AppRoles table 
let roles = AppRoles 
    | mvexpand AppRoles // Expand AppRoles to handle multiple roles 
    | extend RoleName = AppRoles.DisplayName, RoleId = tostring(AppRoles.Id) // Extract role name and ID 
    | project RoleId, RoleName; 
// Process direct assignments 
let directAssignments = roleAssignments 
    | join kind=inner users on $left.PrincipalId == $right.ObjectID // Join with EntraUsers on PrincipalId 
    | join kind=inner roles on $left.AppRoleIdStr == $right.RoleId // Join with roles to get role names 
    | project UserPrincipalName, DisplayName, CreatedDateTime, RoleName, AssignmentType = "Direct", SnapshotDate; 

// Process group-based assignments 

let groupAssignments = roleAssignments 
    | join kind=inner EntraGroupMembership on $left.PrincipalId == $right.GroupId // Join with group membership 
    | mvexpand Members // Expand group members 
    | extend MembersStr = tostring(Members) // Convert the member ID to a string 
    | distinct MembersStr, CreatedDateTime, AppRoleIdStr, SnapshotDate // Get distinct values 
    | join kind=inner users on $left.MembersStr == $right.ObjectIDStr // Join with EntraUsers for user details 
    | join kind=inner roles on $left.AppRoleIdStr == $right.RoleId // Join with roles for role names 
    | project UserPrincipalName, DisplayName, CreatedDateTime, RoleName, AssignmentType = "Group", SnapshotDate; 

// Combine results from direct and group-based assignments 
directAssignments 
| union groupAssignments 

範例:使用 Microsoft Entra 數據建置基本稽核員報告,其中顯示誰在兩個日期之間可以存取應用程式

此報告提供誰在兩個日期期間擁有目標應用程式存取權的概覽。 您可以將其用於安全性稽核、合規性驗證,以及了解組織內的存取模式。

下列查詢是以 entra ID Microsoft 內的特定應用程式為目標,並分析兩個日期之間的角色指派。 查詢會從 AppRoleAssignments 數據表擷取直接角色指派,並將此數據與 EntraUsers 數據表的使用者詳細數據合併,以及 AppRoles 數據表中的角色資訊。

// Set the date range and service principal ID for the query 
let startDate = datetime('2024-01-01'); 
let endDate = datetime('2024-03-14'); 
let servicePrincipalId = "<your service principal-id>"; 

// Query AppRoleAssignments for the specified service principal within the date range 
AppRoleAssignments 
| where ResourceId == servicePrincipalId and 
    todatetime(CreatedDateTime) between (startDate .. endDate) 

// Extend AppRoleId to a string for joining 
| extend AppRoleIdStr = tostring(AppRoleId) 

// Project the necessary fields for the join with EntraUsers and AppRoles 
| project PrincipalId, AppRoleIdStr, CreatedDateTime 

// Join with EntraUsers to get user details 
| join kind=inner (EntraUsers | project UserPrincipalName, DisplayName, ObjectID) on $left.PrincipalId == $right.ObjectID 

// Join with AppRoles to get the role display names 
| join kind=inner ( 
  AppRoles | mvexpand AppRoles | project RoleIdStr = tostring(AppRoles.Id), RoleDisplayName = tostring(AppRoles.DisplayName) 
) on $left.AppRoleIdStr == $right.RoleIdStr 

// Final projection of the report with the current date and time 
| project UserPrincipalName, DisplayName, RoleDisplayName, CreatedDateTime, ReportDate = now() 

範例:檢視在兩個數據快照集日期之間新增至應用程式的使用者

這些報告提供兩個日期之間哪些使用者收到目標應用程式角色指派的檢視。 您可以使用這些報告來追蹤一段時間的應用程式存取變更。

此查詢會以Microsoft Entra 識別碼內的特定應用程式為目標,並在開始和結束日期之間變更角色指派:

// Define the date range and service principal ID for the query 

let startDate = datetime("2024-03-01"); 
let endDate = datetime("2024-03-14"); 
let servicePrincipalId = "<your service principal-id>"; 
let earlierDate = startDate; // Update this to your specific earlier date 

AppRoleAssignments 
| where SnapshotDate < endDate and ResourceId == servicePrincipalId
| project PrincipalId, AppRoleId2 = tostring(AppRoleId), CreatedDateTime 
| join kind=anti ( 
    AppRoleAssignments 
    | where SnapshotDate < earlierDate and ResourceId == servicePrincipalId 
    | project PrincipalId, AppRoleId1 = tostring(AppRoleId) 
) on PrincipalId 
| join kind=inner (EntraUsers) on $left.PrincipalId == $right.ObjectID 
| join kind=inner (AppRoles 
                   | mvexpand AppRoles 
                   | project AppRoleId=tostring(AppRoles.Id), RoleDisplayName=tostring(AppRoles.DisplayName) 
                  ) on $left.AppRoleId2 == $right.AppRoleId 
| project UserPrincipalName, DisplayName, RoleDisplayName, CreatedDateTime, PrincipalId, Change = "Added" 

範例:各種使用存取審查的查詢

檢視存取權審查完成和時間資訊

上傳數據之後,請使用下列 Kusto 查詢來檢閱它:

  • 上次存取權檢閱週期何時完成? 需要多久時間?

    ReviewInstances
    | summarize LastCompletedDate = max(ReviewInstanceEndDateTime),  
                ReviewDuration = datetime_diff('minute', max(ReviewInstanceEndDateTime), min(ReviewInstanceStartDateTime))  
    
  • 存取權檢閱程式是否準時進行(例如,每季)?

    ReviewInstances 
    | extend ExpectedFrequency = "Quarterly" // Replace with the organization's frequency
    | summarize ReviewsCompleted = count(), LastReviewEndDate = max(ReviewInstanceEndDateTime)
    | extend CurrentDate = now(),  
             TimeSinceLastReview = datetime_diff('day', now(), LastReviewEndDate)
    | extend IsOnSchedule = iff(TimeSinceLastReview <= 90, "Yes", "No") // Assuming quarterly = 90 days
    

檢視存取檢閱中的參與和投入

  • 獲指派的檢閱者是誰?

    ReviewInstanceContactedReviewers
    | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerName = DisplayName, ReviewerUserPrincipalName = UserPrincipalName, CreatedDateTime
    
  • 哪些檢閱者積极參與並提供回應?

    ReviewInstanceDecisionItems
    | where ReviewedBy_DisplayName != "AAD Access Reviews"
    | where Decision in ("Approve", "Deny")
    | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerName = ReviewedBy_DisplayName,
    ReviewerUserPrincipalName = ReviewedBy_UserPrincipalName, Decision, ReviewedDateTime
    | distinct AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerName, ReviewerUserPrincipalName, Decision
    
  • 檢閱者回應存取權檢閱要求的百分比為何?

    let TotalReviewers = ReviewInstanceContactedReviewers 
        | summarize Total = dcount(Id) by AccessReviewDefinitionId, AccessReviewInstanceId;  
    
    let RespondedReviewers = ReviewInstanceDecisionItems 
        | where ReviewedBy_DisplayName != "AAD Access Reviews"
        | where ReviewedBy_Id != "00000000-0000-0000-0000-000000000000"
        | where Decision in ("Approve", "Deny")
        | summarize Responded = dcount(ReviewedBy_Id) by AccessReviewDefinitionId, AccessReviewInstanceId;  
    
    TotalReviewers
    | join kind=leftouter RespondedReviewers on AccessReviewDefinitionId, AccessReviewInstanceId
    | extend Responded = coalesce(Responded, 0)  // Replace null with 0 for Responded
    | extend NotResponded = Total - Responded   // Calculate the number of nonresponders
    | extend ResponsePercentage = (Responded * 100.0) / Total  // Percentage of those who responded
    | extend NonResponsePercentage = (NotResponded * 100.0) / Total  // Percentage of those who didn't respond
    | project AccessReviewDefinitionId, AccessReviewInstanceId, Total, Responded, ResponsePercentage, NotResponded, NonResponsePercentage  
    
  • 每個檢閱者何時完成其工作?

    ReviewInstanceDecisionItems 
    | where Decision in ("Approve", "Deny") 
    | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerName = ReviewedBy_DisplayName, ReviewerUserPrincipalName = ReviewedBy_UserPrincipalName, ReviewedDateTime  
    
  • 哪些檢閱者沒有做出任何決定?

    let AllReviewers = ReviewInstanceContactedReviewers 
        | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerId = Id, ReviewerUserPrincipalName = UserPrincipalName, ReviewerName = DisplayName;
    
    let ActiveReviewers = ReviewInstanceDecisionItems
        | where Decision in ("Approve", "Deny")
        | where ReviewedBy_DisplayName != "AAD Access Reviews"
        | where ReviewedBy_Id != "00000000-0000-0000-0000-000000000000"
        | summarize ActiveReviewers = make_set(ReviewedBy_Id) by AccessReviewDefinitionId, AccessReviewInstanceId;
    
    AllReviewers
    | extend ReviewerId = tostring(ReviewerId)  // Ensure ReviewerId is a string
    | join kind=leftanti (
        ActiveReviewers
        | mv-expand ActiveReviewers
        | extend ActiveReviewers = tostring(ActiveReviewers)  // Cast ActiveReviewers to a string
    ) on $left.ReviewerId == $right.ActiveReviewers
    | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerUserPrincipalName, ReviewerName
    
  • 檢閱者沒有互動的百分比為何?

    let TotalReviewers = ReviewInstanceContactedReviewers 
        | summarize Total = dcount(Id) by AccessReviewDefinitionId, AccessReviewInstanceId;
    
    let RespondedReviewers = ReviewInstanceDecisionItems
        | where ReviewedBy_DisplayName != "AAD Access Reviews"
        | where ReviewedBy_Id != "00000000-0000-0000-0000-000000000000"
        | where Decision in ("Approve", "Deny")
        | summarize Responded = dcount(ReviewedBy_Id) by AccessReviewDefinitionId, AccessReviewInstanceId;
    
    TotalReviewers
    | join kind=leftouter RespondedReviewers on AccessReviewDefinitionId, AccessReviewInstanceId
    | extend Responded = coalesce(Responded, 0)  // Replace null with 0 for Responded
    | extend NotResponded = Total - Responded   // Calculate the number of nonresponders
    | extend ResponsePercentage = (Responded * 100.0) / Total  // Percentage of those who responded
    | extend NonResponsePercentage = (NotResponded * 100.0) / Total  // Percentage of those who didn't respond
    | project AccessReviewDefinitionId, AccessReviewInstanceId, Total, Responded, ResponsePercentage, NotResponded, NonResponsePercentage  
    
  • 對於未回應的檢閱者或等待中的決策,是否觸發了提醒?

    // Step 1: Get the list of all reviewers
    let TotalReviewers = ReviewInstanceContactedReviewers 
        | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerId = Id, ReviewerUserPrincipalName = UserPrincipalName, ReviewerName = DisplayName;
    
    // Step 2: Get the list of reviewers who responded 
    let RespondedReviewers = ReviewInstanceDecisionItems
        | where ReviewedBy_DisplayName != "AAD Access Reviews"
        | where ReviewedBy_Id != "00000000-0000-0000-0000-000000000000"
        | where Decision in ("Approve", "Deny")
        | project AccessReviewDefinitionId, AccessReviewInstanceId, RespondedReviewerId = ReviewedBy_Id;
    
    // Step 3: Get the list of review instances
    let ReviewInstancesWithDetails = ReviewInstances
        | project AccessReviewDefinitionId = ReviewDefinitionId,  
                  AccessReviewInstanceId = ReviewInstanceId,  
                  RemindersSent = ReviewDefinitionSettings_ReminderNotificationsEnabled,  
                  StartDate = todatetime(ReviewInstanceStartDateTime),  
                  EndDate = todatetime(ReviewInstanceEndDateTime)
        | extend
                  ReminderSentDate = iif(RemindersSent, StartDate + (EndDate - StartDate) / 2, datetime(null));
    
    // Step 4: Identify nonresponsive reviewers and join with review instance details
    TotalReviewers
    | join kind=leftouter (ReviewInstancesWithDetails) on AccessReviewDefinitionId, AccessReviewInstanceId
    | join kind=leftanti RespondedReviewers on $left.ReviewerId == $right.RespondedReviewerId
    | project AccessReviewDefinitionId, AccessReviewInstanceId, ReviewerUserPrincipalName, ReviewerName, RemindersSent, ReminderSentDate
    

檢視權限審查帶來的使用者和存取變更

  • 誰在存取檢閱期間失去對特定資源的存取權?

    ReviewInstanceDecisionItems 
    | where Decision == "Deny" 
    | project User = Principal_DisplayName, Resource = Resource_DisplayName, Decision, Justification 
    
  • 使用者是否因為不活躍而被標示?

    ReviewInstanceDecisionItems
    | where Insights contains "inactive"
    | project User = Principal_DisplayName, Resource = Resource_DisplayName, Insights, Decision
    
  • 存取移除日期和失去存取的原因為何?

    ReviewInstanceDecisionItems
    | where Decision == "Deny"
    | project User = Principal_DisplayName, Resource=Resource_DisplayName, AccessRemovalDate = AppliedDateTime, Reason = Justification  
    
  • 哪些用戶沒有做出任何決定?

    ReviewInstanceDecisionItems
    | where Decision == "NotReviewed"
    | project User = Principal_DisplayName, Resource=Resource_DisplayName
    
  • 哪些評論沒有評論者?

    ReviewInstances
    | join kind=leftanti (
        ReviewInstanceContactedReviewers
        | summarize by AccessReviewInstanceId
    ) on $left.ReviewInstanceId == $right.AccessReviewInstanceId  
    
  • 哪些評論是沒有使用者的?

    ReviewInstances 
    | join kind=leftanti (
        ReviewInstanceDecisionItems
        | summarize by AccessReviewInstanceId
    ) on $left.ReviewInstanceId == $right.AccessReviewInstanceId
    

存取審查決策摘要

  • 使用者做出哪些決策:已核准、拒絕或未變更?

    ReviewInstanceDecisionItems
    | summarize count() by Decision
    
  • 已核准或拒絕存取的用戶數目為何?

    ReviewInstanceDecisionItems
    | summarize ApprovedCount = countif(Decision == "Approve"), DeniedCount = countif(Decision == "Deny")
    
  • 已記載核准原因嗎?

    ReviewInstanceDecisionItems
    | where Decision == "Approve" and isnotempty(Justification)
    | summarize count() by ReviewedBy_DisplayName
    

確認存取審核的品質並檢查完整性

  • 對休眠使用者而言,存取撤銷是考慮的嗎?

    ReviewInstanceDecisionItems
    | where Insights contains "inactive" and Decision == "Deny"
    | project User = Principal_DisplayName, Decision
    
  • 是否未正確移除任何存取權?

    ReviewInstanceDecisionItems
    | where ApplyResult != "New" and ApplyResult != "AppliedSuccessfully"
    
  • 檢閱者是否記錄其決策?

    ReviewInstanceDecisionItems
    | where isnotempty(Justification)
    | summarize count() by ReviewedBy_DisplayName
    
  • 是否已為每個使用者擷取批注?

    ReviewInstanceDecisionItems
    | where isnotempty(Justification)
    | project User = Principal_DisplayName, Resource = Resource_DisplayName, Comments = Justification
    

設定持續匯入

此教程說明一次性數據擷取、轉換和載入(ETL)過程,將資料一次性擷取、轉換並載入到 Azure 數據總管,以供生成報告。 為了持續報告或比較一段時間的變更,您可以自動化將 Microsoft Entra 的資料填入 Azure 數據總管的過程,以確保您的資料庫持續擁有最新的數據。

您可以使用 Azure 自動化這項 Azure 雲端服務,來託管從 Microsoft Entra ID 和 Microsoft Entra ID 控管擷取數據所需的 PowerShell 腳本。 如需詳細資訊,請參閱 透過 Azure 自動化和 Microsoft Graph 自動化 Microsoft Entra ID 管理工作

您也可以使用 Azure 功能或命令行工具,例如 lightingest 來帶入資料並填入現有的數據表。 如需詳細資訊,請參閱 使用 LightIngest 將資料內嵌至 Azure 資料總管

例如,若要將目前目錄中 EntraAccessPackages.json 檔案載入至 EntraAccessPackages 數據表作為目前登入的使用者,請使用下列命令:

az login
LightIngest.exe "https://ingest-CLUSTERHOSTNAME;Fed=True" -database:"DATABASE" -table:EntraAccessPackages -sourcepath:"." -pattern:"EntraAccessPackages.json" -format:multijson -azcli:true

在 Azure 監視器中查詢數據

如果您要將稽核、登入或其他Microsoft Entra 記錄傳送至 Azure 監視器,您可以將來自該 Azure 監視器 Log Analytics 工作區的記錄併入查詢中。 如需 Azure 監視器與 Azure 數據總管之間關聯性的詳細資訊,請參閱 使用 Azure 數據總管查詢 Azure 監視器中的數據

  1. 登入 Microsoft Entra 系統管理中心。

  2. 選取 [診斷設定]

  3. 選取您要傳送記錄的Log Analytics工作區。

  4. 在 Log Analytics 工作區概觀上,記錄訂用帳戶標識碼、資源名稱,以及工作區的名稱。

  5. 登入 Azure 入口網站。

  6. 移至 Azure 資料總管 Web UI

  7. 請確定已列出您的 Azure 數據總管叢集。

  8. 選擇 + 新增>連線

  9. 在 [[新增連線] 視窗中,輸入 Log Analytics 工作區中的 URL。 URL 是由雲端特定主機名、訂用帳戶標識碼、資源組名和 Azure 監視器 Log Analytics 工作區的工作區名稱所組成,如 新增 Log Analytics 工作區中所述。

  10. 建立連線之後,Log Analytics 工作區會出現在左側窗格中,其中包含您的原生 Azure 數據總管叢集。

    選取 [查詢],然後選取您的 Azure 數據總管叢集。

  11. 在查詢窗格中,請參閱包含 Microsoft Entra 記錄的 Azure 數據總管查詢中的 Azure 監視器數據表。 例如:

    let CL1 = 'https://ade.loganalytics.io/subscriptions/*subscriptionid*/resourcegroups/*resourcegroupname*/providers/microsoft.operationalinsights/workspaces/*workspacename*';
    cluster(CL1).database('*workspacename*').AuditLogs | where Category == "EntitlementManagement"  and OperationName == "Fulfill access package assignment request"
    | mv-expand TargetResources | where TargetResources.type == 'AccessPackage' | project ActivityDateTime,APID = toguid(TargetResources.id)
    | join EntraAccessPackage on $left.APID == $right.Id
    | limit 100
    

從其他來源帶入數據

您可以在 Azure 數據總管中 建立其他數據表,以從其他來源擷取數據。 如果資料位於 JSON 檔案中(類似上述範例)或 CSV 檔案中,您可以在第一次 從檔案取得資料時建立資料表,。 建立數據表之後,您也可以 使用 LightIngest 從 JSON 或 CSV 檔案將數據內嵌至 Azure 數據總管

如需數據擷取的詳細資訊,請參閱 Azure 數據總管數據擷取概觀

範例:結合來自 Microsoft Entra 的應用程式的指派和第二個來源,以建立所有在兩個日期之間曾存取應用程式的用戶報告

此報告說明如何結合兩個不同的系統的數據,以在 Azure 數據總管中建立自定義報表。 其會將使用者、其角色和其他屬性的相關資料從兩個系統彙總成統一格式,以進行分析或報告。

下列範例假設名為 salesforceAssignments 的數據表已填入來自另一個應用程式的數據。 數據表具有數據行 UserNameNameEmployeeIdDepartmentJobTitleAppNameRoleCreatedDateTime

// Define the date range and service principal ID for the query 

let startDate = datetime("2023-06-01"); 
let endDate = datetime("2024-03-13"); 
let servicePrincipalId = "<your service principal-id>"; 

// Pre-process AppRoleAssignments with specific filters and projections 
let processedAppRoleAssignments = AppRoleAssignments 
    | where ResourceId == servicePrincipalId and todatetime(CreatedDateTime) between (startDate .. endDate) 
    | extend AppRoleId = tostring(AppRoleId) 
    | project PrincipalId, AppRoleId, CreatedDateTime, ResourceDisplayName; // Exclude DeletedDateTime and keep ResourceDisplayName 

// Pre-process AppRoles to get RoleDisplayName for each role 
let processedAppRoles = AppRoles 
    | mvexpand AppRoles 
    | project AppRoleId = tostring(AppRoles.Id), RoleDisplayName = tostring(AppRoles.DisplayName); 

// Main query: Process EntraUsers by joining with processed role assignments and roles 
EntraUsers 
    | join kind=inner processedAppRoleAssignments on $left.ObjectID == $right.PrincipalId // Join with role assignments 
    | join kind=inner processedAppRoles on $left.AppRoleId == $right.AppRoleId // Join with roles to get display names 

    // Summarize to get the latest record for each unique combination of user and role attributes 
    | summarize arg_max(AccountEnabled, *) by UserPrincipalName, DisplayName, tostring(EmployeeId), Department, JobTitle, ResourceDisplayName, RoleDisplayName, CreatedDateTime 

    // Final projection of relevant fields, including source indicator and report date 
    | project UserPrincipalName, DisplayName, EmployeeId=tostring(EmployeeId), Department, JobTitle, AccountEnabled=tostring(AccountEnabled), ResourceDisplayName, RoleDisplayName, CreatedDateTime, Source="EntraUsers", ReportDate = now() 

// Union with processed salesforceAssignments to create a combined report 
| union ( 
    salesforceAssignments 

    // Project fields from salesforceAssignments to align with the EntraUsers data structure 
    | project UserPrincipalName = UserName, DisplayName = Name, EmployeeId = tostring(EmployeeId), Department, JobTitle, AccountEnabled = "N/A", ResourceDisplayName = AppName, RoleDisplayName = Role, CreatedDateTime, Source = "salesforceAssignments", ReportDate = now() 
)