Skip to content

AI - 61301 - AI agent identities have assigned human owners and sponsors - #1471

Merged
Thomas Detzner (tdetzner) merged 5 commits into
astaykov/preview-reportfrom
AI-61301
Aug 3, 2026
Merged

AI - 61301 - AI agent identities have assigned human owners and sponsors#1471
Thomas Detzner (tdetzner) merged 5 commits into
astaykov/preview-reportfrom
AI-61301

Conversation

@Manoj-Kesana

Copy link
Copy Markdown
Collaborator

No description provided.

@tdetzner

Copy link
Copy Markdown
Collaborator

Aleksandar Nikolić (@alexandair) - one more for you pls for the PoSH code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AgentOwnershipDistribution tenant-info payload (schema + demo data) and renders it on the report dashboard.
  • Adds a new Recharts-based AgentOwnershipDistribution card component with drill-down lists of agents per bucket.
  • Collects the distribution during Invoke-ZtTenantInfo when running the AI (or All) 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.

Comment thread src/report/src/config/report-data.ts
Comment thread src/report/src-curent/config/report-data.ts
Comment thread src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1 Outdated
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>
@Manoj-Kesana Manoj Kesana (Manoj-Kesana) added the ready for review PR is ready for review and merging label Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ServicePrincipal snapshot indistinguishable from an agent with an empty owners collection. If an agent exists in AgentIdentity but is absent from the separately exported ServicePrincipal table, sp.owners is null and this query reports ownerCount = 0; the agent is then placed in ownerOnly or neither. An agent present only in ServicePrincipal is omitted because AgentIdentity is the left side. This violates the spec requirement to exclude IDs present in only one snapshot and report them through skippedCount; line 147 currently hard-codes that count to zero. Compare both ID sets, classify only their intersection, and derive skippedCount from 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    = 0

Why 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

  1. Restrict the ServicePrincipal side to "@odata.type" = '#microsoft.graph.agentIdentity'.
  2. Compare the complete ID sets with a full outer join, or issue two ID queries and compare them in PowerShell.
  3. Mark a row as matched only when it exists in both exports.
  4. Build $agentIdentities only from matched rows.
  5. Set skippedCount to the number of unmatched IDs rather than zero.
  6. 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.Count

Acceptance tests

  • Agent in both tables is classified normally and skippedCount remains zero.
  • Agent only in AgentIdentity is excluded and increments skippedCount.
  • Agent only in ServicePrincipal is excluded and increments skippedCount.
  • One mismatch does not alter any of the four bucket counts.
  • The number of classified agents plus skippedCount equals 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/$count request does not establish that the group has zero members, but this branch stores $false and 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 into ownerOnly or neither. There is also a helper-contract problem: unsuccessful Invoke-ZtGraphBatchRequest -Matched results do not consistently return the original group ID in Argument, so $countResult.Argument cannot 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

  1. An agent has a group sponsor with effective members.
  2. Graph returns a transient failed subresponse for that group.
  3. The code records $false, or records it under a non-ID failure argument.
  4. $groupHasMembers[$sponsor.id] evaluates as false/null.
  5. The valid sponsor is discarded.
  6. 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/$count
  • ConsistencyLevel: 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–126
  • src/powershell/private/tenantinfo/ai/Add-ZtAgentOwnershipDistribution.ps1, lines 6–158

Review comment

These tests prove only that Invoke-ZtTenantInfo calls 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 for Add-ZtAgentOwnershipDistribution that mock Invoke-DatabaseQuery, Invoke-ZtGraphBatchRequest, and Add-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:

  1. Four-way classification
    • owner + user sponsor
    • owner only
    • user sponsor only
    • neither
  2. Group sponsor resolution
    • group count greater than zero
    • group count zero
    • repeated group ID is deduplicated
  3. Snapshot consistency
    • owner-only snapshot row is skipped
    • sponsor-only snapshot row is skipped
    • skippedCount is exact
  4. Failure handling
    • SQL exception publishes $null
    • batch exception publishes $null
    • unsuccessful batch member publishes $null
    • omitted batch result publishes $null
  5. Empty tenant
    • publishes a zero-valued distribution with empty agent arrays
  6. 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
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tdetzner
Thomas Detzner (tdetzner) merged commit f8cdec0 into astaykov/preview-report Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review PR is ready for review and merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants