Templates/Plugins/Invoke-PluginPostRunOrphanReport.ps1

#PostRun Orphan Report Plugin — IDBridge plugin template
# TemplateVersion: 1
<#
Shipped with the IDBridge module and copied to <RootPath>\Plugins by Install-IDBridge.
Reports enabled directory accounts that carry a PersonID link (AD EmployeeID / Google
externalId of type "organization") but matched no source record this run. Without a source
record the pipeline never updates or deactivates an account, so these linger enabled and
invisible — typical causes: a deleted sheet row, a student who left before LastSeen state
tracking existed, or a person removed from the SIS.

No placeholders: enable its descriptor and it runs. When orphans are found it writes
OrphanReport-<timestamp>.csv to Paths.ExportsRoot and logs a Warn with the counts.
Report-only — it never touches the accounts; offboard them by hand or via a ForceDisable
override row.
#>



function Invoke-PluginPostRunOrphanReport {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [pscustomobject]$RunResult
    )

    #A failed run may have incomplete source data — everyone left would look orphaned
    if (-not $RunResult.Success) {
        Write-Log -Message "Orphan Report: Run failed, skipping report (source data may be incomplete)." -Level Trace
        return
    }

    #TestRun caps each source plugin at 10 records — everyone else would look orphaned
    if ($RunResult.TestRun) {
        Write-Log -Message "Orphan Report: TestRun caps source data, skipping report." -Level Trace
        return
    }

    #region Import Configuration
    try { $IDConfig = Get-IDBridgeConfig } catch { Throw $_ }
    #endregion Import Configuration

    #Build a lookup of every PersonID the run's source plugins produced
    $sourceIDs = @{}
    foreach ($id in $RunResult.SourceData.PersonID) {
        if ($id) { $sourceIDs["$id"] = $true }
    }

    if ($sourceIDs.Count -eq 0) {
        Write-Log -Message "Orphan Report: No source PersonIDs in the RunResult, skipping report." -Level Trace
        return
    }

    $orphans = @()

    #AD: enabled accounts with an EmployeeID that no source record claimed
    if ($RunResult.AD.Enabled) {
        foreach ($user in $RunResult.AD.CurrentUsers) {
            if ($user.Enabled -eq $true -and $user.EmployeeID -and -not $sourceIDs.ContainsKey("$($user.EmployeeID)")) {
                $orphans += [PSCustomObject]@{
                    Directory = 'AD'
                    PersonID  = $user.EmployeeID
                    Account   = $user.UserPrincipalName
                    Name      = $user.DisplayName
                    Location  = $user.DistinguishedName
                }
            }
        }
    }

    #Google: non-suspended accounts with an organization externalId that no source record claimed
    if ($RunResult.Google.Enabled) {
        foreach ($user in $RunResult.Google.CurrentUsers) {
            $pkID = ($user.externalIds | Where-Object { $_.type -eq 'organization' } | Select-Object -First 1).value
            if ($user.suspended -ne $true -and $pkID -and -not $sourceIDs.ContainsKey("$pkID")) {
                $orphans += [PSCustomObject]@{
                    Directory = 'Google'
                    PersonID  = $pkID
                    Account   = $user.primaryEmail
                    Name      = $user.name.fullName
                    Location  = $user.orgUnitPath
                }
            }
        }
    }

    if ($orphans.Count -gt 0) {
        $reportPath = "$($IDConfig.Paths.ExportsRoot)\OrphanReport-$(Get-Date -Format 'yyyy-MM-dd-HH.mm.ss').csv"
        $orphans | Export-Csv -Path $reportPath -NoTypeInformation -Force

        $adCount = @($orphans | Where-Object { $_.Directory -eq 'AD' }).Count
        $googleCount = @($orphans | Where-Object { $_.Directory -eq 'Google' }).Count
        Write-Log -Message "Orphan Report: $($orphans.Count) enabled account(s) with a PersonID link but no source record (AD: $adCount, Google: $googleCount). Report: $reportPath" -Level Warn
    } else {
        Write-Log -Message "Orphan Report: No orphaned accounts found." -Level Info
    }
}