AI - 61301 - AI agent identities have assigned human owners and sponsors - #1471
Conversation
|
Aleksandar Nikolić (@alexandair) - one more for you pls for the PoSH code |
There was a problem hiding this comment.
Pull request overview
This PR adds an “Agent identity accountability” dashboard visualization and the underlying tenant-info collection to show how many AI agent identities have assigned human owners and/or effective sponsors.
Changes:
- Introduces an
AgentOwnershipDistributiontenant-info payload (schema + demo data) and renders it on the report dashboard. - Adds a new Recharts-based
AgentOwnershipDistributioncard component with drill-down lists of agents per bucket. - Collects the distribution during
Invoke-ZtTenantInfowhen running theAI(orAll) pillar and adds targeted Pester coverage.
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/report/src/pages/Dashboard.tsx | Renders the new AI “Agent ownership distribution” card when the metric exists. |
| src/report/src/config/report-data.ts | Adds TS report-contract types + demo payload for agent ownership distribution. |
| src/report/src/components/overview/agent-ownership-distribution.tsx | New UI component (pie + expandable bucket lists) for agent accountability. |
| src/report/src-curent/pages/Dashboard.tsx | Mirrors dashboard rendering in the src-curent report variant. |
| src/report/src-curent/config/report-data.ts | Mirrors TS report-contract updates + demo data in the src-curent variant. |
| src/report/src-curent/components/overview/agent-ownership-distribution.tsx | Mirrors the new UI component in the src-curent variant. |
| src/powershell/private/tenantinfo/Invoke-ZtTenantInfo.ps1 | Hooks the new tenant-info collector into the AI pillar execution path. |
| src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1 | New collector: queries DB, resolves group sponsor effectiveness via Graph batch $count, and builds the distribution payload. |
| code-tests/commands/Invoke-ZtTenantInfo.Tests.ps1 | Adds tests ensuring the new collector is invoked for AI and not for unrelated pillars. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Aleksandar Nikolić (alexandair)
left a comment
There was a problem hiding this comment.
Manoj Kesana (@Manoj-Kesana) Please, address my feedback.
Finding 1 — Snapshot mismatches are classified as ownerless instead of excluded
Severity: High
File: src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1
Primary review range: lines 17–31
Related range: lines 142–148
Review comment
The left join makes a missing
ServicePrincipalsnapshot indistinguishable from an agent with an emptyownerscollection. If an agent exists inAgentIdentitybut is absent from the separately exportedServicePrincipaltable,sp.ownersis null and this query reportsownerCount = 0; the agent is then placed inownerOnlyorneither. An agent present only inServicePrincipalis omitted becauseAgentIdentityis the left side. This violates the spec requirement to exclude IDs present in only one snapshot and report them throughskippedCount; line 147 currently hard-codes that count to zero. Compare both ID sets, classify only their intersection, and deriveskippedCountfrom their symmetric difference.
Affected code
# Lines 17–31
select
ai.id,
ai.displayName,
ai.accountEnabled,
to_json(ai.sponsors) as sponsorsJson,
case
when sp.owners is null then 0
when json_type(sp.owners) = 'ARRAY' then coalesce(json_array_length(sp.owners), 0)
when json_type(sp.owners) = 'OBJECT' then 1
else 0
end as ownerCount
from main.AgentIdentity ai
left join main.ServicePrincipal sp on ai.id = sp.id# Lines 142–148
$distribution = [PSCustomObject]@{
ownerAndSponsor = $agentsByBucket.ownerAndSponsor.Count
ownerOnly = $agentsByBucket.ownerOnly.Count
sponsorOnly = $agentsByBucket.sponsorOnly.Count
neither = $agentsByBucket.neither.Count
skippedCount = 0Why this is a real failure
AgentIdentity and ServicePrincipal are produced by separate export requests. Directory churn between those requests can produce either of these states:
| AgentIdentity row | ServicePrincipal row | Current result | Required result |
|---|---|---|---|
| Present | Missing | Included with HasOwner = false |
Excluded; increment skippedCount |
| Missing | Present | Silently absent | Excluded; increment skippedCount |
| Present | Present | Included | Included |
The dashboard presents neither as the highest-risk population. A snapshot mismatch can therefore falsely report an agent as having no accountable human.
Required implementation change
- Restrict the
ServicePrincipalside to"@odata.type" = '#microsoft.graph.agentIdentity'. - Compare the complete ID sets with a full outer join, or issue two ID queries and compare them in PowerShell.
- Mark a row as matched only when it exists in both exports.
- Build
$agentIdentitiesonly from matched rows. - Set
skippedCountto the number of unmatched IDs rather than zero. - Log one aggregate warning with the skipped count; do not classify unmatched identities.
A suitable query shape is:
with agent_owners as (
select
id,
owners
from main.ServicePrincipal
where "@odata.type" = '#microsoft.graph.agentIdentity'
)
select
coalesce(ai.id, sp.id) as id,
ai.displayName,
ai.accountEnabled,
to_json(ai.sponsors) as sponsorsJson,
ai.id is not null as hasSponsorSnapshot,
sp.id is not null as hasOwnerSnapshot,
case
when sp.owners is null then 0
when json_type(sp.owners) = 'ARRAY' then coalesce(json_array_length(sp.owners), 0)
when json_type(sp.owners) = 'OBJECT' then 1
else 0
end as ownerCount
from main.AgentIdentity ai
full outer join agent_owners sp on ai.id = sp.id
order by coalesce(ai.displayName, '')Then partition the rows before sponsor processing:
$snapshotMismatches = @($rows | Where-Object {
-not ($_.hasSponsorSnapshot -and $_.hasOwnerSnapshot)
})
$matchedRows = @($rows | Where-Object {
$_.hasSponsorSnapshot -and $_.hasOwnerSnapshot
})
# Build the four buckets from $matchedRows only.
$distribution.skippedCount = $snapshotMismatches.CountAcceptance tests
- Agent in both tables is classified normally and
skippedCountremains zero. - Agent only in
AgentIdentityis excluded and incrementsskippedCount. - Agent only in
ServicePrincipalis excluded and incrementsskippedCount. - One mismatch does not alter any of the four bucket counts.
- The number of classified agents plus
skippedCountequals the union of the two source ID sets.
Finding 2 — Failed group-count requests are treated as proof that the sponsor group is empty
Severity: High
File: src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1
Primary review range: lines 57–75
Related range: lines 103–120
Review comment
A failed
transitiveMembers/$countrequest does not establish that the group has zero members, but this branch stores$falseand evaluation later treats that value as “no effective sponsor.” For failures returned as matched results—such as non-4xx responses—agents with valid group sponsors can be moved intoownerOnlyorneither. There is also a helper-contract problem: unsuccessfulInvoke-ZtGraphBatchRequest -Matchedresults do not consistently return the original group ID inArgument, so$countResult.Argumentcannot safely key the lookup on failure. Fail the distribution (or explicitly exclude every affected agent) unless every requested group ID has one successful count response; only a successful count of zero may map to$false.
Affected code
# Lines 68–75
foreach ($countResult in $groupCountResults) {
$gid = $countResult.Argument
if (-not $countResult.Success) {
$groupHasMembers[$gid] = $false
Write-PSFMessage "Failed to get transitive member count for sponsor group $gid (status $($countResult.Status))." -Tag Test -Level Warning
continue
}
$groupHasMembers[$gid] = ([int]($countResult.Result | Select-Object -First 1) -gt 0)
}# Lines 116–120
if ($odataType -eq '#microsoft.graph.user' -or
($odataType -eq '#microsoft.graph.group' -and $groupHasMembers[$sponsor.id])) {
$hasSponsor = $true
break
}Failure sequence
- An agent has a group sponsor with effective members.
- Graph returns a transient failed subresponse for that group.
- The code records
$false, or records it under a non-ID failure argument. $groupHasMembers[$sponsor.id]evaluates as false/null.- The valid sponsor is discarded.
- The agent is shown in the wrong accountability bucket.
The official endpoint is valid and returns a plain integer on success:
GET /v1.0/groups/{id}/transitiveMembers/$countConsistencyLevel: eventual
The defect is failure interpretation, not endpoint construction.
Required implementation change
Use fail-closed aggregation semantics for the chart: every requested group must have exactly one successful result before classification starts.
$groupResolutionFailed = $false
foreach ($countResult in @($groupCountResults)) {
if (-not $countResult.Success) {
$groupResolutionFailed = $true
Write-PSFMessage "Failed to resolve a sponsor group member count (status $($countResult.Status))." -Tag Test -Level Warning
continue
}
$gid = [string]$countResult.Argument
$groupHasMembers[$gid] = (($countResult.Result | Select-Object -First 1) -gt 0)
}
if ($groupResolutionFailed -or $groupHasMembers.Count -ne $uniqueGroupIds.Count) {
Write-PSFMessage 'Agent ownership distribution was omitted because one or more sponsor groups could not be resolved.' -Tag Test -Level Warning
Add-ZtTenantInfo -Name $tenantInfoName -Value $null
return
}This is preferable to incrementing the existing skippedCount, because the UI defines that field specifically as identities excluded due to owner/sponsor snapshot mismatch. If partial group-resolution results need to be shown, introduce a separately named unresolved count and exclude every agent dependent on an unresolved group.
Acceptance tests
- Successful count greater than zero makes the group an effective sponsor.
- Successful count equal to zero does not make the group an effective sponsor.
- Any unsuccessful matched response prevents publication of a potentially misleading distribution.
- A missing response for any requested group also prevents publication.
- Duplicate use of the same sponsor group issues one count request.
- No lookup is keyed using the failed response’s batch-request object.
Finding 3 — Tests cover dispatch only; none of the new security classification behavior is exercised
Severity: Medium
Files:
code-tests/commands/Invoke-ZtTenantInfo.Tests.ps1, lines 111–126src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1, lines 6–158
Review comment
These tests prove only that
Invoke-ZtTenantInfocalls the new helper for the AI pillar. The 158-line helper’s SQL join, snapshot exclusion, sponsor JSON conversion, group deduplication, batch failure behavior, and four-way classification are all untested. Both correctness defects above would pass the current suite. Add focused command tests forAdd-ZtAgentOwnershipDistributionthat mockInvoke-DatabaseQuery,Invoke-ZtGraphBatchRequest, andAdd-ZtTenantInfo, and assert the complete published value.
Current coverage
# Lines 111–126
It "Should collect agent ownership distribution for AI assessments" {
Invoke-ZtTenantInfo -Database 'test' -Pillar 'AI'
Should -Invoke Add-ZtAgentOwnershipDistribution -Times 1 -Exactly -ParameterFilter {
$Database -eq 'test'
}
}
It "Should not collect agent ownership distribution for unrelated pillar-only assessments" {
Invoke-ZtTenantInfo -Database 'test' -Pillar 'Devices'
Should -Invoke Add-ZtAgentOwnershipDistribution -Times 0 -Exactly
}These tests never invoke Add-ZtAgentOwnershipDistribution because it is mocked in BeforeEach.
Required test file
Add code-tests/commands/Add-ZtAgentOwnershipDistribution.Tests.ps1 with at least these contexts:
- Four-way classification
- owner + user sponsor
- owner only
- user sponsor only
- neither
- Group sponsor resolution
- group count greater than zero
- group count zero
- repeated group ID is deduplicated
- Snapshot consistency
- owner-only snapshot row is skipped
- sponsor-only snapshot row is skipped
skippedCountis exact
- Failure handling
- SQL exception publishes
$null - batch exception publishes
$null - unsuccessful batch member publishes
$null - omitted batch result publishes
$null
- SQL exception publishes
- Empty tenant
- publishes a zero-valued distribution with empty agent arrays
- Output integrity
- each matched identity appears in exactly one bucket
- bucket counts match the lengths of their detail arrays
Assertions should capture the value passed to Add-ZtTenantInfo, not merely verify that the command was called:
Should -Invoke Add-ZtTenantInfo -Times 1 -Exactly -ParameterFilter {
$Name -eq 'AgentOwnershipDistribution' -and
$Value.ownerAndSponsor -eq 1 -and
$Value.ownerOnly -eq 1 -and
$Value.sponsorOnly -eq 1 -and
$Value.neither -eq 1 -and
$Value.skippedCount -eq 0
}
Aleksandar Nikolić (alexandair)
left a comment
There was a problem hiding this comment.
LGTM
f8cdec0
into
astaykov/preview-report
No description provided.