tests/Test-Assessment.61014.ps1
|
<#
.SYNOPSIS Agent identities and blueprint principals have assigned technical owners and no disabled agents remain in the directory. .DESCRIPTION Evaluates two sub-conditions for Microsoft Entra Agent ID: A. Every agent identity (microsoft.graph.agentIdentity) and every agent identity blueprint principal (microsoft.graph.agentIdentityBlueprintPrincipal) has at least one assigned owner. B. No agent identity or blueprint principal exists with accountEnabled == false. The check reads the ServicePrincipal table, which is exported under the AI pillar with agentIdentityBlueprintId and the owners related property. Derived types are identified via the "@odata.type" column emitted by Microsoft Graph for derived service principal types. .NOTES Test ID: 61014 Pillar: AI Workshop Task: AI_002 Spec: ztspecs/specs/ai/61014.md #> function Test-Assessment-61014 { [ZtTest( Category = 'Agent Lifecycle', ImplementationCost = 'Medium', CompatibleLicense = ('AAD_BASIC', 'AAD_PREMIUM'), Service = ('Graph'), Pillar = 'AI', RiskLevel = 'High', SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce'), TestId = 61014, Title = 'Agent identities and blueprint principals have assigned technical owners and no disabled agents remain in the directory', UserImpact = 'Medium' )] [CmdletBinding()] param( $Database ) #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking agent identity ownership and disabled state' Write-ZtProgress -Activity $activity -Status 'Querying agent identities and blueprint principals' # Pull agent identities and blueprint principals from the exported ServicePrincipal table. # The owners column is exported as JSON; computing the count in SQL avoids fragile # PowerShell coercion of NULL/object/array values. $sql = @" select id, appId, displayName, accountEnabled, "@odata.type" as odataType, agentIdentityBlueprintId, createdByAppId, case when owners is null then 0 when json_type(owners) = 'ARRAY' then coalesce(json_array_length(owners), 0) when json_type(owners) = 'OBJECT' then 1 else 0 end as ownerCount from main.ServicePrincipal where "@odata.type" in ( '#microsoft.graph.agentIdentity', '#microsoft.graph.agentIdentityBlueprintPrincipal' ) order by "@odata.type", displayName "@ $results = @() try { $results = @(Invoke-DatabaseQuery -Database $Database -Sql $sql) } catch { Write-PSFMessage "Failed to query agent identities from ServicePrincipal table: $_" -Tag Test -Level Warning -ErrorRecord $_ Add-ZtTestResultDetail -SkippedBecause NotApplicable return } if (-not $results -or $results.Count -eq 0) { Add-ZtTestResultDetail -SkippedBecause NotApplicable return } #endregion Data Collection #region Assessment Logic Write-ZtProgress -Activity $activity -Status 'Evaluating ownership and disabled state' $ownerlessObjects = @($results | Where-Object { [int]$_.ownerCount -lt 1 }) $disabledObjects = @($results | Where-Object { $_.accountEnabled -eq $false }) $passed = ($ownerlessObjects.Count -eq 0) -and ($disabledObjects.Count -eq 0) #endregion Assessment Logic #region Report Generation if ($passed) { $testResultMarkdown = "✅ Every agent identity and blueprint principal has at least one assigned owner and no disabled agent identities or blueprint principals exist in the directory.`n`n%TestResult%" } else { $testResultMarkdown = "❌ One or more agent identities or blueprint principals have no assigned owner, or one or more disabled agent identities or blueprint principals exist in the directory.`n`n%TestResult%" } # Portal URLs (kept out of markdown literals per ZT engineering standards). $agentIdentityUrlFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/AgentIdentity.MenuView/~/overview/objectId/{0}/menuId/overview' $blueprintUrlFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/AgentBlueprintDetails.MenuView/~/overview/objectId/{0}' $allAgentsPortalUrl = 'https://entra.microsoft.com/?feature.msaljs=true#view/Microsoft_AAD_RegisteredApps/AllAgents.MenuView/~/allAgentIds' $allBlueprintsPortalUrl = 'https://entra.microsoft.com/?feature.msaljs=true#view/Microsoft_AAD_RegisteredApps/AllAgents.MenuView/~/allAgentBlueprints' $agentIdentityCount = @($results | Where-Object { $_.odataType -eq '#microsoft.graph.agentIdentity' }).Count $blueprintCount = @($results | Where-Object { $_.odataType -eq '#microsoft.graph.agentIdentityBlueprintPrincipal' }).Count $formatTemplate = @' ## {0} | Object type | Display name | Account enabled | | :---------- | :----------- | :-------------- | {1} '@ $mdInfo = "`n**Agent identity inventory:**`n`n" $mdInfo += "* [Agent identities]($allAgentsPortalUrl): $agentIdentityCount`n" $mdInfo += "* [Blueprint principals]($allBlueprintsPortalUrl): $blueprintCount`n" $mdInfo += "* Ownerless objects: $($ownerlessObjects.Count)`n" $mdInfo += "* Disabled objects: $($disabledObjects.Count)`n" if ($ownerlessObjects.Count -gt 0) { $tableRows = '' foreach ($item in ($ownerlessObjects | Sort-Object odataType, displayName)) { $typeName = if ($item.odataType) { $item.odataType -replace '^#microsoft\.graph\.', '' } else { 'unknown' } $displayName = Get-SafeMarkdown -Text $item.displayName $detailsUrl = if ($item.odataType -eq '#microsoft.graph.agentIdentity') { $agentIdentityUrlFormat -f $item.id } elseif ($item.odataType -eq '#microsoft.graph.agentIdentityBlueprintPrincipal') { $blueprintUrlFormat -f $item.id } else { $allAgentsPortalUrl } $nameLink = "[$displayName]($detailsUrl)" $enabled = if ($null -eq $item.accountEnabled) { 'N/A' } elseif ($item.accountEnabled) { '✅ Enabled' } else { '❌ Disabled' } $tableRows += "| ``$typeName`` | $nameLink | $enabled |`n" } $mdInfo += $formatTemplate -f 'Agent identities and blueprint principals without an assigned owner', $tableRows } if ($disabledObjects.Count -gt 0) { $tableRows = '' foreach ($item in ($disabledObjects | Sort-Object odataType, displayName)) { $typeName = if ($item.odataType) { $item.odataType -replace '^#microsoft\.graph\.', '' } else { 'unknown' } $displayName = Get-SafeMarkdown -Text $item.displayName $detailsUrl = if ($item.odataType -eq '#microsoft.graph.agentIdentity') { $agentIdentityUrlFormat -f $item.id } elseif ($item.odataType -eq '#microsoft.graph.agentIdentityBlueprintPrincipal') { $blueprintUrlFormat -f $item.id } else { $allAgentsPortalUrl } $nameLink = "[$displayName]($detailsUrl)" $enabled = if ($null -eq $item.accountEnabled) { 'N/A' } elseif ($item.accountEnabled) { '✅ Enabled' } else { '❌ Disabled' } $tableRows += "| ``$typeName`` | $nameLink | $enabled |`n" } $mdInfo += $formatTemplate -f 'Disabled agent identities and blueprint principals', $tableRows } $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '61014' Title = 'Agent identities and blueprint principals have assigned technical owners and no disabled agents remain in the directory' Status = $passed Result = $testResultMarkdown } Add-ZtTestResultDetail @params } |