Providers/PingOne/Public/Get-PingOneEnvironmentReport.ps1

function Get-PingOneEnvironmentReport {
    <#
    .SYNOPSIS
        Reports what this module has seeded in a PingOne environment, and what shape it is in

    .DESCRIPTION
        Reached through Get-TestEnvironmentReport once a PingOne connection is active.

        Only objects this module can prove it created are counted, using the same selection
        teardown uses, so the report and the teardown can never disagree about what is ours.

        Beyond counts, it surfaces the states the seed exists to create, because a report that
        says "330 users" and nothing else proves nothing about whether a script handles a
        disabled user who still holds memberships:

        - Users by population, enabled, and MFA state.
        - Groups by kind: static, dynamic, and scoped to a population, with direct member
          counts. A dynamic group's count is PingOne's own and lags behind reality until the
          filter evaluates, so a dynamic group reporting zero immediately after a seed is
          expected rather than broken.
        - Applications by protocol and type, with how many are enabled.

        Read-only. Nothing in the environment changes.

        Console output is for a person; JSON, CSV and HTML are for a file, written by the one
        writer every provider shares, as UTF-8. The report object is the shape every provider
        returns: Provider, Target, GeneratedOn, the environment's own facts, Counts, Sections,
        and one property per section.
    .PARAMETER OutputFormat
        Console, JSON, HTML or CSV.

    .PARAMETER OutputPath
        The file to write, or for CSV the folder. Required for anything but Console.

    .PARAMETER PassThru
        Returns the report object as well.

    .OUTPUTS
        PingOneEnvironmentReport, when -PassThru is supplied.

    .EXAMPLE
        PS> Get-TestEnvironmentReport

        DESCRIPTION: Summarises the seeded environment
        OUTPUT: Counts and state breakdowns per object type
        USE CASE: Confirming a seed produced what it should

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/

    .LINK
        New-PingOneEnvironment
        Remove-PingOneEnvironment
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'The summary is written for a person reading it; -PassThru returns the object for scripts.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Console', 'JSON', 'HTML', 'CSV')]
        [string]$OutputFormat = 'Console',

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [switch]$PassThru
    )

    $connection = Get-PingOneConnection
    if ($OutputFormat -ne 'Console' -and -not $OutputPath) {
        throw "-OutputPath is required for the $OutputFormat format."
    }

    $attributes = @(Get-PingOneSeededObject -Type Attributes -Connection $connection)
    $populations = @(Get-PingOneSeededObject -Type Populations -Connection $connection)
    $users = @(Get-PingOneSeededObject -Type Users -Connection $connection)
    $groups = @(Get-PingOneSeededObject -Type Groups -Connection $connection)
    $resources = @(Get-PingOneSeededObject -Type Resources -Connection $connection)
    $applications = @(Get-PingOneSeededObject -Type Applications -Connection $connection)

    $populationName = @{}
    foreach ($population in $populations) { $populationName[[string]$population.id] = $population.name }

    $usersByPopulation = [ordered]@{}
    foreach ($group in ($users | Group-Object { $populationName[[string]$_.population.id] } | Sort-Object Name)) {
        $label = if ($group.Name) { $group.Name } else { '(not in a seeded population)' }
        $usersByPopulation[$label] = $group.Count
    }

    $groupRows = foreach ($group in ($groups | Sort-Object name)) {
        $kind = if ($group.userFilter) { 'Dynamic' } elseif ($group.population) { 'Population-scoped' } else { 'Static' }
        [PSCustomObject]@{
            Name          = $group.name
            Kind          = $kind
            DirectUsers   = [int]$group.directMemberCounts.users
            DirectGroups  = [int]$group.directMemberCounts.groups
        }
    }

    $applicationRows = foreach ($application in ($applications | Sort-Object name)) {
        [PSCustomObject]@{
            Name     = $application.name
            Protocol = $application.protocol
            Type     = $application.type
            Enabled  = [bool]$application.enabled
        }
    }

    $populationRows = foreach ($population in ($populations | Sort-Object name)) {
        [PSCustomObject]@{
            Name        = $population.name
            Description = $population.description
            Users       = @($users | Where-Object { [string]$_.population.id -eq [string]$population.id }).Count
        }
    }
    $userRows = foreach ($user in ($users | Sort-Object username)) {
        [PSCustomObject]@{
            Username   = $user.username
            GivenName  = $user.name.given
            FamilyName = $user.name.family
            Population = $populationName[[string]$user.population.id]
            Enabled    = [bool]$user.enabled
            MfaEnabled = [bool]$user.mfaEnabled
        }
    }
    $resourceRows = foreach ($resource in ($resources | Sort-Object name)) {
        [PSCustomObject]@{ Name = $resource.name; Audience = $resource.audience; Type = $resource.type }
    }
    $attributeRows = foreach ($attribute in ($attributes | Sort-Object name)) {
        [PSCustomObject]@{ Name = $attribute.name; Type = $attribute.type; Required = [bool]$attribute.required; Unique = [bool]$attribute.unique }
    }

    $report = New-TestEnvironmentReport -Provider 'PingOne' -Target $connection.EnvironmentId -TypeName 'PingOneEnvironmentReport' `
        -Property ([ordered]@{
            EnvironmentId     = $connection.EnvironmentId
            EnvironmentName   = $connection.EnvironmentName
            Prefix            = $connection.Prefix
            UsersByPopulation = [PSCustomObject]$usersByPopulation
            UsersDisabled     = @($users | Where-Object { -not $_.enabled }).Count
            UsersWithMfa      = @($users | Where-Object { $_.mfaEnabled }).Count
        }) `
        -Section ([ordered]@{
            Attributes   = @($attributeRows)
            Populations  = @($populationRows)
            Users        = @($userRows)
            Groups       = @($groupRows)
            Resources    = @($resourceRows)
            Applications = @($applicationRows)
        })

    if ($OutputFormat -eq 'Console') {
        Write-TestMessage -Message ("PingOne Test Environment Report ({0})" -f $connection.EnvironmentName) -Type Header
        Write-Host 'Counts'
        foreach ($property in $report.Counts.PSObject.Properties) {
            Write-Host (" {0,-14} {1}" -f $property.Name, $property.Value)
        }
        Write-Host ''
        Write-Host 'Users by population'
        foreach ($property in $report.UsersByPopulation.PSObject.Properties) {
            Write-Host (" {0,-32} {1}" -f $property.Name, $property.Value)
        }
        Write-Host (" disabled {0}, with MFA {1}" -f $report.UsersDisabled, $report.UsersWithMfa)
        Write-Host ''
        Write-Host 'Groups'
        $report.Groups | Format-Table -AutoSize | Out-String -Width 120 | Write-Host
        Write-Host 'Applications'
        $report.Applications | Format-Table -AutoSize | Out-String -Width 120 | Write-Host
    }
    else {
        Export-TestEnvironmentReport -Report $report -OutputFormat $OutputFormat -OutputPath $OutputPath `
            -FilePrefix 'PingOneLab' -Title 'PingOne Test Environment Report' `
            -Note @("Environment $($connection.EnvironmentName) ($($connection.EnvironmentId))")
    }

    if ($PassThru) { return $report }
}