Public/Export-NSPADGroupMembershipReport.ps1

function Export-NSPADGroupMembershipReport {
    <#
    .SYNOPSIS
        Writes one CSV per group (GivenName, Surname, UserPrincipalName, SamAccountName, Group) into
        -Path, moving the previous run's CSVs into an archive folder and deleting archived reports
        older than -RetentionDays. Returns the CSV files written.
    .DESCRIPTION
        A monthly "who is in these groups" report. File names are <yyyy-MM-dd>_<group>.csv. Members
        are users (nested groups expanded with -Recursive); computers and groups are left out.
        Sending the report on (email, ticket) is left to the caller. Supports -WhatIf.
    .PARAMETER Group
        Groups to report on.
    .PARAMETER Path
        Report folder (created when missing).
    .PARAMETER ArchiveFolderName
        Sub-folder of -Path that earlier reports are moved into. Default: PastReports.
    .PARAMETER RetentionDays
        Archived reports older than this are deleted. Default: 180.
    .PARAMETER Recursive
        Include users who are members through nested groups.
    .EXAMPLE
        Export-NSPADGroupMembershipReport -Group 'Group - All Employees', 'Group - On Call' -Path 'D:\Reports\Groups'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([IO.FileInfo])]
    param(
        [Parameter(Mandatory)][string[]]$Group,
        [Parameter(Mandatory)][string]$Path,
        [string]$ArchiveFolderName = 'PastReports',
        [ValidateRange(1, 3650)][int]$RetentionDays = 180,
        [switch]$Recursive
    )
    Assert-NSPADCommand Get-ADGroupMember -ModuleHint 'ActiveDirectory (RSAT)'
    $archive = Join-Path $Path $ArchiveFolderName
    if (-not $PSCmdlet.ShouldProcess($Path, "Write group membership reports for $($Group.Count) group(s)")) { return }
    foreach ($dir in $Path, $archive) { if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null } }

    # Last run's reports move to the archive; archived reports past retention are deleted.
    foreach ($old in @(Get-ChildItem -LiteralPath $Path -Filter '*.csv' -File)) { Move-Item -LiteralPath $old.FullName -Destination $archive -Force }
    $cutoff = (Get-Date).AddDays(-$RetentionDays)
    foreach ($stale in @(Get-ChildItem -LiteralPath $archive -Filter '*.csv' -File | Where-Object { $_.LastWriteTime -lt $cutoff })) { Remove-Item -LiteralPath $stale.FullName -Force }

    $stamp = Get-Date -Format 'yyyy-MM-dd'
    foreach ($g in $Group) {
        $rows = foreach ($m in @(Get-ADGroupMember -Identity $g -Recursive:$Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'user' })) {
            $u = Get-ADUser -Identity $m -Properties GivenName, Surname, UserPrincipalName -ErrorAction Stop
            [pscustomobject]@{ GivenName = $u.GivenName; Surname = $u.Surname; UserPrincipalName = $u.UserPrincipalName; SamAccountName = $u.SamAccountName; Group = $g }
        }
        $file = Join-Path $Path ("{0}_{1}.csv" -f $stamp, ($g -replace '[\\/:*?"<>|]', '_'))
        if ($rows) { $rows | Export-Csv -LiteralPath $file -NoTypeInformation -Encoding UTF8 } else { Set-Content -LiteralPath $file -Value '"GivenName","Surname","UserPrincipalName","SamAccountName","Group"' -Encoding UTF8 }
        Get-Item -LiteralPath $file
    }
}