MAD-Groups.ps1
|
<########################### Groups ############################> # Collecte et analyse des groupes Active Directory. # # Variables consommees (fournies par le contexte Get-MADReport) : # $LimitedView, $ShowSensitiveObjects, $MaxSearchGroups, $DefaultSGs # # Variables produites (utilisees par Dashboard, Resume, HTML MultiPage et OnePage) : # $table - groupes avec membres # $Groupsnomembers - groupes sans membres # $TOPGroupsTable - tableau recapitulatif # $SecurityCount - nb groupes de securite # $DistroCount - nb groupes de distribution # $CustomGroup - nb groupes personnalises (avec membres) # $Groupswithmemebrship - nb groupes avec membres # $Groupswithnomembership - nb groupes sans membres # $GroupsProtected - nb groupes proteges contre la suppression # $GroupsNotProtected - nb groupes non proteges # $totalgroups - nb total de groupes Write-Host "" Write-Host " #=======================================================================" -ForegroundColor DarkCyan Write-Host " # [GROUPES]" -ForegroundColor Cyan Write-Host " #=======================================================================" -ForegroundColor DarkCyan Write-Progress-Custom "Groupes" "Analyse des groupes AD" #Get groups and sort in alphabetical order #list only group with members, this can be interresed on big domain with a lot of groups, you can remove the where if you are in small company #I'm excluded the Exchange groups -ResultSetSize $MaxSearchGroups $SecurityCount = 0 $CustomGroup = 0 $DefaultGroup = 0 $Groupswithmemebrship = 0 $Groupswithnomembership = 0 $GroupsProtected = 0 $GroupsNotProtected = 0 $totalgroups = 0 $DistroCount = 0 # Filter groups based on LimitedView setting if ($LimitedView -and -not $ShowSensitiveObjects.IsPresent) { # Mode LimitedView : Exclure les groupes avec admincount et groupes systeme $Skipdefaultadmingroups = "(&(!(groupType:1.2.840.113556.1.4.803:=1))(!(&(objectCategory=group)(admincount=1)(iscriticalsystemobject=*))))" Write-Progress-Custom "Groupes" "Filtrage des groupes privilegies (mode LimitedView)" } else { # Mode complet : Recuperer TOUS les groupes y compris admin $Skipdefaultadmingroups = "(objectCategory=group)" Write-Progress-Custom "Groupes" "Recuperation de TOUS les groupes (mode complet)" } # ── B02 FIX : charger tous les groupes du domaine en un seul appel LDAP ──────── # On construit un HashSet<string> des DNs de groupes pour permettre une # recherche O(1) lors du traitement de chaque membre, sans aucun appel # Get-ADObject supplementaire. Write-Progress-Custom "Groupes" "Chargement du cache DN->type (batch unique)" $groupDNCache = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase ) Get-ADGroup -LDAPFilter "(objectCategory=group)" -ResultSetSize $null -Properties DistinguishedName | ForEach-Object { [void]$groupDNCache.Add($_.DistinguishedName) } # ──────────────────────────────────────────────────────────────────────────────── Get-ADGroup -LDAPFilter $Skipdefaultadmingroups -ResultSetSize $MaxSearchGroups -Properties Member,ManagedBy,info,created,ProtectedFromAccidentalDeletion | Where-Object { # En mode LimitedView, exclure aussi les groupes par defaut par nom if ($LimitedView -and -not $ShowSensitiveObjects.IsPresent) { $DefaultSGs -notcontains $_.Name } else { $true # Inclure tous les groupes } } | ForEach-Object { $totalgroups++ $OwnerDN = $null if (!$_.member) { $Groupswithnomembership++ # BUG12 FIX : incrementer les compteurs Security/Distribution meme pour les groupes sans membres if ($_.GroupCategory -eq "Security") { $SecurityCount++ } elseif ($_.GroupCategory -eq "Distribution") { $DistroCount++ } if ($($_.ManagedBy)) { $OwnerDN = ($_.ManagedBy -split (",") | Where-Object {$_ -like "CN=*"}) -replace ("CN=","") } $obj = [PSCustomObject]@{ 'Name' = $_.name 'Type' = $_.GroupCategory 'Managed By' = $OwnerDN 'Created' = ($_.created.ToString("yyyy/MM/dd")) 'Default AD Group' = ($DefaultSGs -contains $_.name) 'Protected from Deletion' = $_.ProtectedFromAccidentalDeletion } $Groupsnomembers.Add($obj) } else { $Groupswithmemebrship++ $Type = New-Object 'System.Collections.Generic.List[System.Object]' if ($_.GroupCategory -eq "Security") { $SecurityCount++ $Type = "Security Group" } elseif ($_.GroupCategory -eq "Distribution") { $DistroCount++ $Type = "Distribution Group" } if ($_.ProtectedFromAccidentalDeletion -eq $True) { $GroupsProtected++ } else { $GroupsNotProtected++ } $CustomGroup++ # Distinguer utilisateurs et groupes membres # B02 FIX : lookup O(1) dans le cache — zero appel reseau supplementaire $memberList = @() foreach ($memberDN in $_.member) { $memberName = ($memberDN -split ',')[0] -replace 'CN=','' if ($groupDNCache.Contains($memberDN)) { $memberList += "[G] $memberName" } else { $memberList += $memberName } } $users = $memberList -join ", " $OwnerDN = ($_.ManagedBy -split (",") | Where-Object {$_ -like "CN=*"}) -replace ("CN=","") $obj = [PSCustomObject]@{ 'Name' = $_.name 'Type' = $Type 'Members' = $users 'Managed By' = $OwnerDN 'Created' = ($_.created.ToString("yyyy/MM/dd")) 'Remark' = $_.info 'Protected from Deletion' = $_.ProtectedFromAccidentalDeletion } $Table.Add($obj) } } #TOP groups table $obj1 = [PSCustomObject]@{ 'Total Groups' = $totalgroups 'Groups with members' = $Groupswithmemebrship 'Security Groups' = $SecurityCount 'Distribution Groups' = $DistroCount } $TOPGroupsTable.Add($obj1) Write-Success "Groupes collectes" Write-Host " >> Total: $totalgroups | Securite: $SecurityCount | Distribution: $DistroCount | Vides: $Groupswithnomembership" -ForegroundColor DarkGray # ============================================================ # MEMBRES RECURSIFS DES GROUPES # ============================================================ $GroupMembersDetailTable = New-Object 'System.Collections.Generic.List[System.Object]' function Get-GroupMembersRecursive { param( [string]$GroupName, [string]$TopGroupName, [System.Collections.Generic.HashSet[string]]$Visited = $null ) if ($null -eq $Visited) { $Visited = [System.Collections.Generic.HashSet[string]]::new() } if (-not $Visited.Add($GroupName)) { return } try { $members = Get-ADGroupMember -Identity $GroupName -ErrorAction Stop foreach ($m in $members) { if ($m.objectClass -eq 'group') { Get-GroupMembersRecursive -GroupName $m.SamAccountName -TopGroupName $TopGroupName -Visited $Visited } elseif ($m.objectClass -eq 'user') { $already = $GroupMembersDetailTable | Where-Object { $_.SamAccountName -eq $m.SamAccountName -and $_.'Source Group' -eq $TopGroupName } if (-not $already) { try { $u = Get-ADUser -Identity $m.SamAccountName -Properties ` UserPrincipalName,Enabled,EmailAddress,Description, AccountExpirationDate,whencreated,LastLogonDate, PasswordLastSet,PasswordNeverExpires, ProtectedFromAccidentalDeletion,admincount, DistinguishedName,'msDS-UserPasswordExpiryTimeComputed' ` -ErrorAction Stop $daystoexpire = 0 try { $expiry = [datetime]::FromFileTime($u.'msDS-UserPasswordExpiryTimeComputed') if ($expiry -gt (Get-Date) -and $expiry.Year -lt 9999) { $daystoexpire = ($expiry - (Get-Date)).Days } } catch { } $obj = [PSCustomObject]@{ 'Source Group' = $TopGroupName 'Name' = $u.Name 'SamAccountName' = $u.SamAccountName 'UserPrincipalName' = $u.UserPrincipalName 'Enabled' = $u.Enabled 'Email Address' = $u.EmailAddress 'Description' = $u.description 'Account Expiration' = $u.AccountExpirationDate 'Created' = $u.whencreated 'Last Logon Date' = $u.LastLogonDate 'Password Last Set' = $u.PasswordLastSet 'Password Never Expired' = $u.PasswordNeverExpires 'Days Until password expired' = $daystoexpire 'Protected from Deletion' = $u.ProtectedFromAccidentalDeletion 'Privileged' = if ($u.admincount -eq 1) { 'Yes' } else { 'No' } 'OU' = (($u.DistinguishedName -split ',') | Where-Object {$_ -like 'OU=*'} | ForEach-Object {$_ -replace 'OU=',''}) -join ',' 'CN' = $u.DistinguishedName } $GroupMembersDetailTable.Add($obj) } catch { } } } } } catch { } } foreach ($grp in ($Table | Select-Object -ExpandProperty Name)) { Get-GroupMembersRecursive -GroupName $grp -TopGroupName $grp } |