@@ -49,44 +49,99 @@ $m365GroupCollection | sort-object "Site Name" | Export-CSV $OutPutView -Force -
4949# [ CLI for Microsoft 365] ( #tab/cli-m365-ps )
5050
5151``` powershell
52- $m365Status = m365 status
53- if ($m365Status -match "Logged Out") {
54- m365 login
52+ [CmdletBinding()]
53+ param(
54+ [Parameter(HelpMessage = "Directory path where the CSV report will be stored.")]
55+ [string]$OutputDirectory,
56+
57+ [Parameter(HelpMessage = "Optional custom file name (with or without .csv) for the owners-not-members report.")]
58+ [string]$ReportFileName
59+ )
60+
61+ begin {
62+ m365 login --ensure
63+
64+ if (-not $OutputDirectory) {
65+ $OutputDirectory = if ($MyInvocation.MyCommand.Path) {
66+ Split-Path -Path $MyInvocation.MyCommand.Path
67+ } else {
68+ (Get-Location).Path
69+ }
70+ }
71+
72+ if (-not (Test-Path -Path $OutputDirectory -PathType Container)) {
73+ New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
74+ }
75+
76+ if (-not $ReportFileName) {
77+ $ReportFileName = 'm365OwnersNotMembers-{0}.csv' -f (Get-Date -Format 'yyyyMMdd-HHmmss')
78+ } elseif (-not $ReportFileName.EndsWith('.csv')) {
79+ $ReportFileName = "$ReportFileName.csv"
80+ }
81+
82+ $script:ReportPath = Join-Path -Path $OutputDirectory -ChildPath $ReportFileName
83+ $script:ReportItems = [System.Collections.Generic.List[psobject]]::new()
84+ $script:Summary = [ordered]@{
85+ GroupsEvaluated = 0
86+ OwnersAdded = 0
87+ OwnersFailed = 0
88+ }
89+
90+ Write-Host "Starting owner membership audit..."
91+ Write-Host "Report will be saved to $ReportPath"
5592}
5693
57- $dateTime = (Get-Date).toString("dd-MM-yyyy")
58- $invocation = (Get-Variable MyInvocation).Value
59- $directorypath = Split-Path $invocation.MyCommand.Path
60- $fileName = "m365OwnersNotMembers-" + $dateTime + ".csv"
61- $OutPutView = $directorypath + "\" + $fileName
62- # Array to Hold Result - PSObjects
63- $m365GroupCollection = @()
64- #Write-host $"$ownerName not part of member in $siteUrl";
65- $m365Sites = m365 spo site list --query "[?Template == 'GROUP#0' && Template != 'RedirectSite#0'].{GroupId:GroupId, Url:Url, Title:Title}" --output json | ConvertFrom-Json
66- $m365Sites | ForEach-Object {
67- $groupId = $_.GroupId -replace "/Guid\((.*)\)/",'$1';
68- $siteUrl = $_.Url;
69- $siteName = $_.Title
70- #if owner is not part of m365 group member
71- (m365 entra m365group user list --role Owner --groupId $groupId --output json | ConvertFrom-Json) | foreach-object {
72- $owner = $_;
73- $ownerDisplayName = $owner.displayName
74- if (!(m365 entra m365group user list --role Member --groupId $groupId --query "[?displayName == '$ownerDisplayName']" --output json | ConvertFrom-Json)) {
75- $ExportVw = New-Object PSObject
76- $ExportVw | Add-Member -MemberType NoteProperty -name "Site Name" -value $siteName
77- $ExportVw | Add-Member -MemberType NoteProperty -name "Site URL" -value $siteUrl
78- $ExportVw | Add-Member -MemberType NoteProperty -name "Owner Name" -value $ownerDisplayName
79- $m365GroupCollection += $ExportVw
80- m365 entra m365group user add --role Owner --groupId $groupId --userName $owner.userPrincipalName
81- Write-host "$ownerDisplayName has been added as member in $siteUrl";
94+ process {
95+ $sites = m365 spo site list --query "[?Template == 'GROUP#0' && Template != 'RedirectSite#0'].{GroupId:GroupId, Url:Url, Title:Title}" --output json | ConvertFrom-Json
96+
97+ foreach ($site in $sites) {
98+ $Summary.GroupsEvaluated++
99+ Write-Host "Processing group '$($site.Title)' ($($site.Url))"
100+
101+ $groupId = $site.GroupId -replace "/Guid\((.*)\)/", '$1'
102+ $owners = m365 entra m365group user list --role Owner --groupId $groupId --output json | ConvertFrom-Json
103+
104+ foreach ($owner in $owners) {
105+ $ownerDisplayName = $owner.displayName
106+ $isMember = m365 entra m365group user list --role Member --groupId $groupId --query "[?displayName == '$ownerDisplayName']" --output json | ConvertFrom-Json
107+
108+ if (-not $isMember) {
109+ Write-Host " Owner '$ownerDisplayName' missing from members, attempting to add..."
110+
111+ $ReportItems.Add([pscustomobject]@{
112+ 'Site Name' = $site.Title
113+ 'Site URL' = $site.Url
114+ 'Owner Name' = $ownerDisplayName
115+ })
116+
117+ $addResult = m365 entra m365group user add --role Member --groupId $groupId --userName $owner.userPrincipalName --output json 2>&1
118+
119+ if ($LASTEXITCODE -ne 0) {
120+ Write-Warning "Failed to add $ownerDisplayName as member in $($site.Url). CLI returned: $addResult"
121+ $Summary.OwnersFailed++
122+ continue
123+ }
124+
125+ Write-Host " Added $ownerDisplayName as member in $($site.Url)"
126+ $Summary.OwnersAdded++
127+ } else {
128+ Write-Host " Owner '$ownerDisplayName' already a member; skipping"
129+ }
82130 }
83131 }
84132}
85- # Export the result array to CSV file
86- $m365GroupCollection | sort-object "Site Name" | Export-CSV $OutPutView -Force -NoTypeInformation
87133
88- #Disconnect SharePoint online connection
89- m365 logout
134+ end {
135+ if ($ReportItems.Count -gt 0) {
136+ $ReportItems | Sort-Object 'Site Name' | Export-Csv -Path $ReportPath -NoTypeInformation -Force
137+ Write-Host "Report exported to $ReportPath"
138+ } else {
139+ Write-Host "No discrepancies detected across the evaluated groups."
140+ }
141+
142+ Write-Host ("Summary: {0} groups checked, {1} owners added as members, {2} owners failed to add." -f $Summary.GroupsEvaluated, $Summary.OwnersAdded, $Summary.OwnersFailed)
143+ }
144+
90145```
91146
92147[ !INCLUDE [ More about CLI for Microsoft 365] ( ../../docfx/includes/MORE-CLIM365.md )]
@@ -103,6 +158,7 @@ Sample first appeared on [Ensuring Owners Are Members](https://reshmeeauckloo.co
103158| ----------------------------------------- |
104159| [ Reshmee Auckloo (Main author)] ( https://github.com/reshmee011 ) |
105160| [ Michał Kornet (CLI for M365 version)] ( https://github.com/mkm17 ) |
161+ | Adam Wójcik |
106162
107163
108164[ !INCLUDE [ DISCLAIMER] ( ../../docfx/includes/DISCLAIMER.md )]
0 commit comments