├── .gitignore ├── AdminRoles ├── Get-MgRoleReport.ps1 └── Get-MsolRoleReport.ps1 ├── Applications ├── Deprecated_Get-AADApplicationsSAML.ps1 ├── Get-MgApplicationsCredentials.ps1 └── Get-MgApplicationsSAML.ps1 ├── Audit ├── Audit-GuestsAccounts.ps1 ├── Audit-GuestsActions.ps1 └── Search-AdminRolesChanges.ps1 ├── AzureAD └── readme.md ├── Exchange ├── Get-ExchangeRoleReport.ps1 ├── Get-ExoProtocols.ps1 ├── Get-MailAutoForwarded.ps1 ├── Get-MailboxForwarding.ps1 ├── Set-ExoCalendarDefaultPermissions.ps1 └── Set-RemoteDomainWithExternalForward.ps1 ├── Get-Office365TenantID.ps1 ├── Intune └── CMTrace │ ├── CMTrace.exe │ ├── archives │ ├── CMTrace 5.0.9078.1000.exe │ └── CMTrace 5.00.7804.1000.exe │ └── readme.md ├── Licenses ├── Download-LicensesFriendlyNames.ps1 ├── Get-MsolSub.ps1 └── LicensesFriendlyName.csv ├── M365Groups-Teams └── Get-Microsoft365GroupsDetails.ps1 ├── MessageCenter ├── Get-M365MessageCenterMessages.ps1 └── New-EntraIDAppWithMessageCenterRead.ps1 ├── MicrosoftEntraID ├── Get-AzureADAppPermissions.ps1 ├── Get-DsregcmdStatus.ps1 └── Get-LapsEntraIDPassword.ps1 ├── Modules └── Install-Microsoft365PShellModules.ps1 ├── OneDrive └── Invoke-OneDriveKnownFoldersLinksFix.ps1 ├── Outlook ├── AutomaticMigrationNewOutlookGPO │ └── Create-GPODisableAutomaticNewOutlookMigration.ps1 └── AutomaticMigrationNewOutlookRegistry │ └── DisableNewOutlookAutomaticMigration.reg ├── Password ├── Get-MgPasswordPolicies.ps1 ├── Get-MgUserPasswordInfo.ps1 └── Get-MsolPasswordPolicies.ps1 ├── README.md ├── Search-EmailAddressInMicrosoftCloud.ps1 ├── Search-EmailAddressInMicrosoftCloudv2.ps1 └── SharePoint ├── Export-SPOAdmin.ps1 ├── Get-SPOSitesDetails.ps1 └── Get-SPOSitesExternalUsers.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /AdminRoles/Get-MgRoleReport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get-MgRoleReport.ps1 - Reports on Microsoft Entra ID (Azure AD) roles 4 | 5 | .DESCRIPTION 6 | By default, the report contains only the roles with members. 7 | To get all the role, included empty roles, add -IncludeEmptyRoles $true 8 | 9 | .OUTPUTS 10 | The report is output to an array contained all the audit logs found. 11 | To export in a csv, do Get-MgRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 12 | 13 | .EXAMPLE 14 | Get-MgRoleReport 15 | 16 | .EXAMPLE 17 | Get-MgRoleReport -IncludeEmptyRoles $true 18 | 19 | .EXAMPLE 20 | Get-MgRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 21 | 22 | .LINK 23 | https://itpro-tips.com/get-the-office-365-admin-roles-and-track-the-changes/ 24 | 25 | .NOTES 26 | Written by Bastien Perez (Clidsys.com - ITPro-Tips.com) 27 | For more Office 365/Microsoft 365 tips and news, check out ITPro-Tips.com. 28 | 29 | Version History: 30 | ## [1.7] - 2025-04-04 31 | ### Changed 32 | - Add scopes for `RoleManagement.Read.All` and `AuditLog.Read.All` permissions 33 | 34 | ## [1.6] - 2025-02-26 35 | ### Changed 36 | - Add `permissionsNeeded` variable 37 | - Add `onpremisesSyncEnabled` property for groups 38 | - Add all type objects in the cache array 39 | - Add `LastNonInteractiveSignInDateTime` property for users 40 | 41 | ## [1.5] - 2025-02-25 42 | ### Changed 43 | - Always return `true` or `false` for `onPremisesSyncEnabled` properties 44 | - Fix issues with `objectsCacheArray` that was not working 45 | - Sign-in activity tracking for service principals 46 | 47 | ### Plannned for next release 48 | - Switch to `Invoke-MgGraphRequest` instead of `Get-Mg*` CMDlets 49 | 50 | ## [1.4] - 2025-02-13 51 | ### Added 52 | - Sign-in activity tracking for users 53 | - Account enabled status. 54 | - On-premises sync enabled status. 55 | - Remove old parameters 56 | - Test if already connected to Microsoft Graph and with the right permissions 57 | 58 | ## [1.3] - 2024-05-15 59 | ### Changed 60 | - Changes not specified. 61 | 62 | ## [1.2] - 2024-03-13 63 | ### Changed 64 | - Changes not specified. 65 | 66 | ## [1.1] - 2023-12-01 67 | ### Changed 68 | - Changes not specified. 69 | 70 | ## [1.0] - 2023-10-19 71 | ### Initial Release 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 78 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 79 | DEALINGS IN THE SOFTWARE. 80 | 81 | #> 82 | function Get-MgRoleReport { 83 | [CmdletBinding()] 84 | param ( 85 | [Parameter(Mandatory = $false)] 86 | [boolean]$IncludePIMEligibleAssignments = $true, 87 | [Parameter(Mandatory = $false)] 88 | [switch]$ForceNewToken, 89 | # using with the Maester framework 90 | [Parameter(Mandatory = $false)] 91 | [switch]$MaesterMode 92 | ) 93 | 94 | $modules = @( 95 | 'Microsoft.Graph.Authentication' 96 | 'Microsoft.Graph.Identity.Governance' 97 | 'Microsoft.Graph.Users' 98 | 'Microsoft.Graph.Groups' 99 | 'Microsoft.Graph.Beta.Reports' 100 | ) 101 | 102 | foreach ($module in $modules) { 103 | try { 104 | Import-Module $module -ErrorAction Stop 105 | } 106 | catch { 107 | Write-Warning "First, install module $module" 108 | return 109 | } 110 | } 111 | 112 | $isConnected = $false 113 | 114 | $isConnected = $null -ne (Get-MgContext -ErrorAction SilentlyContinue) 115 | 116 | if ($ForceNewToken.IsPresent) { 117 | Write-Verbose 'Disconnecting from Microsoft Graph' 118 | $null = Disconnect-MgGraph -ErrorAction SilentlyContinue 119 | $isConnected = $false 120 | } 121 | 122 | $scopes = (Get-MgContext).Scopes 123 | 124 | # Audit.Log.Read.All for sign-in activity 125 | # RoleManagement.Read.All for role assignment (PIM eligible and permanent) 126 | # Directory.Read.All for user and group and service principal information 127 | $permissionsNeeded = 'Directory.Read.All', 'RoleManagement.Read.All', 'AuditLog.Read.All' 128 | foreach ($permission in $permissionsNeeded) { 129 | if ($scopes -notcontains $permission) { 130 | Write-Verbose "You need to have the $permission permission in the current token, disconnect to force getting a new token with the right permissions" 131 | } 132 | } 133 | 134 | if (-not $isConnected) { 135 | Write-Verbose "Connecting to Microsoft Graph. Scopes: $permissionsNeeded" 136 | $null = Connect-MgGraph -Scopes $permissionsNeeded -NoWelcome 137 | } 138 | 139 | try { 140 | #$mgRoles = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop 141 | 142 | $mgRoles = Get-MgRoleManagementDirectoryRoleAssignment -All -ExpandProperty Principal 143 | #$mgRoles = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments' -OutputType PSObject).Value 144 | 145 | 146 | # The maximum property is 1 so we need to do a second request to get the role definition 147 | $mgRolesDefinition = Get-MgRoleManagementDirectoryRoleAssignment -All -ExpandProperty roleDefinition 148 | #$mgRolesDefinition = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$expand=roleDefinition" -OutputType PSObject).Value 149 | } 150 | catch { 151 | Write-Warning $($_.Exception.Message) 152 | } 153 | 154 | foreach ($mgRole in $mgRoles) { 155 | # Add the role definition to the object 156 | Add-Member -InputObject $mgRole -MemberType NoteProperty -Name RoleDefinitionExtended -Value ($mgRolesDefinition | Where-Object { $_.id -eq $mgRole.id }).roleDefinition 157 | #Add-Member -InputObject $mgRole -MemberType NoteProperty -Name RoleDefinitionExtended -Value ($mgRolesDefinition | Where-Object { $_.id -eq $mgRole.id }).roleDefinition.description 158 | } 159 | 160 | if ($IncludePIMEligibleAssignments) { 161 | Write-Verbose 'Collecting PIM eligible role assignments...' 162 | try { 163 | $mgRoles += (Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty * -ErrorAction Stop | Select-Object id, principalId, directoryScopeId, roleDefinitionId, status, principal, @{Name = 'RoleDefinitionExtended'; Expression = { $_.roleDefinition } }) 164 | #$mgRoles += (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedule' -OutputType PSObject).Value 165 | } 166 | catch { 167 | Write-Warning "Unable to get PIM eligible role assignments: $($_.Exception.Message)" 168 | } 169 | } 170 | 171 | [System.Collections.Generic.List[PSObject]]$rolesMembers = @() 172 | 173 | foreach ($mgRole in $mgRoles) { 174 | $principal = switch ($mgRole.principal.AdditionalProperties.'@odata.type') { 175 | '#microsoft.graph.user' { $mgRole.principal.AdditionalProperties.userPrincipalName; break } 176 | '#microsoft.graph.servicePrincipal' { $mgRole.principal.AdditionalProperties.appId; break } 177 | '#microsoft.graph.group' { $mgRole.principalid; break } 178 | 'default' { '-' } 179 | } 180 | 181 | $object = [PSCustomObject][ordered]@{ 182 | Principal = $principal 183 | PrincipalDisplayName = $mgRole.principal.AdditionalProperties.displayName 184 | PrincipalType = $mgRole.principal.AdditionalProperties.'@odata.type'.Split('.')[-1] 185 | AssignedRole = $mgRole.RoleDefinitionExtended.displayName 186 | AssignedRoleScope = $mgRole.directoryScopeId 187 | AssignmentType = if ($mgRole.status -eq 'Provisioned') { 'Eligible' } else { 'Permanent' } 188 | RoleIsBuiltIn = $mgRole.RoleDefinitionExtended.isBuiltIn 189 | RoleTemplate = $mgRole.RoleDefinitionExtended.templateId 190 | DirectMember = $true 191 | Recommendations = 'Check if the user has alternate email or alternate phone number on Microsoft Entra ID' 192 | } 193 | 194 | $rolesMembers.Add($object) 195 | 196 | if ($object.PrincipalType -eq 'group') { 197 | # need to get ID for Get-MgGroupMember 198 | $group = Get-MgGroup -GroupId $object.Principal -Property Id, onPremisesSyncEnabled 199 | $object | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $([bool]($group.onPremisesSyncEnabled -eq $true)) 200 | 201 | #$group = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($object.Principal)" -OutputType PSObject) 202 | 203 | $groupMembers = Get-MgGroupMember -GroupId $group.Id -Property displayName, userPrincipalName 204 | #$groupMembers = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($group.Id)/members" -OutputType PSObject).Value 205 | 206 | foreach ($member in $groupMembers) { 207 | $typeMapping = @{ 208 | '#microsoft.graph.user' = 'user' 209 | '#microsoft.graph.group' = 'group' 210 | '#microsoft.graph.servicePrincipal' = 'servicePrincipal' 211 | '#microsoft.graph.device' = 'device' 212 | '#microsoft.graph.orgContact' = 'contact' 213 | '#microsoft.graph.application' = 'application' 214 | } 215 | 216 | $memberType = if ($typeMapping[$member.AdditionalProperties.'@odata.type']) { 217 | $typeMapping[$member.AdditionalProperties.'@odata.type'] 218 | } 219 | else { 220 | 'Unknown' 221 | } 222 | 223 | $object = [PSCustomObject][ordered]@{ 224 | Principal = $member.AdditionalProperties.userPrincipalName 225 | PrincipalDisplayName = $member.AdditionalProperties.displayName 226 | PrincipalType = $memberType 227 | AssignedRole = $mgRole.RoleDefinitionExtended.displayName 228 | AssignedRoleScope = $mgRole.directoryScopeId 229 | AssignmentType = if ($mgRole.status -eq 'Provisioned') { 'Eligible' } else { 'Permanent' } 230 | RoleIsBuiltIn = $mgRole.RoleDefinitionExtended.isBuiltIn 231 | RoleTemplate = $mgRole.RoleDefinitionExtended.templateId 232 | DirectMember = $false 233 | Recommendations = 'Check if the user has alternate email or alternate phone number on Microsoft Entra ID' 234 | } 235 | 236 | $rolesMembers.Add($object) 237 | } 238 | } 239 | } 240 | 241 | $object = [PSCustomObject] [ordered]@{ 242 | Principal = 'Partners' 243 | PrincipalDisplayName = 'Partners' 244 | PrincipalType = 'Partners' 245 | AssignedRole = 'Partners' 246 | AssignedRoleScope = 'Partners' 247 | AssignmentType = 'Partners' 248 | RoleIsBuiltIn = 'Not applicable' 249 | RoleTemplate = 'Not applicable' 250 | DirectMember = 'Not applicable' 251 | Recommendations = 'Please check this URL to identify if you have partner with admin roles https: / / admin.microsoft.com / AdminPortal / Home#/partners. More information on https://practical365.com/identifying-potential-unwanted-access-by-your-msp-csp-reseller/' 252 | } 253 | 254 | $rolesMembers.Add($object) 255 | 256 | #foreach user, we check if the user is global administrator. If global administrator, we add a new parameter to the object recommandationRole to tell the other role is not useful 257 | $globalAdminsHash = @{} 258 | $rolesMembers | Where-Object { $_.AssignedRole -eq 'Global Administrator' } | ForEach-Object { 259 | $globalAdminsHash[$_.Principal] = $true 260 | } 261 | 262 | $rolesMembers | ForEach-Object { 263 | if ($globalAdminsHash.ContainsKey($_.Principal) -and $_.AssignedRole -ne 'Global Administrator') { 264 | $_ | Add-Member -MemberType NoteProperty -Name 'RecommandationRole' -Value 'This user is Global Administrator. The other role(s) is/are not useful' 265 | } 266 | else { 267 | $_ | Add-Member -MemberType NoteProperty -Name 'RecommandationRole' -Value '' 268 | } 269 | } 270 | 271 | [System.Collections.Generic.List[Object]]$objectsCacheArray = @() 272 | 273 | foreach ($member in $rolesMembers) { 274 | 275 | $lastSignInDateTime = $null 276 | $accountEnabled = $null 277 | $onPremisesSyncEnabled = $null 278 | 279 | if ($objectsCacheArray.Principal -Contains $member.Principal) { 280 | $accountEnabled = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).AccountEnabled 281 | $lastSignInDateTime = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).LastSignInDateTime 282 | $lastNonInteractiveSignInDateTime = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).LastNonInteractiveSignInDateTime 283 | $onPremisesSyncEnabled = ($objectsCacheArray | Where-Object { $_.Principal -eq $member.Principal }).onPremisesSyncEnabled 284 | } 285 | else { 286 | $lastSignInActivity = $null 287 | 288 | switch ($member.PrincipalType) { 289 | 'user' { 290 | # If we use Get-MgUser -UserId $member.Principal -Property AccountEnabled, SignInActivity, onPremisesSyncEnabled, 291 | # we encounter the error 'Get-MgUser_Get: Get By Key only supports UserId, and the key must be a valid GUID'. 292 | # This is because the sign-in data comes from a different source that requires a GUID to retrieve the account's sign-in activity. 293 | # Therefore, we must provide the account's object identifier for the command to function correctly. 294 | # To overcome this issue, we use the -Filter parameter to search for the user by their UserPrincipalName. 295 | $mgUser = Get-MgUser -Filter "UserPrincipalName eq '$($member.Principal)'" -Property AccountEnabled, SignInActivity, onPremisesSyncEnabled 296 | $accountEnabled = $mgUser.AccountEnabled 297 | $lastSignInDateTime = $mgUser.signInActivity.LastSignInDateTime 298 | $lastNonInteractiveSignInDateTime = $mgUser.signInActivity.LastNonInteractiveSignInDateTime 299 | $onPremisesSyncEnabled = [bool]($mgUser.onPremisesSyncEnabled -eq $true) 300 | 301 | $member | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $onPremisesSyncEnabled 302 | 303 | break 304 | } 305 | 306 | 'group' { 307 | $accountEnabled = 'Not applicable' 308 | $lastSignInDateTime = 'Not applicable' 309 | $lastNonInteractiveSignInDateTime = 'Not applicable' 310 | # onpremisesSyncEnabled already get from Get-MgGroup in the previous loop 311 | 312 | break 313 | } 314 | 315 | 'servicePrincipal' { 316 | $lastSignInActivity = (Get-MgBetaReportServicePrincipalSignInActivity -Filter "appId eq '$($member.Principal)'").LastSignInActivity 317 | $accountEnabled = 'Not applicable' 318 | $lastSignInDateTime = $lastSignInActivity.LastSignInDateTime 319 | $lastNonInteractiveSignInDateTime = $lastSignInActivity.LastNonInteractiveSignInDateTime 320 | $onPremisesSyncEnabled = $false 321 | 322 | $member | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $onPremisesSyncEnabled 323 | 324 | break 325 | } 326 | 327 | 'Partners' { 328 | $accountEnabled = 'Not applicable' 329 | $lastSignInDateTime = 'Not applicable' 330 | $lastNonInteractiveSignInDateTime = 'Not applicable' 331 | $onPremisesSyncEnabled = 'Not applicable' 332 | 333 | $member | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $false 334 | 335 | break 336 | } 337 | 338 | 'default' { 339 | $accountEnabled = 'Not applicable' 340 | $lastSignInDateTime = 'Not applicable' 341 | $lastNonInteractiveSignInDateTime = 'Not applicable' 342 | $onPremisesSyncEnabled = 'Not applicable' 343 | 344 | $member | Add-Member -MemberType NoteProperty -Name 'OnPremisesSyncEnabled' -Value $onPremisesSyncEnabled 345 | } 346 | } 347 | } 348 | 349 | $member | Add-Member -MemberType NoteProperty -Name 'LastSignInDateTime' -Value $lastSignInDateTime 350 | $member | Add-Member -MemberType NoteProperty -Name 'LastNonInteractiveSignInDateTime' -Value $lastNonInteractiveSignInDateTime 351 | $member | Add-Member -MemberType NoteProperty -Name 'AccountEnabled' -Value $accountEnabled 352 | 353 | # only add if not already in the cache 354 | if (-not $objectsCacheArray.Principal -Contains $member.Principal) { 355 | $objectsCacheArray.Add($member) 356 | } 357 | } 358 | 359 | return $rolesMembers 360 | } -------------------------------------------------------------------------------- /AdminRoles/Get-MsolRoleReport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get-MsolRoleReport.ps1 - Reports on Office 365 Admin Role 4 | 5 | .DESCRIPTION 6 | This script produces a report of the membership of Office 365 admin role groups. 7 | By default, the report contains only the groups with members. 8 | To get all the role, included empty roles, add -IncludeEmptyRoles $true 9 | 10 | .OUTPUTS 11 | The report is output to an array contained all the audit logs found. 12 | To export in a csv, do Get-MsolRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 13 | 14 | .EXAMPLE 15 | Get-MsolRoleReport 16 | 17 | .EXAMPLE 18 | Get-MsolRoleReport -IncludeEmptyRoles $true 19 | 20 | .EXAMPLE 21 | Get-MsolRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 22 | 23 | .LINK 24 | https://itpro-tips.com/2020/get-the-office-365-admin-roles-and-track-the-changes/ 25 | https://github.com/itpro-tips/Microsoft365-Toolbox/blob/master/AdminRoles/Get-MsolRoleReport.ps1 26 | 27 | .NOTES 28 | Written by Bastien Perez (Clidsys.com - ITPro-Tips.com) 29 | For more Office 365/Microsoft 365 tips and news, check out ITPro-Tips.com. 30 | 31 | Version history: 32 | V1.0, 17 august 2020 - Initial version 33 | V1.1, 05 april 2022 - Add alternate email, Phone number, PIN 34 | V1.2, 27 april april 2022 - Add GroupNameUsedInConditionnalAccess to check if user is member of group used in conditionnal access 35 | V1.3, 2 may 2022 - Add Partners links 36 | v1.4 october 2022 37 | 38 | /!\ 39 | /!\ 40 | /!\ 41 | CAREFUL, THIS SCRIPT CAN STOP WORKING AFTER DECEMBER 2022 DUE TO MICROSOFT DELETION OF MSOL AND AZURE AD MODULES 42 | https://office365itpros.com/2022/03/17/azure-ad-powershell-deprecation/ 43 | /!\ 44 | /!\ 45 | /!\ 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 52 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 53 | DEALINGS IN THE SOFTWARE. 54 | 55 | #> 56 | function Get-MsolRoleReport { 57 | [CmdletBinding()] 58 | param ( 59 | [boolean]$IncludeEmptyRoles, 60 | [String]$GroupNameUsedInConditionnalAccess 61 | ) 62 | 63 | try { 64 | Import-Module MSOnline -ErrorAction stop 65 | } 66 | catch { 67 | Write-Warning 'First, install the official Microsoft MSOnline module : Install-Module MSOnline' 68 | return 69 | } 70 | 71 | try { 72 | $msolRoles = Get-MsolRole -ErrorAction Stop 73 | } 74 | catch { 75 | Connect-MsolService 76 | $msolRoles = Get-MsolRole 77 | } 78 | 79 | Write-Warning "Warning 1: This script script may no longer work after June 2023 due to Microsoft deprecation of MSOL and Azure AD modules. More information on https://office365itpros.com/2023/04/04/azure-ad-powershell-deprecation-2/" Write-Warning "Warning 2: This script use MSOLService so it doesn't return assingment via PIM (Privileged Identity Management). The results can be not accurate. Prefer using https://github.com/itpro-tips/Microsoft365-Toolbox/blob/master/AdminRoles/Get-MgRoleReport.ps1" 80 | 81 | # Use MsolService because returns more role and allows MFA status 82 | [System.Collections.Generic.List[PSObject]]$rolesMembers = @() 83 | 84 | foreach ($msolRole in $msolRoles) { 85 | 86 | # Global administrator is called Company administrator in Microsoft Graph API and Azure AD PowerShell https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-assign-admin-roles#global-administrator--company-administrator 87 | # Other roles also have another name, but the name is understable 88 | switch ($msolRole.Name) { 89 | 'Company Administrator' { 90 | $msolRole.Name = 'Company Administrator/Global administrator' 91 | break 92 | } 93 | } 94 | 95 | try { 96 | 97 | $roleMembers = @(Get-MsolRoleMember -RoleObjectId $msolRole.ObjectId) 98 | 99 | # Add green color if member found into the role 100 | if ($roleMembers.count -gt 0) { 101 | Write-Host -ForegroundColor Green "Role $($msolRole.Name) - Member(s) found: $($roleMembers.count)" 102 | } 103 | else { 104 | Write-Host -ForegroundColor Cyan "Role $($msolRole.Name) - Member found: $($roleMembers.count)" 105 | } 106 | 107 | if ($IncludeEmptyRoles -and $roleMembers.count -eq 0) { 108 | $object = [PSCustomObject] [ordered]@{ 109 | 'Role' = $msolRole.Name 110 | 'RoleDescription' = $msolRole.Description 111 | 'MemberDisplayName' = '-' 112 | 'MemberUserPrincipalName' = '-' 113 | 'MemberEmail' = '-' 114 | 'RoleMemberType' = '-' 115 | 'MemberAccountEnabled' = '-' 116 | 'MemberLastDirSyncTime' = '-' 117 | 'MemberMFAState(IgnoreIfConditionalAccessIsUsed)' = '-' 118 | 'MemberStrongAuthNDefaultMethodType' = '-' 119 | 'MemberObjectID' = '-' 120 | 'MemberAlternateEmailAddresses' = '-' 121 | 'MemberStrongAuthNEmail' = '-' 122 | 'MemberStrongAuthNPin' = '-' 123 | 'MemberStrongAuthNOldPin' = '-' 124 | 'MemberStrongAuthNPhoneNumber' = '-' 125 | 'MemberStrongAuthNAlternativePhoneNumber' = '-' 126 | 'Recommendations' = '-' 127 | } 128 | 129 | $rolesMembers.Add($object) 130 | 131 | } 132 | else { 133 | foreach ($roleMember in $roleMembers) { 134 | # if user already exist in the arraylist, we look for to prevent a new Get-MsolUser (time consuming) 135 | # Select only the first if user already exists in multiple roles 136 | if ($rolesMembers.MemberObjectID -contains $roleMember.ObjectID) { 137 | $found = $rolesMembers | Where-Object { $_.MemberObjectID -eq $roleMember.ObjectID } | Select-Object -First 1 138 | $object = [PSCustomObject][ordered]@{ 139 | 'Role' = $msolRole.Name 140 | 'RoleDescription' = $msolRole.Description 141 | 'MemberDisplayName' = $found.MemberDisplayName 142 | 'MemberUserPrincipalName' = $found.MemberUserPrincipalName 143 | 'MemberEmail' = $found.MemberEmail 144 | 'RoleMemberType' = $found.RoleMemberType 145 | 'MemberAccountEnabled' = $found.MemberAccountEnabled 146 | 'MemberLastDirSyncTime' = $found.MemberLastDirSyncTime 147 | 'MemberMFAState(IgnoreIfConditionalAccessIsUsed)' = $found.'MemberMFAState(IgnoreIfConditionalAccessIsUsed)' 148 | 'MemberStrongAuthNDefaultMethodType' = $found.MemberStrongAuthNDefaultMethodType 149 | 'MemberObjectID' = $found.MemberObjectID 150 | 'MemberAlternateEmailAddresses' = $found.MemberAlternateEmailAddresses 151 | 'MemberStrongAuthNEmail' = $found.MemberStrongAuthNEmail 152 | 'MemberStrongAuthNPin' = $found.MemberStrongAuthNPin 153 | 'MemberStrongAuthNOldPin' = $found.MemberStrongAuthNOldPin 154 | 'MemberStrongAuthNPhoneNumber' = $found.MemberStrongAuthNPhoneNumber 155 | 'MemberStrongAuthNAlternativePhoneNumber' = $found.MemberStrongAuthNAlternativePhoneNumber 156 | 'Recommendations' = $found.Recommendations 157 | } 158 | } 159 | else { 160 | if ($roleMember.RoleMemberType -eq 'ServicePrincipal') { 161 | $member = Get-MsolServicePrincipal -SearchString $roleMember.DisplayName 162 | } 163 | # Sometimes, user is service account, not present in Office 365. We set ErrorAction SilentlyContinue to prevent error. not handle non user type 164 | else { 165 | $member = Get-MsolUser -ObjectId $roleMember.ObjectID -ErrorAction SilentlyContinue 166 | } 167 | 168 | $MFAState = $member.StrongAuthenticationRequirements.State 169 | 170 | if ($null -eq $MFA) { 171 | $MFAState = 'Disabled' 172 | } 173 | 174 | if ($null -eq $member.LastDirSyncTime) { 175 | $lastDirSyncTime = 'Not a synchronized user' 176 | } 177 | else { 178 | $lastDirSyncTime = $member.LastDirSyncTime 179 | } 180 | 181 | $object = [PSCustomObject] [ordered]@{ 182 | 'Role' = $msolRole.Name 183 | 'RoleDescription' = $msolRole.Description 184 | 'MemberDisplayName' = $roleMember.DisplayName 185 | 'MemberUserPrincipalName' = $member.UserPrincipalName 186 | 'MemberEmail' = $roleMember.EmailAddress 187 | 'RoleMemberType' = $roleMember.RoleMemberType 188 | 'MemberAccountEnabled' = -not $member.AccountEnabled # BlockCredential is the opposite 189 | 'MemberLastDirSyncTime' = $lastDirSyncTime 190 | 'MemberMFAState(IgnoreIfConditionalAccessIsUsed)' = $MFAState 191 | 'MemberStrongAuthNDefaultMethodType' = if ($null -eq ($member.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq $true }).MethodType) { '-' } else { ($member.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq $true }).MethodType } 192 | 'MemberObjectID' = $member.ObjectId 193 | 'MemberAlternateEmailAddresses' = if (($member.AlternateEmailAddresses.count -eq 0)) { '-' } else { $member.AlternateEmailAddresses -join '|' } 194 | 'MemberStrongAuthNEmail' = if ($null -eq $member.StrongAuthenticationUserDetails.Email) { '-' } else { $member.StrongAuthenticationUserDetails.Email } 195 | 'MemberStrongAuthNPin' = if ($null -eq $member.StrongAuthenticationUserDetails.Pin) { '-' } else { $member.StrongAuthenticationUserDetails.Pin } 196 | 'MemberStrongAuthNOldPin' = if ($null -eq $member.StrongAuthenticationUserDetails.OldPin) { '-' } else { $member.StrongAuthenticationUserDetails.OldPin } 197 | 'MemberStrongAuthNPhoneNumber' = if ($null -eq $member.StrongAuthenticationUserDetails.PhoneNumber) { '-' } else { $member.StrongAuthenticationUserDetails.PhoneNumber } 198 | 'MemberStrongAuthNAlternativePhoneNumber' = if ($null -eq $member.StrongAuthenticationUserDetails.AlternativePhoneNumber) { '-' } else { $member.StrongAuthenticationUserDetails.AlternativePhoneNumber } 199 | 'Recommendations' = '' 200 | } 201 | 202 | $recommendationsString = $null 203 | 204 | if ($object.MemberAlternateEmailAddresses -ne '-') { 205 | $recommendationsString = "alternate email address (user profile in Azure AD portal) = $($object.MemberAlternateEmailAddresses);" 206 | } 207 | 208 | if ($object.MemberStrongAuthNEmail -ne '-') { 209 | $recommendationsString += "authentication email (Authentication Methods in Azure AD portal) = $($object.MemberStrongAuthNEmail);" 210 | } 211 | 212 | if ($object.MemberStrongAuthNPhoneNumber -ne '-') { 213 | $recommendationsString += "phone number = $($object.MemberStrongAuthNPhoneNumber);" 214 | } 215 | 216 | if ($object.MemberStrongAuthNAlternativePhoneNumber -ne '-') { 217 | $recommendationsString += "alternate phone number = $($object.MemberStrongAuthNAlternativePhoneNumber);" 218 | } 219 | 220 | if ($null -ne $recommendationsString) { 221 | $object.Recommendations = "Be careful about this admin user. If someone access the following phone(s)/email(s), he can reset the user password: $recommendationsString" 222 | } 223 | } 224 | 225 | $rolesMembers.Add($object) 226 | } 227 | } 228 | } 229 | catch { 230 | Write-Warning $_.Exception.Message 231 | } 232 | 233 | } 234 | 235 | $object = [PSCustomObject] [ordered]@{ 236 | 'Role' = 'Partners' 237 | 'RoleDescription' = 'Partners' 238 | 'MemberDisplayName' = 'Partners' 239 | 'MemberUserPrincipalName' = 'Partners' 240 | 'MemberEmail' = 'Partners' 241 | 'RoleMemberType' = 'Partners' 242 | 'MemberAccountEnabled' = 'Partners' 243 | 'MemberLastDirSyncTime' = 'Partners' 244 | 'MemberMFAState(IgnoreIfConditionalAccessIsUsed)' = 'Partners' 245 | 'MemberStrongAuthNDefaultMethodType' = 'Partners' 246 | 'MemberObjectID' = 'Partners' 247 | 'MemberAlternateEmailAddresses' = 'Partners' 248 | 'MemberStrongAuthNEmail' = 'Partners' 249 | 'MemberStrongAuthNPin' = 'Partners' 250 | 'MemberStrongAuthNOldPin' = 'Partners' 251 | 'MemberStrongAuthNPhoneNumber' = 'Partners' 252 | 'MemberStrongAuthNAlternativePhoneNumber' = 'Partners' 253 | 'Recommendations' = 'Please check this URL to identify if you have partner with admin roles https://admin.microsoft.com/AdminPortal/Home#/partners. More information on https://practical365.com/identifying-potential-unwanted-access-by-your-msp-csp-reseller/' 254 | } 255 | 256 | $rolesMembers.Add($object) 257 | 258 | if ($GroupNameUsedInConditionnalAccess) { 259 | 260 | $tempRolesMembers = New-Object 'System.Collections.Generic.List[System.Object]' 261 | 262 | $msolGroup = Get-MsolGroup -SearchString $GroupNameUsedInConditionnalAccess 263 | $msolGroupMembers = Get-MsolGroupMember -GroupObjectId $msolGroup.ObjectID 264 | 265 | foreach ($roleMember in $rolesMembers) { 266 | $isMember = $false 267 | 268 | if ($msolGroupMembers.ObjectId -contains $roleMember.MemberObjectID) { 269 | $isMember = $true 270 | } 271 | 272 | $roleMember | Add-Member -MemberType NoteProperty -Name MemberOfGroupUsedByConditionnalAccess -Value $isMember 273 | 274 | $tempRolesMembers.Add($roleMember) 275 | } 276 | 277 | $rolesMembers = $tempRolesMembers 278 | 279 | } 280 | 281 | return $rolesMembers 282 | } -------------------------------------------------------------------------------- /Applications/Deprecated_Get-AADApplicationsSAML.ps1: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | [System.Collections.Generic.List[PSObject]]$samlApplicationsArray = @() 4 | $samlApplications = Get-AzureADServicePrincipal -All $true | Where-Object {($_.Tags -contains 'WindowsAzureActiveDirectoryGalleryApplicationNonPrimaryV1') -or ($_.Tags -contains 'WindowsAzureActiveDirectoryCustomSingleSignOnApplication')} 5 | 6 | foreach ($samlApp in $samlApplications) { 7 | $object = [PSCustomObject][ordered]@{ 8 | DisplayName = $samlApp.DisplayName 9 | Id = $samlApp.ObjectId 10 | AppId = $samlApp.AppId 11 | LoginUrl = $samlApp.LoginUrl 12 | LogoutUrl = $samlApp.LogoutUrl 13 | NotificationEmailAddresses = $samlApp.NotificationEmailAddresses -join '|' 14 | AppRoleAssignmentRequired = '' 15 | PreferredSingleSignOnMode = '' 16 | PreferredTokenSigningKeyEndDateTime = '' 17 | # PreferredTokenSigningKeyEndDateTime is date time, compared to now and see it is valid 18 | PreferredTokenSigningKeyValid = '' 19 | PreferredTokenSigningKeyThumbprint = $samlApp.PreferredTokenSigningKeyThumbprint 20 | ReplyUrls = $samlApp.ReplyUrls -join '|' 21 | SignInAudience = $samlApp.SignInAudience 22 | } 23 | 24 | $samlApplicationsArray.Add($object) 25 | } 26 | 27 | return $samlApplicationsArray -------------------------------------------------------------------------------- /Applications/Get-MgApplicationsCredentials.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Version History: 3 | 4 | ## [1.2] - 2025-04-04 5 | ### Changed 6 | - Format output for Owners property 7 | 8 | ## [1.1] - 2025-02-26 9 | ### Changed 10 | - Transform the script into a function 11 | - Add `ForceNewToken` parameter 12 | - Test if already connected to Microsoft Graph and with the right permissions 13 | 14 | ## [1.0] - 2024-xx-xx 15 | ### Initial Release 16 | #> 17 | 18 | function Get-MgApplicationsCredentials { 19 | [CmdletBinding()] 20 | param ( 21 | [Parameter(Mandatory = $false)] 22 | [switch]$ForceNewToken 23 | ) 24 | 25 | try { 26 | # for Get-MgApplication/Get-MgApplicationOwner 27 | Import-Module 'Microsoft.Graph.Applications' -ErrorAction Stop -ErrorVariable mgGraphAppsMissing 28 | } 29 | catch { 30 | if ($mgGraphAppsMissing) { 31 | Write-Warning "Failed to import Microsoft.Graph.Applications module: $($mgGraphAppsMissing.Exception.Message)" 32 | } 33 | if ($mgGraphIdentitySignInsMissing) { 34 | Write-Warning "Failed to import Microsoft.Graph.Identity.SignIns module: $($mgGraphIdentitySignInsMissing.Exception.Message)" 35 | } 36 | return 37 | } 38 | 39 | $isConnected = $false 40 | 41 | $isConnected = $null -ne (Get-MgContext -ErrorAction SilentlyContinue) 42 | 43 | if ($ForceNewToken.IsPresent) { 44 | Write-Verbose 'Disconnecting from Microsoft Graph' 45 | $null = Disconnect-MgGraph -ErrorAction SilentlyContinue 46 | $isConnected = $false 47 | } 48 | 49 | $scopes = (Get-MgContext).Scopes 50 | 51 | $permissionsNeeded = 'Application.Read.All' 52 | $permissionMissing = $permissionsNeeded -notin $scopes 53 | 54 | if ($permissionMissing) { 55 | Write-Verbose "You need to have the $permissionsNeeded permission in the current token, disconnect to force getting a new token with the right permissions" 56 | } 57 | 58 | if (-not $isConnected) { 59 | Write-Verbose "Connecting to Microsoft Graph. Scopes: $permissionsNeeded" 60 | $null = Connect-MgGraph -Scopes $permissionsNeeded -NoWelcome 61 | } 62 | 63 | [System.Collections.Generic.List[PSObject]]$credentialsArray = @() 64 | 65 | $mgApps = Get-MgApplication -All 66 | 67 | foreach ($mgApp in $mgApps) { 68 | $owner = Get-MgApplicationOwner -ApplicationId $mgApp.Id 69 | 70 | # if severral owners, join them with '|' 71 | 72 | foreach ($keyCredential in $mgApp.KeyCredentials) { 73 | $object = [PSCustomObject][ordered]@{ 74 | DisplayName = $mgApp.DisplayName 75 | CredentialType = 'KeyCredentials' 76 | AppId = $mgApp.AppId 77 | CredentialDescription = $keyCredential.DisplayName 78 | CredentialStartDate = $keyCredential.StartDateTime 79 | CredentialExpiryDate = $keyCredential.EndDateTime 80 | # CredentialExpiryDate is date time, compared to now and see it is valid 81 | CredentialValid = $keyCredential.EndDateTime -gt (Get-Date) 82 | Type = $keyCredential.Type 83 | Usage = $keyCredential.Usage 84 | Owners = $owner.AdditionalProperties.userPrincipalName -join '|' 85 | } 86 | 87 | $credentialsArray.Add($object) 88 | } 89 | 90 | foreach ($passwordCredential in $mgApp.PasswordCredentials) { 91 | $object = [PSCustomObject][ordered]@{ 92 | DisplayName = $mgApp.DisplayName 93 | CredentialType = 'PasswordCredentials' 94 | AppId = $mgApp.AppId 95 | CredentialDescription = $passwordCredential.DisplayName 96 | CredentialStartDate = $passwordCredential.StartDateTime 97 | CredentialExpiryDate = $passwordCredential.EndDateTime 98 | # CredentialExpiryDate is date time, compared to now and see it is valid 99 | CredentialValid = $passwordCredential.EndDateTime -gt (Get-Date) 100 | Type = 'NA' 101 | Usage = 'NA' 102 | Owners = $owner.AdditionalProperties.userPrincipalName -join '|' 103 | } 104 | 105 | $credentialsArray.Add($object) 106 | } 107 | } 108 | 109 | return $credentialsArray 110 | } -------------------------------------------------------------------------------- /Applications/Get-MgApplicationsSAML.ps1: -------------------------------------------------------------------------------- 1 | # article : https://itpro-tips.com/get-azure-ad-saml-certificate-details/ 2 | # the information about the SAML applications clams is not available in the Microsoft Graph API v1 but in https://main.iam.ad.ext.azure.com/api/ApplicationSso//FederatedSsoV2 so we don't get them 3 | <# 4 | Version History: 5 | 6 | ## [1.2] - 2025-04-04 7 | ### Changed 8 | - Change Write-Warning message in the catch block to Import-Module 9 | 10 | ## [1.1] - 2025-02-26 11 | ### Changed 12 | - Transform the script into a function 13 | - Add `ForceNewToken` parameter 14 | - Test if already connected to Microsoft Graph and with the right permissions 15 | 16 | ## [1.0] - 2024-xx-xx 17 | ### Initial Release 18 | #> 19 | 20 | function Get-MgApplicationsSAML { 21 | [CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory = $false)] 24 | [switch]$ForceNewToken 25 | ) 26 | 27 | try { 28 | # At the date of writing (december 2023), PreferredTokenSigningKeyEndDateTime parameter is only on Beta profile 29 | Import-Module 'Microsoft.Graph.Beta.Applications' -ErrorAction Stop -ErrorVariable mgGraphAppsMissing 30 | } 31 | catch { 32 | if ($mgGraphAppsMissing) { 33 | Write-Warning "Please install the Microsoft.Graph.Beta.Applications module: $($mgGraphAppsMissing.Exception.Message)" 34 | } 35 | 36 | return 37 | } 38 | 39 | $isConnected = $false 40 | 41 | $isConnected = $null -ne (Get-MgContext -ErrorAction SilentlyContinue) 42 | 43 | if ($ForceNewToken.IsPresent) { 44 | Write-Verbose 'Disconnecting from Microsoft Graph' 45 | $null = Disconnect-MgGraph -ErrorAction SilentlyContinue 46 | $isConnected = $false 47 | } 48 | 49 | $scopes = (Get-MgContext).Scopes 50 | 51 | $permissionsNeeded = 'Application.Read.All' 52 | $permissionMissing = $permissionsNeeded -notin $scopes 53 | 54 | if ($permissionMissing) { 55 | Write-Verbose "You need to have the $permissionsNeeded permission in the current token, disconnect to force getting a new token with the right permissions" 56 | } 57 | 58 | if (-not $isConnected) { 59 | Write-Verbose "Connecting to Microsoft Graph. Scopes: $permissionsNeeded" 60 | $null = Connect-MgGraph -Scopes $permissionsNeeded -NoWelcome 61 | } 62 | 63 | [System.Collections.Generic.List[PSObject]]$samlApplicationsArray = @() 64 | $samlApplications = Get-MgBetaServicePrincipal -Filter "PreferredSingleSignOnMode eq 'saml'" 65 | 66 | foreach ($samlApp in $samlApplications) { 67 | $object = [PSCustomObject][ordered]@{ 68 | DisplayName = $samlApp.DisplayName 69 | Id = $samlApp.Id 70 | AppId = $samlApp.AppId 71 | LoginUrl = $samlApp.LoginUrl 72 | LogoutUrl = $samlApp.LogoutUrl 73 | NotificationEmailAddresses = $samlApp.NotificationEmailAddresses -join '|' 74 | AppRoleAssignmentRequired = $samlApp.AppRoleAssignmentRequired 75 | PreferredSingleSignOnMode = $samlApp.PreferredSingleSignOnMode 76 | PreferredTokenSigningKeyEndDateTime = $samlApp.PreferredTokenSigningKeyEndDateTime 77 | # PreferredTokenSigningKeyEndDateTime is date time, compared to now and see it is valid 78 | PreferredTokenSigningKeyValid = $samlApp.PreferredTokenSigningKeyEndDateTime -gt (Get-Date) 79 | ReplyUrls = $samlApp.ReplyUrls -join '|' 80 | SignInAudience = $samlApp.SignInAudience 81 | } 82 | 83 | $samlApplicationsArray.Add($object) 84 | } 85 | 86 | return $samlApplicationsArray 87 | } -------------------------------------------------------------------------------- /Audit/Audit-GuestsAccounts.ps1: -------------------------------------------------------------------------------- 1 | #https://petri.com/knowing-guest-accounts-office-365 2 | $EndDate = (Get-Date).AddDays(1); $StartDate = (Get-Date).AddDays(-10); 3 | $Records = (Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "Add Member to Group" -ResultSize 2000 -Formatted) 4 | If ($Records.Count -eq 0) { 5 | Write-Host "No Group Add Member records found." } 6 | Else { 7 | Write-Host "Processing" $Records.Count "audit records..." 8 | $Report = [System.Collections.Generic.List[Object]]::new() 9 | ForEach ($Rec in $Records) { 10 | $AuditData = ConvertFrom-Json $Rec.Auditdata 11 | # Only process the additions of guest users to groups 12 | If ($AuditData.ObjectId -Like "*#EXT#*") { 13 | $TimeStamp = Get-Date $Rec.CreationDate -format g 14 | # Try and find the timestamp when the Guest account was created in AAD 15 | Try {$AADCheck = (Get-Date(Get-AzureADUser -ObjectId $AuditData.ObjectId).RefreshTokensValidFromDateTime -format g) } 16 | Catch {Write-Host "Azure Active Directory record for" $AuditData.ObjectId "no longer exists" } 17 | If ($TimeStamp -eq $AADCheck) { # It's a new record, so let's write it out 18 | $NewGuests++ 19 | $ReportLine = [PSCustomObject]@{ 20 | TimeStamp = $TimeStamp 21 | User = $AuditData.UserId 22 | Action = $AuditData.Operation 23 | GroupName = $AuditData.modifiedproperties.newvalue[1] 24 | Guest = $AuditData.ObjectId } 25 | $Report.Add($ReportLine) }} 26 | }} 27 | Write-Host $NewGuests "new guest records found..." 28 | $Report | Sort GroupName, Timestamp | Get-Unique -AsString | Format-Table Timestamp, Groupname, Guest -------------------------------------------------------------------------------- /Audit/Audit-GuestsActions.ps1: -------------------------------------------------------------------------------- 1 | Connect-MsolService 2 | Connect-ExchangeOnline 3 | 4 | $extUsers = Get-MsolUser | Where-Object {$_.UserPrincipalName -like "*#EXT#*" } 5 | $extUsers | ForEach { 6 | $auditEventsForUser = Search-UnifiedAuditLog -EndDate $((Get-Date)) -StartDate $((Get-Date).AddDays(-365)) -UserIds $_.UserPrincipalName 7 | Write-Host "Events for" $_.DisplayName "created at" $_.WhenCreated 8 | $auditEventsForUser | FT 9 | } -------------------------------------------------------------------------------- /Audit/Search-AdminRolesChanges.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Search-AdminRolesChanges.ps1 - Reports on Office 365 Admin Role 4 | 5 | .DESCRIPTION 6 | This script produces a report of the membership of Office 365 admin role groups. 7 | By default, the report contains only the groups with members. 8 | To get all the role, included empty roles, add -IncludeEmptyRoles $true 9 | 10 | .OUTPUTS 11 | The report is output to an array contained all the audit logs found. 12 | To export in a csv, do Search-AdminRolesChanges | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRolesChange.csv" 13 | 14 | .EXAMPLE 15 | Search-AdminRolesChanges 16 | 17 | .LINK 18 | https://itpro-tips.com/2020/get-the-office-365-admin-roles-and-track-the-changes/ 19 | https://github.com/itpro-tips/Microsoft365-Toolbox/blob/master/Audit/Search-AdminRolesChanges.ps1 20 | 21 | 22 | .NOTES 23 | Written by Bastien Perez (ITPro-Tips.com) 24 | For more Office 365/Microsoft 365 tips and news, check out ITPro-Tips.com. 25 | 26 | Version history: 27 | V1.0, 17 august 2020 - Initial version 28 | V.1.1, 4 january 2020 - Add ObjectUserPrincipalName 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 35 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 36 | DEALINGS IN THE SOFTWARE. 37 | 38 | #> 39 | 40 | # Admin roles list: https://docs.microsoft.com/en-us/microsoft-365/compliance/search-the-audit-log-in-security-and-compliance?view=o365-worldwide 41 | function Search-AdminRolesChanges { 42 | [CmdletBinding()] 43 | param ( 44 | [string[]]$ObjectIDs 45 | ) 46 | 47 | try { 48 | Import-Module exchangeonlinemanagement -ErrorAction stop 49 | } 50 | catch { 51 | Write-Warning 'First, install the official Microsoft Exchange Online Management module : Install-Module ExchangeOnlineManagement' 52 | return 53 | } 54 | 55 | try { 56 | $null = Get-Command Search-UnifiedAuditLog -ErrorAction Stop 57 | } 58 | catch { 59 | Write-Host 'Connect to Exchange Online' -ForegroundColor Cyan 60 | 61 | try { 62 | Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop 63 | } 64 | catch { 65 | Write-Warning 'Unable to connect to Exchange Online' 66 | return 67 | } 68 | } 69 | 70 | try { 71 | #$maxAdminLogAge = [System.TimeSpan]::Parse((Get-AdminAuditLogConfig).AdminAuditLogAgeLimit).Days 72 | $maxAdminLogAge = 365 73 | Write-Host 'Search Add/Remove Member to Role actions logs' -ForegroundColor Cyan 74 | 75 | if ($ObjectIDs) { 76 | $objects = New-Object System.Collections.Generic.List[String] 77 | foreach ($obj in $ObjectIDs) { 78 | $user = Get-User $obj 79 | $null = $objects.Add($user.UserPrincipalName) 80 | $null = $objects.Add($user.ExternalDirectoryObjectId) 81 | } 82 | 83 | } 84 | else { 85 | # Set tp $null to search All 86 | $objects = $null 87 | } 88 | 89 | $records = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-$maxAdminLogAge) -EndDate (Get-Date).AddDays(1) -Operations ('Add Member to Role', 'Remove Member From Role') -ResultSize 2000 -Formatted -ObjectIds $objects 90 | } 91 | catch { 92 | Write-Warning "Unable to gather information $($_.Exception.Message)" 93 | return 94 | } 95 | 96 | if ($records.Count -eq 0) { 97 | Write-Host 'No audit logs found' -ForegroundColor Green 98 | } 99 | else { 100 | 101 | Write-Host "Processing $($Records.Count) audit records..." -ForegroundColor Cyan 102 | 103 | $report = New-Object System.Collections.Generic.List[Object] 104 | ForEach ($record in $records) { 105 | $auditData = ConvertFrom-Json $record.Auditdata 106 | 107 | $timeStamp = Get-Date $record.CreationDate -format g 108 | 109 | # Test if ObjectID is GUID or the UserPrincipalName 110 | if ($auditData.ObjectID -match '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$') { 111 | $objectID = $auditData.ObjectID 112 | } 113 | else { 114 | $objectID = ($auditData.Target | Where-Object { $_.Type -eq 2 -and $_.ID -match '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$' }).ID # several values are with Type=2 ('User_GUID', 'GUID', 'User', 'NetID') The good value is a GUID so we filter one the value which matches GUID 115 | } 116 | 117 | $object = [PSCustomObject]@{ 118 | TimeStamp = $timeStamp 119 | ObjectId = $objectID 120 | ObjectUserPrincipalName = ($auditData.Target | Where-Object { $_.Type -eq 5 }).Id # value is @{ID=xxx@domain; Type=5 121 | RoleName = $auditData.modifiedproperties.newvalue[1] 122 | Action = $auditData.Operation 123 | Actor = $auditData.UserId 124 | ActorIpAddress = $auditData.ActorIpAddress 125 | } 126 | 127 | $report.Add($object) 128 | } 129 | 130 | return $report 131 | } 132 | } -------------------------------------------------------------------------------- /AzureAD/readme.md: -------------------------------------------------------------------------------- 1 | See folder [Microsoft Entra ID](https://github.com/itpro-tips/Microsoft365-Toolbox/tree/master/MicrosoftEntraID) 2 | -------------------------------------------------------------------------------- /Exchange/Get-ExchangeRoleReport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get-ExchangeRoleReport - Reports on Exchange RBAC roles and permissions. 4 | 5 | .DESCRIPTION 6 | This script produces a report of the membership of Exchange RBAC role groups. 7 | By default, the report contains only the groups with members. 8 | 9 | .OUTPUTS 10 | The report is output to an array contained all the audit logs found. 11 | To export in a csv, do Get-ExchangeRoleReport | Export-CSV -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 12 | 13 | .EXAMPLE 14 | Get-RBACReport 15 | 16 | 17 | .LINK 18 | 19 | 20 | .NOTES 21 | Written by Bastien Perez (ITPro-Tips.com) 22 | 23 | Version history: 24 | V1.0, 14 april 2022 - Initial version 25 | v2.0 22 november 2024 - Add option to see permission graph - not work by now 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 33 | DEALINGS IN THE SOFTWARE. 34 | 35 | #> 36 | function Get-ExchangeRoleReport { 37 | [CmdletBinding()] 38 | param ( 39 | [switch]$ShowGraph 40 | ) 41 | 42 | try { 43 | Import-Module ExchangeOnlineManagement -ErrorAction stop 44 | } 45 | catch { 46 | Write-Warning 'First, install the official Microsoft Import-Module ExchangeOnlineManagement module : Install-Module ExchangeOnlineManagement' 47 | return 48 | } 49 | 50 | try { 51 | # -ShowPartnerLinked : 52 | # This ShowPartnerLinked switch specifies whether to return built-in role groups that are of type PartnerRoleGroup. You don't need to specify a value with this switch. 53 | # This type of role group is used in the cloud-based service to allow partner service providers to manage their customer organizations. 54 | # These types of role groups can't be edited and are not shown by default. 55 | $exchangeRoles = Get-RoleGroup -ShowPartnerLinked -ErrorAction Stop 56 | } 57 | catch { 58 | Connect-ExchangeOnline 59 | $exchangeRoles = Get-RoleGroup -ShowPartnerLinked -ErrorAction Stop 60 | } 61 | 62 | $exchangeRolesMembership = New-Object 'System.Collections.Generic.List[System.Object]' 63 | 64 | foreach ($exchangeRole in $exchangeRoles) { 65 | try { 66 | $roleMembers = @(Get-RoleGroupMember -Identity $exchangeRole.Identity -ResultSize Unlimited) 67 | 68 | # Add green color if member found into the role 69 | if ($roleMembers.count -gt 0) { 70 | Write-Host -ForegroundColor Green "Role $($exchangeRole.Name) - Member(s) found: $($roleMembers.count)" 71 | } 72 | else { 73 | Write-Host -ForegroundColor Cyan "Role $($exchangeRole.Name) - Member found: $($roleMembers.count)" 74 | } 75 | 76 | if ($exchangeRole.Description -eq '' -and $exchangeRole.Name -like 'ISVMailboxUsers_*') { 77 | $roleDescription = 'Third-party application developer mailbox role' 78 | } 79 | else { 80 | $roleDescription = $exchangeRole.Description 81 | } 82 | 83 | if ($roleMembers.count -eq 0) { 84 | $object = [PSCustomObject][ordered]@{ 85 | 'Role' = $exchangeRole.Name 86 | 'MemberName' = '-' 87 | 'MemberDisplayName' = '-' 88 | 'MemberPrimarySMTPAddres' = '-' 89 | 'MemberIsDirSynced' = '-' 90 | 'MemberObjectID' = '-' 91 | 'MemberRecipientTypeDetails' = '-' 92 | # si descriptin vide et si nom like ISVMailboxUsers_* on donne une description 'Third-party application developer mailbox role' 93 | 'RoleDescription' = $roleDescription 94 | } 95 | 96 | $exchangeRolesMembership.Add($object) 97 | 98 | } 99 | else { 100 | 101 | foreach ($roleMember in $roleMembers) { 102 | # if user already exist in the arraylist, we look for to prevent a new Get-MsolUser (time consuming) 103 | # Select only the first if user already exists in multiple roles 104 | 105 | $object = [PSCustomObject][ordered]@{ 106 | 'Role' = $exchangeRole.Name 107 | 'MemberName' = $roleMember.Name 108 | 'MemberDisplayName' = $roleMember.DisplayName 109 | 'MemberPrimarySMTPAddres' = $roleMember.PrimarySmtpAddress 110 | 'MemberIsDirSynced' = $roleMember.IsDirSynced 111 | 'MemberObjectID' = $roleMember.ExternalDirectoryObjectId 112 | 'MemberRecipientTypeDetails' = $roleMember.RecipientTypeDetails 113 | 'RoleDescription' = $roleDescription 114 | } 115 | 116 | $exchangeRolesMembership.Add($object) 117 | } 118 | 119 | } 120 | } 121 | catch { 122 | Write-Warning $_.Exception.Message 123 | } 124 | } 125 | 126 | # il faut ajouter Get-ManagementRoleAssignment pour avoir les permissions car parfois cela n'est pas fait via un groupe 127 | #Get-ManagementRoleAssignment -RoleAssigneeType User 128 | return $exchangeRolesMembership 129 | } -------------------------------------------------------------------------------- /Exchange/Get-ExoProtocols.ps1: -------------------------------------------------------------------------------- 1 | $tenantSmtpClientAuthenticationDisabled = (Get-TransportConfig).SmtpClientAuthenticationDisabled 2 | 3 | if ($tenantSmtpClientAuthenticationDisabled) { 4 | Write-Host "SMTP Client Authentication is disabled" -ForegroundColor Green 5 | $tenantSmtpClientAuthenticationEnabled = $false 6 | } 7 | else { 8 | Write-Host "SMTP Client Authentication is enabled" -ForegroundColor Yellow 9 | $tenantSmtpClientAuthenticationEnabled = $true 10 | } 11 | 12 | # PropertySets All because by default SMTPClientAuthenticationDisabled is not returned 13 | $casMailboxes = Get-EXOCasMailbox -ResultSize Unlimited -PropertySets All 14 | 15 | <# 16 | ECPEnabled : True 17 | OWAEnabled : True 18 | ImapEnabled : True 19 | PopEnabled : True 20 | MAPIEnabled : True 21 | EwsEnabled : True 22 | ActiveSyncEnabled : True 23 | #> 24 | 25 | [System.Collections.Generic.List[PSObject]]$exoCasMailboxesArray = @() 26 | foreach ($casMailbox in $casMailboxes) { 27 | 28 | $object = [PSCustomObject][ordered]@{ 29 | Name = $casMailbox.Name 30 | ECPEnabled = $casMailbox.ECPEnabled 31 | OWAEnabled = $casMailbox.OWAEnabled 32 | ImapEnabled = $casMailbox.ImapEnabled 33 | PopEnabled = $casMailbox.PopEnabled 34 | MAPIEnabled = $casMailbox.MAPIEnabled 35 | EwsEnabled = $casMailbox.EwsEnabled 36 | ActiveSyncEnabled = $casMailbox.ActiveSyncEnabled 37 | # CMDlet returns SMTPClientAuthenticationDisabled but we want SMTPClientAuthenticationEnabled 38 | SMTPClientAuthenticationEnabled = if ($null -ne $casMailbox.SMTPClientAuthenticationDisabled) { -not $casMailbox.SMTPClientAuthenticationDisabled }else { '-' } 39 | TenantSmtpClientAuthenticationEnabled = $tenantSmtpClientAuthenticationEnabled 40 | } 41 | 42 | $exoCasMailboxesArray.Add($object) 43 | } 44 | 45 | return $exoCasMailboxesArray -------------------------------------------------------------------------------- /Exchange/Get-MailAutoForwarded.ps1: -------------------------------------------------------------------------------- 1 | <#You can use this cmdlet to search message data for the last 10 days. If you run this cmdlet without any parameters, data from last 10 days 2 | If you enter a time period that's older than 10 days, you won't receive an error, but the command will return no results. 3 | To search for message data that is greater than 10 days old, use the Start-HistoricalSearch and Get-HistoricalSearch cmdlets. 4 | Careful about http://blog.icewolf.ch/archive/2020/10/06/how-to-control-the-many-ways-of-email-forwarding-in.aspx 5 | Reports: https://protection.office.com/reportv2?id=MailFlowForwarding&pivot=Name 6 | https://misstech.co.uk/2020/07/27/new-controls-available-to-block-automatic-email-forwarding/ 7 | #> 8 | 9 | function Get-MailAutoForwarded { 10 | [CmdletBinding()] 11 | param ( 12 | [Parameter()] 13 | [string]$SenderAddress, 14 | [switch]$FailedOnly, 15 | [int]$Days = 10 16 | ) 17 | 18 | $remoteDomains = Get-RemoteDomain 19 | 20 | foreach ($remoteDomain in $remoteDomains) { 21 | Write-Host "Remote Domain '$remotedomain' AutoForwardEnabled: $($remoteDomain.AutoForwardEnabled)" -ForegroundColor Cyan 22 | } 23 | 24 | $outboundSpamPolicies = Get-HostedOutboundSpamFilterPolicy 25 | 26 | foreach ($outboundSpamPolicy in $outboundSpamPolicies) { 27 | 28 | Write-Host "OutboundSpamPolicy '$($outboundSpamPolicy.Name)' AutoForwardingMode: $($outboundSpamPolicy.AutoForwardingMode)" -ForegroundColor Cyan 29 | 30 | $autoForwardMode = $outboundSpamPolicy.AutoForwardingMode 31 | 32 | if ($autoForwardMode -eq 'Automatic') { 33 | Write-Host "Careful, the value 'Automatic is now the same as AutoForwardEnable=Off, means autoForward is even if the Remote domain(s) are configured with AutoForwardEnable = `$true 34 | Sources: 35 | https://office365itpros.com/2020/11/12/microsoft-clamps-down-mail-forwarding-exchange-online/ 36 | http://blog.icewolf.ch/archive/2020/10/06/how-to-control-the-many-ways-of-email-forwarding-in.aspx 37 | https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/external-email-forwarding?view=o365-worldwide 38 | https://techcommunity.microsoft.com/t5/exchange-team-blog/all-you-need-to-know-about-automatic-email-forwarding-in/ba-p/2074888 39 | 40 | RoadMap ID: MC221113" -ForeGround Yellow 41 | } 42 | } 43 | 44 | Write-Host 'Get messages from the last' $days 'days' -ForegroundColor Cyan 45 | 46 | Write-Host "`nIf you prefer, you can use the report in https://admin.cloud.microsoft/exchange?ref=/homepage#/reports/autoforwardedmessages`n" -ForegroundColor Green 47 | $params = @{} 48 | 49 | $params.Add('StartDate', (Get-Date).AddDays(-$days)) 50 | $params.Add('EndDate', (Get-Date)) 51 | 52 | #The PageSize parameter specifies the maximum number of entries per page. Valid input for this parameter is an integer between 1 and 5000. The default value is 1000. 53 | $params.PageSize = 5000 54 | 55 | if ($SenderAddress) { 56 | $params.Add('SenderAddress', $SenderAddress) 57 | } 58 | if ($FailedOnly) { 59 | $params.Add('FailedOnly', $FailedOnly) 60 | } 61 | 62 | $messages = Get-MessageTrace @params 63 | <# 64 | if ($SenderAddress) { 65 | if ($FailedOnly) { 66 | $messages = Get-MessageTrace -Status Failed -StartDate (Get-Date).AddDays(-$days) -EndDate (Get-Date) -PageSize 5000 -SenderAddress $SenderAddress 67 | } 68 | else { 69 | $messages = Get-MessageTrace -StartDate (Get-Date).AddDays(-$days) -EndDate (Get-Date) -PageSize 5000 -SenderAddress $SenderAddress 70 | } 71 | } 72 | else { 73 | if ($FailedOnly) { 74 | $messages = Get-MessageTrace -Status Failed -StartDate (Get-Date).AddDays(-$days) -EndDate (Get-Date) -PageSize 5000 75 | } 76 | else { 77 | $messages = Get-MessageTrace -StartDate (Get-Date).AddDays(-$days) -EndDate (Get-Date) -PageSize 5000 78 | } 79 | } 80 | #> 81 | 82 | Write-Host "Search in the $($messages.count) messages to find autoforward" -ForegroundColor green 83 | 84 | #only one Get-MessageTraceDetail for all because it's time consuming (12 seconds -> 8 seconds for about 20 messages!) 85 | #$messagesAutoForwarded = $messages | Get-MessageTraceDetail | Where-Object { $_.Detail -like '*LED=250 2.1.5 RESOLVER.MSGTYPE.AF; handled AutoForward addressed to external recipient*' -or $_.Detail -like '*LED=250 2.1.5 RESOLVER.FWD.Forwarded; recipient forward*' } 86 | $messagesAutoForwarded = $messages | ForEach-Object { Get-MessageTraceDetail -RecipientAddress $_.RecipientAddress -MessageTraceId $_.MessageTraceId | Where-Object { $_.Detail -like '*LED=250 2.1.5 RESOLVER.MSGTYPE.AF; handled AutoForward addressed to external recipient*' -or $_.Detail -like '*LED=250 2.1.5 RESOLVER.FWD.Forwarded; recipient forward*' } } 87 | 88 | [System.Collections.Generic.List[PSObject]]$emailsWithAutoForward = @() 89 | 90 | foreach ($messageAF in $messagesAutoForwarded) { 91 | # Get-MessageTraceDetail does not return info about sender, etc. so we search in the $messages list the $message 92 | $message = $messages | Where-Object { $_.MessageId -eq $messageAF.MessageId } 93 | 94 | $message | ForEach-Object { 95 | $object = [PSCustomObject] [ordered]@{ 96 | SenderAddress = $_.SenderAddress 97 | RecipientAddress = $_.RecipientAddress 98 | Subject = $_.Subject 99 | Detail = $messageAF.detail 100 | Status = $_.Status 101 | Received = $_.Received 102 | FromIP = $_.FromIP 103 | ToIP = $_.ToIP 104 | MessageId = $_.MessageId 105 | } 106 | 107 | $emailsWithAutoForward.add($object) 108 | } 109 | } 110 | 111 | return $emailsWithAutoForward 112 | } -------------------------------------------------------------------------------- /Exchange/Get-MailboxForwarding.ps1: -------------------------------------------------------------------------------- 1 | # Priority 1: forwardingAddress 2 | # Priority 2: forwardingSMTPAddress 3 | # Priority 3: inbox rule 4 | 5 | # Autoforward works if forwardingAddress because it's an internal object 6 | # TODO: 7 | # Add forwardWorks for inbox rules 8 | # Add forwardWorks if RemoteDomain enable 9 | 10 | function Get-MailboxForwarding { 11 | 12 | [CmdletBinding()] 13 | param ( 14 | [Parameter(Mandatory = $false)] 15 | [ValidateNotNullOrEmpty()] 16 | [string[]]$Mailboxes, 17 | [Parameter(Mandatory = $false)] 18 | [switch]$ForwardingAndForwardingSMTPOnly, 19 | [Parameter(Mandatory = $false)] 20 | [switch]$InboxRulesOnly, 21 | [Parameter(Mandatory = $false)] 22 | [switch]$ExportResults, 23 | [Parameter(Mandatory = $false)] 24 | [switch]$ExchangeOnPremise 25 | ) 26 | 27 | function Translate-Recipient { 28 | Param ( 29 | [Parameter(Mandatory = $true)] 30 | [string]$Recipient 31 | ) 32 | 33 | # "name" [EX:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=xxxx] if recipient is an object in the organization (mailbox, mail contact, etc.) 34 | # "name" [SMTP: is not in the same organization 35 | 36 | if ($Recipient -like '*`[SMTP:*@*') { 37 | # need to escape the [ 38 | $recipientConverted = ($Recipient -split 'SMTP:')[1].TrimEnd(']') 39 | } 40 | # we use the LegacyExchangeDN after the EX: to get the recipient domain 41 | elseif ($Recipient -like '*`[EX:*') { 42 | # remove the last character (]) 43 | $temp = ($Recipient -split 'EX:')[1].TrimEnd(']') 44 | $recipientConverted = $hashRecipients[$temp] 45 | } 46 | else { 47 | $recipientConverted = 'unknown format' 48 | } 49 | 50 | return $recipientConverted 51 | } 52 | 53 | [System.Collections.Generic.List[PSObject]]$mailboxesList = @() 54 | [System.Collections.Generic.List[PSObject]]$forwardList = @() 55 | # inboxForwardList is use to contains the mailbox with inbox rules with forward. We need to use it as temporary storage to check if the mailbox has already a forward set by forwardingAddress or forwardingSMTPAddress 56 | [System.Collections.Generic.List[PSObject]]$inboxForwardList = @() 57 | 58 | Write-Host -ForegroundColor cyan 'Get Accepted Domain in Exchange Online to identify internal/external forward' 59 | $internalDomains = (Get-AcceptedDomain).DomainName 60 | 61 | $remoteDomains = Get-RemoteDomain 62 | 63 | foreach ($remoteDomain in $remoteDomains) { 64 | Write-Host "Remote Domain '$remotedomain' AutoForwardEnabled: $($remoteDomain.AutoForwardEnabled)" -ForegroundColor Cyan 65 | } 66 | 67 | if (-not $ExchangeOnPremise) { 68 | $outboundSpamPolicies = [array](Get-HostedOutboundSpamFilterPolicy) 69 | 70 | Write-Host -ForegroundColor Cyan "You have $($outboundSpamPolicies.Count) OutboundSpamPolicies": 71 | foreach ($outboundSpamPolicy in $outboundSpamPolicies) { 72 | 73 | $state = (Get-HostedOutboundSpamFilterRule | Where-Object { $_.HostedOutboundSpamFilterPolicy -eq $outboundSpamPolicy.Name }).State 74 | 75 | if ($state -eq 'Enabled') { 76 | $prefix = '' 77 | $color = 'Cyan' 78 | } 79 | else { 80 | $prefix = '[NOT ENABLED] ' 81 | $color = 'Gray' 82 | } 83 | 84 | Write-Host "- $prefix`OutboundSpamPolicy '$($outboundSpamPolicy.Name)' - AutoForwardingMode: $($outboundSpamPolicy.AutoForwardingMode)" -ForegroundColor $color 85 | 86 | $autoForwardMode = $outboundSpamPolicy.AutoForwardingMode 87 | 88 | if ($autoForwardMode -eq 'Automatic' -and $state -eq 'Enabled') { 89 | Write-Host "Careful, the value 'Automatic is now the same as AutoForwardEnable=Off, means autoForward is disabled even if the Remote domain(s) are configured with AutoForwardEnable = `$true 90 | Sources: 91 | https://office365itpros.com/2020/11/12/microsoft-clamps-down-mail-forwarding-exchange-online/ 92 | http://blog.icewolf.ch/archive/2020/10/06/how-to-control-the-many-ways-of-email-forwarding-in.aspx 93 | https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/external-email-forwarding?view=o365-worldwide 94 | https://techcommunity.microsoft.com/t5/exchange-team-blog/all-you-need-to-know-about-automatic-email-forwarding-in/ba-p/2074888 95 | 96 | RoadMap ID: MC221113" -ForeGround Yellow 97 | } 98 | } 99 | } 100 | 101 | $hashRecipients = @{} 102 | 103 | Write-Host -ForegroundColor Cyan 'Get all Exchange recipients' 104 | 105 | # Get all recipients, needed for forwardingAddress 106 | if (-not $ExchangeOnPremise) { 107 | $recipients = Get-EXORecipient -ResultSize Unlimited 108 | } 109 | else { 110 | $recipients = Get-Recipient -ResultSize Unlimited 111 | } 112 | 113 | $recipients | ForEach-Object { 114 | $hashRecipients.Add($_.Name, $_.PrimarySmtpAddress) 115 | } 116 | 117 | $properties = @('Identity', 'Name', 'DistinguishedName', 'PrimarySmtpAddress', 'ForwardingAddress', 'ForwardingSmtpAddress', 'DeliverToMailboxAndForward', 'LegacyExchangeDN', 'UserPrincipalName', 'DisplayName') 118 | 119 | # Get-LegacyExchangeDN, needed for inbox rules. We can also use name or ID but legacyExchangeDN is more reliable 120 | # Get-EXORecipient does not contain LegacyExchangeDN property so we need to get it from Get-EXOMailbox / Get-DistributionGroup and Get-UnifiedGroup 121 | if (-not $ExchangeOnPremise) { 122 | 123 | # Get-EOMailbox doesn't contain all the properties we need by default, so we need to specify them 124 | Get-EXOMailbox -ResultSize Unlimited -Properties $properties | Select-Object $properties | ForEach-Object { 125 | $mailboxesList.Add($_) 126 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 127 | } 128 | 129 | Get-DistributionGroup -ResultSize Unlimited | ForEach-Object { 130 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 131 | } 132 | 133 | Get-UnifiedGroup -ResultSize Unlimited | ForEach-Object { 134 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 135 | } 136 | } 137 | else { 138 | <# 139 | Get-Mailbox -ResultSize Unlimited | ForEach-Object { 140 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 141 | } 142 | #> 143 | 144 | Get-Mailbox -ResultSize Unlimited | Select-Object $properties | ForEach-Object { 145 | $mailboxesList.Add($_) 146 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 147 | } 148 | 149 | Get-DistributionGroup -ResultSize Unlimited | ForEach-Object { 150 | $hashRecipients.Add($_.LegacyExchangeDN, $_.PrimarySmtpAddress) 151 | } 152 | } 153 | 154 | # if mailboxes is specified, get only these mailboxes 155 | if ($null -ne $Mailboxes -and $Mailboxes.Count -gt 0) { 156 | [System.Collections.Generic.List[Object]]$tempMailboxesList = @() 157 | foreach ($mbx in $Mailboxes) { 158 | try { 159 | $mailbox = $mailboxesList | Where-Object { $_.PrimarySMTPAddress -eq $mbx } 160 | $tempMailboxesList.Add($mailbox) 161 | } 162 | catch { 163 | Write-Warning "$mbx mailbox not found. $($_.Exception.Message)" 164 | } 165 | } 166 | 167 | $mailboxesList = $tempMailboxesList 168 | } 169 | # else get all mailboxes 170 | else { 171 | # all mailboxes are in $mailboxesList 172 | # nothing to do here 173 | } 174 | 175 | # To prevent, block via rule and via OWA policy 176 | 177 | # If user set forwardingSMTPaddress+deliverToMailboxAndForward is set AND forwardingAddress is also set. The Exchange Online CMDLet will tell us the deliverToMailboxAndForward is set... but no ! 178 | # Many ways to block automatic email forwarding in Exchange Online : https://techcommunity.microsoft.com/t5/exchange-team-blog/the-many-ways-to-block-automatic-email-forwarding-in-exchange/ba-p/607579 179 | # https://nedimmehic.org/2019/08/08/disable-forwarding-in-owa-with-powershell/ 180 | 181 | # Identify mailbox with DistinguishedName to prevent issue in case of alias/name duplicate 182 | <# 183 | $mbxWithForward = $mailboxesList | Where-Object { ($null -ne $_.ForwardingSMTPAddress) -or ($null -ne $_.ForwardingAddress) } | Select-Object Name, PrimarySmtpAddress, ForwardingSMTPAddress, ForwardingAddress, @{Name = 'ForwardingAddressConvertSMTP'; Expression = { if ($null -ne $_.ForwardingAddress) { (Get-Recipient -Identity "$($_.ForwardingAddress)").PrimarySmtpAddress } } }, DeliverToMailboxAndForward 184 | 185 | if ($null -ne $mbxWithForward) { 186 | Write-Host -ForegroundColor Yellow "ForwardingAddress and ForwardingSMTP Address found" 187 | $mbxWithForward 188 | } 189 | else { 190 | Write-Host -ForegroundColor Green "SERVER SIDE (forwardingAddress and ForwardingSMTP Address) : No forward on server side" 191 | } 192 | #> 193 | 194 | if (-not($InboxRulesOnly)) { 195 | foreach ($mailbox in $mailboxesList) { 196 | Write-Host -ForegroundColor cyan "Processing ForwardingAddress|ForwardingSMTPAddress - $($mailbox.Name) - $($mailbox.PrimarySMTPAddress)" 197 | #$forward = $mailbox | Where-Object { ($null -ne $_.ForwardingSMTPAddress) -or ($null -ne $_.ForwardingAddress) } | Select-Object Name, PrimarySmtpAddress, ForwardingSMTPAddress, ForwardingAddress, @{Name = 'ForwardingAddressConvertSMTP'; Expression = { if ($null -ne $_.ForwardingAddress) { $hashRecipients[$_.ForwardingAddress] } } }, DeliverToMailboxAndForward 198 | 199 | <# --- Forwarding Address part --- 200 | ForwardingAddress is a RecipientIdParameter and used when you want to forward emails to a mail-enabled object. 201 | The target object should exists in your ActiveDirectory | Exchange Online as a mail-enabled object like MailUser, Contact or RemoteMailUser. 202 | If you do not have a mail-enabled object for your forwarding address then this will not work. 203 | ForwardingAddress can be set by using the -ForwardingAddress parameter in the command set-mailbox. 204 | #> 205 | 206 | if ($ExchangeOnPremise) { 207 | $forwardingAddress = $mailbox | Where-Object { ($null -ne $_.ForwardingAddress) } | Select-Object Name, PrimarySmtpAddress, ForwardingAddress, @{Name = 'ForwardingAddressConverted'; Expression = { if ($null -ne $_.ForwardingAddress) { $hashRecipients[$_.ForwardingAddress.Name].Address } } }, DeliverToMailboxAndForward 208 | } 209 | else { 210 | $forwardingAddress = $mailbox | Where-Object { ($null -ne $_.ForwardingAddress) } | Select-Object Name, PrimarySmtpAddress, ForwardingAddress, @{Name = 'ForwardingAddressConverted'; Expression = { if ($null -ne $_.ForwardingAddress) { $hashRecipients[$_.ForwardingAddress] } } }, DeliverToMailboxAndForward 211 | } 212 | 213 | #$forward = $mailbox | Where-Object { ($null -ne $_.ForwardingSMTPAddress -and -not($internalDomains -contains $_.ForwardingSMTPAddress.split('@')[1])) -or ($null -ne $_.ForwardingAddress -and -not($internalDomains -contains $hashRecipients[$_.ForwardingSMTPAddress].split('@')[1]) ) } | Select-Object Name, PrimarySmtpAddress, ForwardingSMTPAddress, ForwardingAddress, @{Name = 'ForwardingAddressConvertSMTP'; Expression = { if ($null -ne $_.ForwardingAddress) { $hashRecipients[$_.ForwardingAddress] } } }, DeliverToMailboxAndForward 214 | 215 | if ($null -ne $forwardingAddress) { 216 | Write-Host -ForegroundColor yellow "$($mailbox.Name) - $($mailbox.PrimarySMTPAddress) - 1 forwardingAddress parameter found" 217 | 218 | $recipientDomain = $forwardingAddress.ForwardingAddressConverted.Split('@')[1] 219 | 220 | if ($internalDomains -contains $recipientDomain) { 221 | $forwardingWorks = "True ($recipientDomain = internalDomain)" 222 | } 223 | elseif ($autoForwardMode -eq 'Automatic' -or $autoForwardMode -eq 'Off') { 224 | $forwardingWorks = "False (Autoforward mode = $autoForwardMode)" 225 | } 226 | else { 227 | $forwardingWorks = "Yes (Autoforward mode = $autoForwardMode) and address used is an internal object (contact or mailbox)" 228 | } 229 | 230 | $object = [PSCustomObject][ordered]@{ 231 | Identity = $mailbox.Identity 232 | Name = $mailbox.Name 233 | DisplayName = $mailbox.DisplayName 234 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 235 | UserPrincipalName = $mailbox.UserPrincipalName 236 | ForwardingAddressConverted = $forwardingAddress.ForwardingAddressConverted 237 | ForwardType = 'ForwardingAddress' 238 | ForwardScope = '' 239 | Precedence = '-' 240 | ForwardingAddress = $forwardingAddress.ForwardingAddress 241 | ForwardingSMTPAddress = '-' 242 | ForwardingWorks = $forwardingWorks 243 | DeliverToMailboxAndForward = '-' 244 | InboxRulePriority = '-' 245 | InboxRuleEnabled = '-' 246 | InboxRuleForwardAddressConverted = '-' 247 | InboxRuleRedirectTo = '-' 248 | InboxRuleForwardTo = '-' 249 | InboxRuleForwardAsAttachmentTo = '-' 250 | InboxRuleSendTextMessageNotificationTo = '-' 251 | InboxRuleDescription = '-' 252 | } 253 | 254 | #Add object to an array 255 | $forwardList.Add($object) 256 | } 257 | 258 | <# --- Forwarding SMTP Address part --- 259 | On the other hand, ForwardingSMTPAddress, it is a ProxyAddresses Value and has lower priority than ForwardingAddress. 260 | You can set this attribute with a remote SMTP address even if there is no mail-enabled Object exists in your ActiveDirectory | Exchange Online 261 | User can set ForwardingSMTPAddress in OWA. 262 | The ForwardingSMTPAddress has a higher priority than InboxRule : 263 | 'This is expected behavior. Forwarding on a mailbox overrides an inbox redirection rule. To enable the redirection rule, remove forwarding on the mailbox.' 264 | (https://support.microsoft.com/en-us/help/3069075/inbox-rule-to-redirect-messages-doesn-t-work-if-forwarding-is-set-up-o 265 | ) 266 | 267 | #> 268 | $forwardingSMTPAddress = $mailbox | Where-Object { ($null -ne $_.ForwardingSMTPAddress) } 269 | #$forward = $mailbox | Where-Object { ($null -ne $_.ForwardingSMTPAddress -and -not($internalDomains -contains $_.ForwardingSMTPAddress.split('@')[1])) -or ($null -ne $_.ForwardingAddress -and -not($internalDomains -contains $hashRecipients[$_.ForwardingSMTPAddress].split('@')[1]) ) } | Select-Object Name, PrimarySmtpAddress, ForwardingSMTPAddress, ForwardingAddress, @{Name = 'ForwardingAddressConvertSMTP'; Expression = { if ($null -ne $_.ForwardingAddress) { $hashRecipients[$_.ForwardingAddress] } } }, DeliverToMailboxAndForward 270 | 271 | if ($null -ne $forwardingSMTPAddress) { 272 | Write-Host -ForegroundColor yellow "$($mailbox.Name) - $($mailbox.PrimarySMTPAddress) - 1 forwardingSMTPAddress parameter found" 273 | 274 | # we need to check if the forwardList.PrimarySMTPAddress already contains 275 | #if ($forwardList.PrimarySMTPAddress -contains $mailbox.PrimarySmtpAddress) { 276 | if ($forwardList.PrimarySMTPAddress -contains $mailbox.PrimarySmtpAddress) { 277 | $precedence = 'ForwardingAddress is already set for this mailbox. ForwardingAddress has a higher priority than the ForwardingSMTPAddress. This ForwardingSMTPAddress is ignored' 278 | } 279 | else { 280 | $precedence = '-' 281 | } 282 | 283 | if ($ExchangeOnPremise) { 284 | # in exchange on premise, the ForwardingSMTPAddress is a ProxyAddress and is stored in the ProxyAddressesString attribute (the value is smtp:xxx) 285 | $recipientDomain = $forwardingSMTPAddress.ForwardingSmtpAddress.ProxyAddressString.Split('@')[1] 286 | $forwardingAddressConverted = $forwardingSMTPAddress.ForwardingSmtpAddress.ProxyAddressString.replace('smtp:', '') 287 | } 288 | else { 289 | $recipientDomain = $forwardingSMTPAddress.forwardingSMTPAddress.Split('@')[1] 290 | $forwardingAddressConverted = $forwardingSMTPAddress.forwardingSMTPAddress.replace('smtp:', '') 291 | } 292 | 293 | 294 | if ($internalDomains -contains $recipientDomain) { 295 | $forwardingWorks = "True ($recipientDomain = internalDomain)" 296 | } 297 | elseif ($autoForwardMode -eq 'Automatic' -or $autoForwardMode -eq 'Off') { 298 | $forwardingWorks = "False (Autoforward mode = $autoForwardMode)" 299 | } 300 | else { 301 | $forwardingWorks = "Maybe (Autoforward mode = $autoForwardMode), check if RemoteDomain(s) allows external forwarding and check if TransportRule(s) exist to prevent external forwarding" 302 | } 303 | 304 | $object = [PSCustomObject][ordered]@{ 305 | Identity = $mailbox.Identity 306 | Name = $mailbox.Name 307 | DisplayName = $mailbox.DisplayName 308 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 309 | UserPrincipalName = $mailbox.UserPrincipalName 310 | ForwardingAddressConverted = $forwardingAddressConverted 311 | ForwardType = 'ForwardingSMTPAddress' 312 | ForwardScope = '' 313 | Precedence = $precedence 314 | ForwardingAddress = '-' 315 | ForwardingSMTPAddress = $forwardingSMTPAddress.ForwardingSMTPAddress 316 | ForwardingWorks = $forwardingWorks 317 | DeliverToMailboxAndForward = $forwardingSMTPAddress.DeliverToMailboxAndForward 318 | InboxRulePriority = '-' 319 | InboxRuleEnabled = '-' 320 | InboxRuleForwardAddressConverted = '-' 321 | InboxRuleRedirectTo = '-' 322 | InboxRuleForwardTo = '-' 323 | InboxRuleForwardAsAttachmentTo = '-' 324 | InboxRuleSendTextMessageNotificationTo = '-' 325 | InboxRuleDescription = '-' 326 | } 327 | 328 | #Add object to an array 329 | $forwardList.Add($object) 330 | } 331 | } 332 | } 333 | #$mailboxesWithInboxForward = New-Object 'System.Collections.Generic.List[System.Object]' 334 | $i = 0 335 | if (-not($ForwardingAndForwardingSMTPOnly)) { 336 | foreach ($mailbox in $mailboxesList) { 337 | $i++ 338 | Write-Host -ForegroundColor cyan "Processing Inbox rules - $($mailbox.Name) - $($mailbox.PrimarySMTPAddress) [$i/$($mailboxesList.count)]" 339 | 340 | $inboxForwardRules = @(Get-InboxRule -Mailbox "$($mailbox.DistinguishedName)" | Where-Object { ($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo) -or ($null -ne $_.RedirectTo) -or ($_.SendTextMessageNotificationTo.count -gt 0) }) | Select-Object Identity, Enabled, ForwardTo, ForwardAsAttachmentTo, RedirectTo, SendTextMessageNotificationTo, Description, Priority 341 | 342 | if ($inboxForwardRules.count -gt 0) { 343 | Write-Host -ForegroundColor yellow "$($mailbox.Name) - $($mailbox.PrimarySMTPAddress) - $(($inboxForwardRules ).count) forward rule(s) found" 344 | } 345 | 346 | foreach ($inboxForwardRule in $inboxForwardRules) { 347 | if ($forwardList.PrimarySMTPAddress -contains $mailbox.PrimarySmtpAddress -and ($forwardList.ForwardingAddress -ne '-' -or $forwardList.ForwardingSMTPAddress -ne '-')) { 348 | $precedence = 'ForwardingAddress | ForwardingSMTPAddress is already set for this mailbox. They have a higher priority than inbox rules. This inbox rule will be ignored unless DeliverToMailboxAndForward is set to $true' 349 | } 350 | else { 351 | $precedence = '-' 352 | } 353 | 354 | # ForwardTo, ForwardAsAttachmentTo, RedirectTo are in the following format: 355 | # "name" [EX:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=xxxx] if recipient is an object in the organization (mailbox, mail contact, etc.) 356 | # "name" [SMTP: is not in the same organization 357 | # ForwardTo, ForwardAsAttachmentTo, RedirectTo can be a list of recipients 358 | # SendTextMessageNotificationTo is a list of phone numbers 359 | 360 | foreach ($forwardTo in $inboxForwardRule.ForwardTo) { 361 | 362 | if ($ExchangeOnPremise) { 363 | $inboxForwardRuleDescription = $inboxForwardRule.description.ToString().replace("`t", "") # delete line breaks and tabs 364 | } 365 | else { 366 | $inboxForwardRuleDescription = $inboxForwardRule.description.replace("`r`n", " ").replace("`t", "") # delete line breaks and tabs 367 | } 368 | 369 | $recipientConverted = Translate-Recipient -Recipient $forwardTo 370 | 371 | $object = [PSCustomObject][ordered]@{ 372 | Identity = $mailbox.Identity 373 | Name = $mailbox.Name 374 | DisplayName = $mailbox.DisplayName 375 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 376 | UserPrincipalName = $mailbox.UserPrincipalName 377 | ForwardingAddressConverted = $recipientConverted 378 | ForwardType = 'InboxRule' 379 | ForwardScope = $forwardingScope 380 | Precedence = $precedence 381 | ForwardingAddress = '-' 382 | ForwardingSMTPAddress = '-' 383 | ForwardingWorks = 'Not evaluated yet (check precedence and InboxRuleEnabled and forward address)' 384 | DeliverToMailboxAndForward = '-' 385 | InboxRulePriority = $inboxForwardRule.Priority 386 | InboxRuleEnabled = $inboxForwardRule.Enabled 387 | InboxRuleForwardAddressConverted = $recipientConverted 388 | InboxRuleRedirectTo = '-' 389 | InboxRuleForwardTo = $forwardTo 390 | InboxRuleForwardAsAttachmentTo = '-' 391 | InboxRuleSendTextMessageNotificationTo = '-' 392 | InboxRuleDescription = $inboxForwardRuleDescription 393 | } 394 | 395 | #Add object to an array 396 | # $forwardList.Add($object) 397 | $inboxForwardList.Add($object) 398 | } 399 | 400 | foreach ($forwardAsAttachmentTo in $inboxForwardRule.ForwardAsAttachmentTo) { 401 | if ($ExchangeOnPremise) { 402 | $inboxForwardRuleDescription = $inboxForwardRule.description.ToString().("`r`n", " ").replace("`t", "") # delete line breaks and tabs 403 | } 404 | else { 405 | $inboxForwardRuleDescription = $inboxForwardRule.description.replace("`r`n", " ").replace("`t", "") # delete line breaks and tabs 406 | } 407 | 408 | $recipientConverted = Translate-Recipient -Recipient $forwardAsAttachmentTo 409 | 410 | $object = [PSCustomObject][ordered]@{ 411 | Identity = $mailbox.Identity 412 | Name = $mailbox.Name 413 | DisplayName = $mailbox.DisplayName 414 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 415 | UserPrincipalName = $mailbox.UserPrincipalName 416 | ForwardingAddressConverted = $recipientConverted 417 | ForwardType = 'InboxRule' 418 | ForwardScope = $forwardingScope 419 | Precedence = $precedence 420 | ForwardingAddress = '-' 421 | ForwardingSMTPAddress = '-' 422 | ForwardingWorks = 'Not evaluated yet (check precedence and InboxRuleEnabled and forward address)' 423 | DeliverToMailboxAndForward = '-' 424 | InboxRulePriority = $inboxForwardRule.Priority 425 | InboxRuleEnabled = $inboxForwardRule.Enabled 426 | InboxRuleForwardAddressConverted = $recipientConverted 427 | InboxRuleRedirectTo = '-' 428 | InboxRuleForwardTo = '-' 429 | InboxRuleForwardAsAttachmentTo = $forwardAsAttachmentTo 430 | InboxRuleSendTextMessageNotificationTo = '-' 431 | InboxRuleDescription = $inboxForwardRuleDescription 432 | } 433 | 434 | #Add object to an array 435 | #$forwardList.Add($object) 436 | $inboxForwardList.Add($object) 437 | } 438 | 439 | foreach ($redirectTo in $inboxForwardRule.RedirectTo) { 440 | 441 | if ($ExchangeOnPremise) { 442 | $inboxForwardRuleDescription = $inboxForwardRule.description.ToString().replace("`r`n", " ").replace("`t", "") # delete line breaks and tabs 443 | } 444 | else { 445 | $inboxForwardRuleDescription = $inboxForwardRule.description.replace("`r`n", " ").replace("`t", "") # delete line breaks and tabs 446 | } 447 | 448 | $recipientConverted = Translate-Recipient -Recipient $redirectTo 449 | 450 | $object = [PSCustomObject][ordered]@{ 451 | Identity = $mailbox.Identity 452 | Name = $mailbox.Name 453 | DisplayName = $mailbox.DisplayName 454 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 455 | UserPrincipalName = $mailbox.UserPrincipalName 456 | ForwardingAddressConverted = $recipientConverted 457 | ForwardType = 'InboxRule' 458 | ForwardScope = $forwardingScope 459 | Precedence = $precedence 460 | ForwardingAddress = '-' 461 | ForwardingSMTPAddress = '-' 462 | ForwardingWorks = 'Not evaluated yet (check precedence and InboxRuleEnabled and forward address)' 463 | DeliverToMailboxAndForward = '-' 464 | InboxRulePriority = $inboxForwardRule.Priority 465 | InboxRuleEnabled = $inboxForwardRule.Enabled 466 | InboxRuleForwardAddressConverted = $recipientConverted 467 | InboxRuleRedirectTo = $redirectTo 468 | InboxRuleForwardTo = '-' 469 | InboxRuleForwardAsAttachmentTo = '-' 470 | InboxRuleSendTextMessageNotificationTo = '-' 471 | InboxRuleDescription = $inboxForwardRuleDescription 472 | } 473 | 474 | #Add object to an array 475 | #$forwardList.Add($object) 476 | $inboxForwardList.Add($object) 477 | } 478 | 479 | foreach ($sendTextMessageNotificationTo in $inboxForwardRule.SendTextMessageNotificationTo) { 480 | 481 | if ($ExchangeOnPremise) { 482 | $sendTextMessageNotificationToDescription = $sendTextMessageNotificationTo.Description.ToString().("`r`n", " ").replace("`t", "") # delete line breaks and tabs 483 | } 484 | else { 485 | $sendTextMessageNotificationToDescription = $sendTextMessageNotificationTo.Description.replace("`r`n", " ").replace("`t", "") # delete line breaks and tabs 486 | } 487 | 488 | $forwardingScope = 'External' 489 | 490 | $object = [PSCustomObject][ordered]@{ 491 | Identity = $mailbox.Identity 492 | Name = $mailbox.Name 493 | DisplayName = $mailbox.DisplayName 494 | PrimarySmtpAddress = $mailbox.PrimarySmtpAddress 495 | UserPrincipalName = $mailbox.UserPrincipalName 496 | ForwardingAddressConverted = $sendTextMessageNotificationTo 497 | ForwardType = 'InboxRule' 498 | ForwardScope = $forwardingScope 499 | InboxRulePriority = $inboxForwardRule.Priority 500 | InboxRuleEnabled = $inboxForwardRule.Enabled 501 | InboxRuleForwardAddressConverted = $temp 502 | InboxRuleRedirectTo = '-' 503 | InboxRuleForwardTo = '-' 504 | InboxRuleForwardAsAttachmentTo = '-' 505 | InboxRuleSendTextMessageNotificationTo = $sendTextMessageNotificationTo 506 | InboxRuleDescription = $sendTextMessageNotificationToDescription 507 | } 508 | 509 | #Add object to an array 510 | #$forwardList.Add($object) 511 | $inboxForwardList.Add($object) 512 | } 513 | } 514 | } 515 | } 516 | 517 | $inboxForwardList | ForEach-Object { 518 | $forwardList.Add($_) 519 | } 520 | 521 | Write-Host -ForegroundColor cyan "$($forwardList.count) forward(s) found" 522 | 523 | $forwardList | ForEach-Object { 524 | if ((($_.ForwardingAddressConverted -like '*@*') -and -not($internalDomains -contains $_.ForwardingAddressConverted.split('@')[1])) -or (($_.ForwardingSMTPAddress -like '*@*') -and -not($internalDomains -contains $_.ForwardingSMTPAddress.split('@')[1])) -or (($_.InboxRuleForwardAddressConverted -like '*@*') -and -not($internalDomains -contains $_.InboxRuleForwardAddressConverted.split('@')[1]))) { 525 | $_.ForwardScope = 'External' 526 | } 527 | else { 528 | $_.ForwardScope = 'Internal' 529 | } 530 | } 531 | 532 | if ($ExportResults) { 533 | $filepath = "$($env:temp)\$(Get-Date -format yyyyMMdd_hhmm)_forward.csv" 534 | Write-Host -ForegroundColor green "Export results to $filepath" 535 | 536 | $forwardList | Export-CSV -NoTypeInformation -Encoding UTF8 $filepath 537 | 538 | Invoke-Item $filepath 539 | } 540 | else { 541 | return $forwardList 542 | } 543 | } -------------------------------------------------------------------------------- /Exchange/Set-ExoCalendarDefaultPermissions.ps1: -------------------------------------------------------------------------------- 1 | # permission list : https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights 2 | <# 3 | The following individual permissions are available: 4 | 5 | None: The user has no access to view or interact with the folder or its contents. 6 | CreateItems: The user can create items in the specified folder. 7 | CreateSubfolders: The user can create subfolders in the specified folder. 8 | DeleteAllItems: The user can delete all items in the specified folder. 9 | DeleteOwnedItems: The user can only delete items that they created from the specified folder. 10 | EditAllItems: The user can edit all items in the specified folder. 11 | EditOwnedItems: The user can only edit items that they created in the specified folder. 12 | FolderContact: The user is the contact for the specified public folder. 13 | FolderOwner: The user is the owner of the specified folder. The user can view the folder, move the folder, and create subfolders. The user can't read items, edit items, delete items, or create items. 14 | FolderVisible: The user can view the specified folder, but can't read or edit items within the specified public folder. 15 | ReadItems: The user can read items within the specified folder. 16 | The roles that are available, along with the permissions that they assign, are described in the following list: 17 | 18 | Author: CreateItems, DeleteOwnedItems, EditOwnedItems, FolderVisible, ReadItems 19 | Contributor: CreateItems, FolderVisible 20 | Editor: CreateItems, DeleteAllItems, DeleteOwnedItems, EditAllItems, EditOwnedItems, FolderVisible, ReadItems 21 | NonEditingAuthor: CreateItems, DeleteOwnedItems, FolderVisible, ReadItems 22 | Owner: CreateItems, CreateSubfolders, DeleteAllItems, DeleteOwnedItems, EditAllItems, EditOwnedItems, FolderContact, FolderOwner, FolderVisible, ReadItems 23 | PublishingAuthor: CreateItems, CreateSubfolders, DeleteOwnedItems, EditOwnedItems, FolderVisible, ReadItems 24 | PublishingEditor: CreateItems, CreateSubfolders, DeleteAllItems, DeleteOwnedItems, EditAllItems, EditOwnedItems, FolderVisible, ReadItems 25 | Reviewer: FolderVisible, ReadItems 26 | #> 27 | 28 | $mbxs = Get-Mailbox -ResultSize unlimited -RecipientTypeDetails Usermailbox 29 | 30 | #$permission = 'Reviewer' 31 | $permission = 'LimitedDetails' 32 | 33 | foreach ($mbx in $mbxs) { 34 | $calFolders = @($mbx | Get-MailboxFolderStatistics -FolderScope Calendar | Where-Object { $_.FolderType -eq 'Calendar' } ) 35 | 36 | $defaultCalendar = $calFolders | Where-Object ContainerClass -eq '' 37 | 38 | Write-Host -ForegroundColor Cyan "Add $permission permissions for $($mbx.alias):\$($defaultCalendar.Name)" 39 | Set-MailboxFolderPermission -Identity "$($mbx.alias):\$($defaultCalendar.Name)" -User Default -AccessRights $permission 40 | } -------------------------------------------------------------------------------- /Exchange/Set-RemoteDomainWithExternalForward.ps1: -------------------------------------------------------------------------------- 1 | # I had problems sending to an address with an external forward, even with this remotedomain parameter. solved when I deleted the base address in the OWA cache (auto-complete list). 2 | # I don't know if it's related or just a waiting time... 3 | # and it doesn't work anymore... in my opinion there's a waiting time 4 | # and works again after a waiting time... 5 | $domain = Read-Host 'Domain name?' 6 | 7 | Get-HostedOutboundSpamFilterPolicy | Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'On' 8 | 9 | # Automatic transfer blocked by default 10 | Set-RemoteDomain Default -AutoForwardEnabled $false 11 | 12 | # New domain creation and automatic transfer authorization 13 | New-RemoteDomain -DomainName $domain -Name $domain 14 | Set-RemoteDomain -Identity $domain -AutoForwardEnabled $true -------------------------------------------------------------------------------- /Get-Office365TenantID.ps1: -------------------------------------------------------------------------------- 1 | # Another great way to get tenant ID from domain: https://www.whatismytenantid.com/result 2 | Param( 3 | [string]$Domain 4 | ) 5 | 6 | (Invoke-WebRequest https://login.windows.net/$domain/.well-known/openid-configuration|ConvertFrom-Json).Token_Endpoint.Split("/")[3] -------------------------------------------------------------------------------- /Intune/CMTrace/CMTrace.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastienperez/Microsoft365-Toolbox/2a803dcb81564a8753bcdfbbe2354e164344221d/Intune/CMTrace/CMTrace.exe -------------------------------------------------------------------------------- /Intune/CMTrace/archives/CMTrace 5.0.9078.1000.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastienperez/Microsoft365-Toolbox/2a803dcb81564a8753bcdfbbe2354e164344221d/Intune/CMTrace/archives/CMTrace 5.0.9078.1000.exe -------------------------------------------------------------------------------- /Intune/CMTrace/archives/CMTrace 5.00.7804.1000.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastienperez/Microsoft365-Toolbox/2a803dcb81564a8753bcdfbbe2354e164344221d/Intune/CMTrace/archives/CMTrace 5.00.7804.1000.exe -------------------------------------------------------------------------------- /Intune/CMTrace/readme.md: -------------------------------------------------------------------------------- 1 | CMtrace tool extracted from https://www.microsoft.com/en-us/evalcenter/download-microsoft-endpoint-configuration-manager > `SMSSETUP\TOOLS` folder -------------------------------------------------------------------------------- /Licenses/Download-LicensesFriendlyNames.ps1: -------------------------------------------------------------------------------- 1 | # the CSV url is from https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference 2 | # not sure if the URL is always the same 3 | 4 | $url = 'https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv' 5 | 6 | # download the CSV file 7 | Invoke-RestMethod -Uri $url -OutFile $PSScriptRoot\LicensesFriendlyName.csv -------------------------------------------------------------------------------- /Licenses/Get-MsolSub.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | source : https://o365reports.com/2020/03/04/export-office-365-license-expiry-date-report-powershell/= 3 | #> 4 | function Get-MsolSub { 5 | [CmdletBinding()] 6 | param ( 7 | ) 8 | 9 | [System.Collections.Generic.List[PSObject]]$subscriptionsArray = @() 10 | 11 | $friendlyNameHash = @{} 12 | 13 | Import-Csv -Path "$PSScriptRoot\LicensesFriendlyName.csv" -ErrorAction Stop -Delimiter ',' | Select-Object String_Id, Product_Display_Name -Unique | ForEach-Object { 14 | $friendlyNameHash.Add($_.String_Id, $_.Product_Display_Name) 15 | } 16 | 17 | # Get available subscriptions in the tenant 18 | $subscriptions = Get-MsolSubscription 19 | 20 | foreach ($subscription in $subscriptions) { 21 | # Determine subscription type 22 | $subscriptionType = if ($subscription.IsTrial) { 'Trial' } 23 | elseif ($subscription.SKUPartNumber -like '*Free*' -or $null -eq $subscription.NextLifeCycleDate) { 'Free' } 24 | else { 'Purchased' } 25 | 26 | # Friendly Expiry Date 27 | $expiryDate = $subscription.NextLifeCycleDate 28 | $friendlyExpiryDate = if ($null -ne $expiryDate) { 29 | $daysToExpiry = (New-TimeSpan -Start (Get-Date) -End $expiryDate).Days 30 | switch ($subscription.Status) { 31 | 'Enabled' { "Will expire in $daysToExpiry days" } 32 | 'Warning' { "Expired. Will suspend in $daysToExpiry days" } 33 | 'Suspended' { "Expired. Will delete in $daysToExpiry days" } 34 | 'LockedOut' { 'Subscription is locked. Please contact Microsoft' } 35 | } 36 | } 37 | else { 38 | 'Never Expires' 39 | } 40 | 41 | # Creating custom object for each subscription 42 | $object = [PSCustomObject][ordered]@{ 43 | 'Subscription Name' = $subscription.SKUPartNumber 44 | 'Friendly Subscription Name' = $friendlyNameHash[$subscription.SKUPartNumber] 45 | 'Subscribed Date' = $subscription.DateCreated 46 | 'Total Licenses' = $subscription.TotalLicenses 47 | 'Subscription Type' = $subscriptionType 48 | 'License Expiry Date/Next LifeCycle Activity Date' = $expiryDate 49 | 'Friendly Expiry Date' = $friendlyExpiryDate 50 | 'Status' = $subscription.Status 51 | } 52 | 53 | $subscriptionsArray.Add($object) 54 | } 55 | 56 | # Return the results 57 | return $subscriptionsArray 58 | } -------------------------------------------------------------------------------- /Licenses/LicensesFriendlyName.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastienperez/Microsoft365-Toolbox/2a803dcb81564a8753bcdfbbe2354e164344221d/Licenses/LicensesFriendlyName.csv -------------------------------------------------------------------------------- /M365Groups-Teams/Get-Microsoft365GroupsDetails.ps1: -------------------------------------------------------------------------------- 1 | function Get-Microsoft365GroupsDetails { 2 | 3 | <# 4 | Connect-ExchangeOnline 5 | 6 | Connect-MicrosoftTeams 7 | 8 | Import-Module AzureADPreview 9 | Connect-AzureAD 10 | 11 | $domain = (Get-AzureADDomain | Where-Object {$_.IsInitial}).Name 12 | $domain = $domain.replace('.onmicrosoft.com','') 13 | 14 | Connect-SPOService https://$domain-admin.sharepoint.com 15 | 16 | #> 17 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 18 | 19 | [System.Collections.Generic.List[PSObject]]$data = @() 20 | 21 | 22 | Write-Host 'Get Azure AD Users' -ForegroundColor Cyan 23 | $AllAADUsers = Get-AzureADUser -All:$true -ErrorAction SilentlyContinue 24 | 25 | $allAADUsersHash = @{} 26 | 27 | $AllAADUsers | ForEach-Object { 28 | $allAADUsersHash.Add($_.UserPrincipalName, $_.UserType) 29 | } 30 | 31 | <#$hashtable all users and guest 32 | $AllMemberUsers = @{} 33 | $AllAADUsers | Where-Object { $_.UserType -eq 'Member' -or $null -eq $_.UserType } | ForEach-Object { $AllMemberUsers.Add($_.UserPrincipalName, 'Member') } 34 | 35 | $AllGuestUsers = @{} 36 | $AllAADUsers | Where-Object { $_.UserType -eq 'Guest' } | ForEach-Object { 37 | if ($null -eq $_.mail) { 38 | $AllGuestUsers.Add($_.UserPrincipalName, 'Guest') 39 | } 40 | else { 41 | $AllGuestUsers.Add($_.mail, 'Guest') 42 | } 43 | } 44 | #> 45 | Write-Host 'Get Microsoft 365 Groups' -ForegroundColor Cyan 46 | $groups = Get-Recipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited | Sort-Object DisplayName 47 | 48 | Write-Host 'Get Microsoft 365 Teams' -ForegroundColor Cyan 49 | $teams = Get-Team -NumberOfThreads 20 50 | 51 | $TeamsList = @{} 52 | $teams | ForEach-Object { $TeamsList.Add($_.GroupId, $_.DisplayName) } 53 | 54 | Write-Host 'Get SharePoint Sites' -ForegroundColor Cyan 55 | $allSPOSites = Get-SPOSite -Limit All | Select-Object Url, SharingCapability 56 | 57 | $hashSPOSites = @{} 58 | $allSPOSites | ForEach-Object { 59 | $hashSPOSites.Add($_.Url, $_.SharingCapability) 60 | } 61 | 62 | $defaultAADSettings = (Get-AzureADDirectorySetting | Where-Object { $_.displayname -eq 'Group.Unified' }).Values 63 | 64 | if (($defaultAADSettings | Where-Object { $_.Name -eq 'EnableGroupCreation' }) -eq $false) { 65 | $unifiedGroupCreationAllowed = 'false (from default settings)' 66 | } 67 | else { 68 | # true or empty means that all users can create unified groups (default configuration)) 69 | $unifiedGroupCreationAllowed = 'true (from default settings)' 70 | } 71 | 72 | if (($defaultAADSettings | Where-Object { $_.Name -eq 'AllowGuestsToAccessGroups' }) -eq $false) { 73 | $AllowGuestsToAccessGroups = 'false (from default settings)' 74 | } 75 | else { 76 | $AllowGuestsToAccessGroups = 'true (from default settings)' 77 | } 78 | 79 | if (($defaultAADSettings | Where-Object { $_.Name -eq 'AllowGuestsToBeGroupOwner' }) -eq $false) { 80 | $AllowGuestsToBeGroupOwner = 'false (from default settings)' 81 | } 82 | else { 83 | $AllowGuestsToBeGroupOwner = 'true (from default settings)' 84 | } 85 | 86 | if (($defaultAADSettings | Where-Object { $_.Name -eq 'AllowToAddGuests' }) -eq $false) { 87 | $AllowToAddGuests = 'false (from default settings)' 88 | } 89 | else { 90 | $AllowToAddGuests = 'true (from default settings)' 91 | } 92 | 93 | $i = 0 94 | Write-Host 'Gather information about Teams/Microsoft 365 groups' -ForegroundColor Cyan 95 | foreach ($group in $groups) { 96 | $i++ 97 | 98 | #[string]$channelsNames = [string]$teamOwnersEmails = [string]$uGroupMembersEmail = [string]$teamGuestsEmails = [string]$uGroupOwnersEmail = '' 99 | [System.Collections.Generic.List[PSObject]]$channelsNames = @() 100 | [System.Collections.Generic.List[PSObject]]$uGroupMembersEmail = @() 101 | [System.Collections.Generic.List[PSObject]]$uGroupOwnersEmail = @() 102 | 103 | $teamEnabled = $false 104 | $team = $null 105 | $NumberofChats = $null 106 | $LastItemAddedtoTeams = $null 107 | 108 | Write-Host "Get details for $($group.Name) - $i of $($groups.count)" -ForegroundColor Cyan 109 | 110 | $uGroup = Get-UnifiedGroup -Identity $group.DistinguishedName 111 | 112 | $uGroupMembers = Get-UnifiedGroupLinks -Identity $uGroup.DistinguishedName -LinkType Members 113 | 114 | foreach ($uMember in $uGroupMembers) { 115 | if ($uMember.PrimarySmtpAddress) { 116 | $uMemberStr = $uMember.PrimarySmtpAddress 117 | 118 | } 119 | elseif ($uMember.DisplayName) { 120 | $uMemberStr = $uMember.DisplayName 121 | } 122 | else { 123 | $uMemberStr = $uMember.Name 124 | } 125 | 126 | $uGroupMembersEmail.Add($uMemberStr) 127 | } 128 | 129 | $uGroupOwners = Get-UnifiedGroupLinks -Identity $uGroup.DistinguishedName -LinkType Owners 130 | 131 | foreach ($uOwner in $uGroupOwners) { 132 | if ($uOwner.PrimarySmtpAddress) { 133 | $uOwnerStr = $uOwner.PrimarySmtpAddress 134 | } 135 | elseif ($uOwner.DisplayName) { 136 | $uOwnerStr = $uOwner.DisplayName 137 | } 138 | else { 139 | $uOwnerStr = $uOwner.Name 140 | } 141 | 142 | $uGroupOwnersEmail.Add($uOwnerStr) 143 | } 144 | 145 | 146 | # If Team-Enabled, we can find the date of the last chat compliance record 147 | if ($TeamsList.ContainsKey($uGroup.ExternalDirectoryObjectId)) { 148 | $teamEnabled = $True 149 | 150 | $teamChatData = (Get-MailboxFolderStatistics -Identity $uGroup.PrimarySmtpAddress -IncludeOldestAndNewestItems -FolderScope ConversationHistory) 151 | 152 | if ($teamChatData.ItemsInFolder[1] -ne 0) { 153 | $LastItemAddedtoTeams = $teamChatData.NewestItemReceivedDate[1] 154 | $NumberofChats = $teamChatData.ItemsInFolder[1] 155 | if ($teamChatData.NewestItemReceivedDate -le $WarningEmailDate) { 156 | Write-Host "Team-enabled group $($ugroup.DisplayName) has only $($teamChatData.ItemsInFolder[1]) compliance record(s)" 157 | } 158 | } 159 | 160 | $channelsList = Get-TeamChannel -GroupId $uGroup.ExternalDirectoryObjectId 161 | 162 | foreach ($channel in $channelsList) { 163 | $channelsNames.Add($channel.DisplayName) 164 | } 165 | 166 | 167 | $teamsUsers = Get-TeamUser -GroupId $uGroup.ExternalDirectoryObjectId 168 | $teamsMember = $teamsUsers | Where-Object { $_.Role -eq 'member' } 169 | $teamsGuest = $teamsUsers | Where-Object { $_.Role -eq 'guest' } 170 | $teamsOwners = $teamsUsers | Where-Object { $_.Role -eq 'owner' } 171 | 172 | $teamAllowToAddGuests = $null 173 | $teamAllowAddGuestsToAccessGroups = $null 174 | $teamGuestSettings = $null 175 | 176 | $teamGuestSettings = Get-AzureADObjectSetting -TargetType groups -TargetObjectId $uGroup.ExternalDirectoryObjectId 177 | 178 | $team = Get-Team -GroupID $uGroup.ExternalDirectoryObjectId 179 | 180 | if ($null -ne $teamGuestSettings) { 181 | 182 | # https://learn.microsoft.com/en-us/entra/identity/users/groups-settings-cmdlets#update-settings-for-a-specific-group 183 | $guestSettings = ((Get-AzureADObjectSetting -TargetType groups -TargetObjectId $uGroup.ExternalDirectoryObjectId).ToJson() | ConvertFrom-Json).Values 184 | 185 | foreach ($guestSetting in $guestSettings) { 186 | 187 | switch ($guestSetting.Name) { 188 | AllowToAddGuests { 189 | if ($null -eq $guestSettings.value) { 190 | $teamAllowToAddGuests = $AllowToAddGuests 191 | } 192 | elseif ($guestSettings.value -eq 'false') { 193 | $teamAllowToAddGuests = 'True' 194 | } 195 | else { 196 | $teamAllowToAddGuests = 'True' 197 | } 198 | 199 | break 200 | } 201 | AllowGuestsToAccessGroups { 202 | if ($null -eq $guestSettings.value) { 203 | $teamAllowAddGuestsToAccessGroups = $AllowGuestsToAccessGroups 204 | } 205 | elseif ($guestSettings.value -eq 'false') { 206 | $teamAllowAddGuestsToAccessGroups = 'False' 207 | } 208 | else { 209 | $teamAllowAddGuestsToAccessGroups = 'True' 210 | } 211 | 212 | break 213 | } 214 | AllowGuestsToBeGroupOwner { 215 | if ($null -eq $guestSettings.value) { 216 | $teamAllowGuestsToBeGroupOwner = $AllowGuestsToBeGroupOwner 217 | } 218 | elseif ($guestSettings.value -eq 'false') { 219 | $teamAllowGuestsToBeGroupOwner = 'False' 220 | } 221 | else { 222 | $teamAllowGuestsToBeGroupOwner = 'True' 223 | } 224 | 225 | break 226 | } 227 | } 228 | } 229 | } 230 | else { 231 | $teamAllowToAddGuests = $AllowToAddGuests 232 | $teamAllowAddGuestsToAccessGroups = $AllowGuestsToAccessGroups 233 | $teamAllowGuestsToBeGroupOwner = $AllowGuestsToBeGroupOwner 234 | } 235 | } 236 | #Team name TeamMail Channels MembersCount OwnersCount GuestsCount Privacy 237 | 238 | $object = [PSCustomObject][ordered] @{ 239 | GroupID = $uGroup.ExternalDirectoryObjectId 240 | GroupTeamMainMail = $uGroup.PrimarySmtpAddress 241 | GroupTeamAllMailAddresses = $uGroup.EmailAddresses -split ',' -join '|' 242 | GroupHiddenfromOutlook = $uGroup.HiddenFromExchangeClientsEnabled 243 | GroupAccessType = $uGroup.AccessType 244 | GroupExternalMemberCount = $uGroup.GroupExternalMemberCount 245 | GroupName = $uGroup.DisplayName 246 | GroupDescription = $uGroup.Description 247 | GroupCreationUTCTime = $uGroup.WhenCreatedUTC 248 | SharePointSiteURL = if ($uGroup.SharePointSiteUrl) { $uGroup.SharePointSiteUrl }else { '-' } 249 | SharePointDocumentsURL = if ($uGroup.SharePointDocumentsUrl) { $uGroup.SharePointDocumentsUrl }else { '-' } 250 | # SharePointSiteUrl can be empty (exemple of allcompany group) 251 | SharePointSiteSharingCapability = if ($uGroup.SharePointSiteUrl) { $hashSPOSites[$uGroup.SharePointSiteUrl] }else { '-' } 252 | TeamEnabled = $teamEnabled 253 | TeamStandardChannelCount = ($channelsList | Where-Object { $_.MembershipType -eq 'Standard' } | Measure-Object).Count 254 | TeamPrivateChannelCount = ($channelsList | Where-Object { $_.MembershipType -eq 'Private' } | Measure-Object).Count 255 | TeamSharedChannelCount = ($channelsList | Where-Object { $_.MembershipType -eq 'Shared' } | Measure-Object).Count 256 | TeamChannelsNames = $channelsNames -join '|' 257 | LastItemAddedtoTeams = $LastItemAddedtoTeams 258 | NumberofChats = $NumberofChats 259 | uGroupMembersCount = $uGroupMembersEmail.count 260 | uGroupMembersEmail = $uGroupMembersEmail -join '|' 261 | TeamOwnersCount = $teamsOwners.Count 262 | TeamOwnersEmails = $teamsOwners.User -join '|' 263 | TeamMemberCount = $teamsMember.Count 264 | TeamMemberEmails = $teamsMember.User -join '|' 265 | TeamGuestCount = $teamsGuest.Count 266 | TeamGuestEmails = $teamsGuest.User -join '|' 267 | TeamAllUsersCount = $teamsUsers.Count 268 | TeamAllUsersEmails = $teamsUsers.User -join '|' 269 | TeamAllowToAddGuests = $teamAllowToAddGuests 270 | TeamAllowGuestsToBeGroupOwner = $teamAllowGuestsToBeGroupOwner 271 | TeamAllowGuestsToAccessGroups = $teamAllowAddGuestsToAccessGroups 272 | TeamMemberSettingsAllowCreateUpdateChannels = $team.AllowCreateUpdateChannels 273 | TeamMemberSettingsAllowDeleteChannels = $team.AllowDeleteChannels 274 | TeamMemberSettingsAllowAddRemoveApps = $team.AllowAddRemoveApps 275 | TeamMemberSettingsAllowCreateUpdateRemoveTabs = $team.AllowCreateUpdateRemoveTabs 276 | TeamMemberSettingsAllowCreateUpdateRemoveConnectors = $team.AllowCreateUpdateRemoveConnectors 277 | TeamMessagingSettingsAllowUserEditMessages = $team.AllowUserEditMessages 278 | TeamMessagingSettingsAllowUserDeleteMessages = $team.AllowUserDeleteMessages 279 | TeamMessagingSettingsAllowOwnerDeleteMessages = $team.AllowOwnerDeleteMessages 280 | TeamMessagingSettingsAllowTeamMentions = $team.AllowTeamMentions 281 | TeamMessagingSettingsAllowChannelMentions = $team.AllowChannelMentions 282 | TeamGuestSettingsAllowCreateUpdateChannels = $team.AllowCreateUpdateChannels 283 | TeamGuestSettingsAllowDeleteChannels = $team.AllowDeleteChannels 284 | TeamFunSettingsAllowGiphy = $team.AllowGiphy 285 | TeamFunSettingsGiphyContentRating = $team.GiphyContentRating 286 | TeamFunSettingsAllowStickersAndMemes = $team.AllowStickersAndMemes 287 | TeamFunSettingsAllowCustomMemes = $team.AllowCustomMemes 288 | TeamChannelsCount = $channelsList.Count 289 | UnifiedGroupWelcomeMessageEnabled = $uGroup.WelcomeMessageEnabled 290 | } 291 | 292 | $data.Add($object) 293 | } 294 | 295 | return $data 296 | } -------------------------------------------------------------------------------- /MessageCenter/Get-M365MessageCenterMessages.ps1: -------------------------------------------------------------------------------- 1 | function Get-M365MessageCenterMessages { 2 | [CmdLetbinding()] 3 | param( 4 | [Parameter(Mandatory = $true)] 5 | [String]$ClientID, 6 | [Parameter(Mandatory = $true)] 7 | [String]$ClientSecret, 8 | [Parameter(Mandatory = $true)] 9 | [String]$TenantDomain 10 | ) 11 | 12 | $body = @{ 13 | grant_type = 'client_credentials' 14 | resource = 'https://graph.microsoft.com' 15 | client_id = $ClientID 16 | client_secret = $ClientSecret 17 | earliest_time = "-$($Hours)h@s" 18 | } 19 | 20 | $oauth = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$($TenantDomain)/oauth2/token?api-version=1.0" -Body $body 21 | $headerParams = @{'Authorization' = "$($oauth.token_type) $($oauth.access_token)" } 22 | 23 | try { 24 | $allMessages = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages' -Headers $headerParams -Method GET -ErrorAction Stop 25 | } 26 | catch { 27 | Write-Warning "$($_.Exception.Message -replace "`n", ' ' -replace "`r", ' ')" 28 | 29 | return 30 | } 31 | 32 | $output = @{ } 33 | $output.MessageCenterInformation = foreach ($Message in $AllMessages.Value) { 34 | 35 | [PSCustomObject]@{ 36 | Id = $Message.Id 37 | Title = $Message.Title 38 | Service = $Message.services 39 | LastUpdatedTime = if ($Message.lastModifiedDateTime) { [DateTime]::Parse($Message.lastModifiedDateTime.ToString()) } else { $null } 40 | LastUpdatedDays = if ($Message.lastModifiedDateTime) { ((Get-Date).Subtract([DateTime]::Parse($Message.lastModifiedDateTime.ToString()))).Days } else { $null } 41 | ActionRequiredByDateTime = if ( $Message.actionRequiredByDateTime) { [DateTime]::Parse($Message.actionRequiredByDateTime.ToString()) } else { $null } 42 | RoadmapId = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value 43 | Category = $Message.category 44 | } 45 | } 46 | 47 | $output.MessageCenterInformationExtended = foreach ($Message in $AllMessages.Value) { 48 | [PSCustomObject] @{ 49 | Id = $Message.Id 50 | Title = $Message.Title 51 | Service = $Message.services 52 | LastUpdatedTime = if ($Message.lastModifiedDateTime) { [DateTime]::Parse($Message.lastModifiedDateTime.ToString()) } else { $null } 53 | LastUpdatedDays = if ($Message.lastModifiedDateTime) { ((Get-Date).Subtract([DateTime]::Parse($Message.lastModifiedDateTime.ToString()))).Days } else { $null } 54 | ActionRequiredByDateTime = if ($Message.actionRequiredByDateTime) { [DateTime]::Parse($Message.actionRequiredByDateTime.ToString()) } else { $null } 55 | Tags = $Message.Tags 56 | Bloglink = ($Message.details | Where-Object { $_.name -eq 'bloglink' }).Value 57 | RoadmapId = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value 58 | RoadmapIdLinks = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value | ForEach-Object { 59 | "https://www.microsoft.com/en-us/microsoft-365/roadmap?filters=&searchterms=$_" 60 | } 61 | Category = $Message.category 62 | IsMajorChange = $Message.isMajorChange 63 | Severity = $Message.Severity 64 | StartTime = If ($Message.startDateTime) { [DateTime]::Parse($Message.startDateTime.ToString()) } else { $null } 65 | EndTime = if ($Message.endDateTime) { [DateTime]::Parse($Message.endDateTime.ToString()) } else { $null } 66 | Message = $Message.body.content 67 | } 68 | } 69 | 70 | $output 71 | } -------------------------------------------------------------------------------- /MessageCenter/New-EntraIDAppWithMessageCenterRead.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .synopsis 3 | Creates a new Entra ID Application with Message Center Read permissions 4 | .DESCRIPTION 5 | This script creates a new Entra ID Application with Message Center Read permissions. 6 | The script will create a new application registration in Microosft Entra ID, create a client secret, and assign the required permissions. 7 | The script will output the Client ID, Tenant ID, and Client Secret. 8 | This script will grant admin consent for the application (application permissions, not delegated permissions). 9 | The script will also provide a URL to check admin consent for the application. 10 | .EXAMPLE 11 | .\New-EntraIDAppWithMessageCenterRead.ps1 -ApplicationName 'MessageCenterRead' 12 | .NOTES 13 | Bastien PEREZ 14 | #> 15 | 16 | function New-MessageCenterReadAppRegistration { 17 | [CmdletBinding()] 18 | param ( 19 | [Parameter(Mandatory = $false, HelpMessage = 'Provide a name for the Message Center Read Application. Example: MessageCenterRead')] 20 | [string]$ApplicationName = 'MessageCenter-Read' 21 | ) 22 | 23 | $scopes = 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All' 24 | Write-Host 'Connecting to Microsoft Graph with Scopes: $scopes' -ForegroundColor Cyan 25 | 26 | Connect-MgGraph -Scopes $scopes -NoWelcome 27 | 28 | $tenantDetail = Get-MgOrganization 29 | $tenantID = $tenantDetail.Id 30 | 31 | $appRegistrationParams = @{ 32 | displayName = $ApplicationName 33 | description = "App registration for $ApplicationName" 34 | isFallbackPublicClient = 'True' 35 | signInAudience = 'AzureADMyOrg' 36 | } 37 | 38 | Write-Host -ForegroundColor Cyan "Creating $ApplicationName app registration." 39 | try { 40 | $mgApp = New-MgApplication -BodyParameter $AppRegistrationParams -ErrorAction Stop 41 | Write-Host "Successfully created $ApplicationName app registration." -ForegroundColor Green 42 | } 43 | catch { 44 | throw "Failed to create $applicationName app registration: $($_.Exception.Message)" 45 | return 46 | } 47 | 48 | # Even if the application is created, the service principal is not created yet, so we need to create it 49 | Write-Host -ForegroundColor Cyan "Creating service principal for $ApplicationName app registration" 50 | try { 51 | $mgSP = New-MgServicePrincipal -AppId $mgApp.AppId -DisplayName $mgApp.DisplayName 52 | } 53 | catch { 54 | throw "Failed to create service principal for $ApplicationName app registration: $($_.Exception.Message)" 55 | return 56 | } 57 | 58 | # Create a client secret 59 | try { 60 | $passwordCredential = @{ 61 | displayName = "$ApplicationName-Client Secret" 62 | endDateTime = (Get-Date).AddMonths(12) 63 | } 64 | 65 | Write-Host -ForegroundColor Cyan "Creating client secret for $ApplicationName" 66 | $mgAppPassword = Add-MgApplicationPassword -ApplicationId $mgApp.Id -PasswordCredential $passwordCredential 67 | $clientSecret = $mgAppPassword.SecretText 68 | Write-Host 'Successfully created client secret.' -ForegroundColor Green 69 | } 70 | catch { 71 | throw "Failed to create client secret: $_" 72 | } 73 | 74 | Write-Host -ForegroundColor Cyan "Assigning permissions to $ApplicationName app registration." 75 | # Get the main Microsoft Graph service 76 | $msGraphId = '00000003-0000-0000-c000-000000000000' 77 | $msGraphSP = Get-MgServicePrincipal -Filter "AppId eq '$msGraphId'" 78 | 79 | # Creating the required permissions 80 | $permission = @{ 81 | ResourceAppId = $graphApiId 82 | ResourceAccess = @( 83 | @{ 84 | Id = ($graphServicePrincipal.AppRoles | Where-Object { $_.Value -eq 'ServiceMessage.Read.All' }).Id 85 | Type = 'Role' 86 | } 87 | ) 88 | } 89 | 90 | Write-Host -ForegroundColor Cyan "Adding required permissions to $ApplicationName app registration." 91 | Update-MgApplication -ApplicationId $mgApp.Id -RequiredResourceAccess @($permission) 92 | 93 | $msGraphApp = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" 94 | $appRole = $msGraphApp.AppRoles | Where-Object Value -EQ 'ServiceMessage.Read.All' 95 | 96 | Write-Host -ForegroundColor Cyan "Admin consent for $ApplicationName - Application permissions (not delegated)" 97 | New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mgSP.id -PrincipalId $mgSP.Id -ResourceId $msGraphSP.Id -AppRoleId $appRole.Id 98 | 99 | Write-Host 'Client ID:' -ForegroundColor Cyan 100 | Write-Host "$($mgApp.AppId)" 101 | Write-Host 'Tenant ID:' -ForegroundColor Cyan 102 | Write-Host "$($TenantId)" 103 | Write-Host 'Client Secret:' -ForegroundColor Cyan 104 | Write-Host "$($ClientSecret)" 105 | 106 | $url = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($mgApp.AppId)/isMSAApp~/false" 107 | Write-Warning "Check admin grant consent of this app on $url" 108 | } -------------------------------------------------------------------------------- /MicrosoftEntraID/Get-AzureADAppPermissions.ps1: -------------------------------------------------------------------------------- 1 | Function Get-AzureADAppPermissions { 2 | 3 | Write-Host 'Gathering information about Azure AD integrated applications...' -ForegroundColor Cyan 4 | 5 | try { 6 | $servicePrincipals = Get-AzureADservicePrincipal -All:$true | Where-Object {$_.Tags -eq 'WindowsAzureActiveDirectoryIntegratedApp'} 7 | } 8 | catch { 9 | Write-Host 'You must connect to Azure AD first' -ForegroundColor Red -ErrorAction Stop 10 | return 11 | } 12 | 13 | [System.Collections.Generic.List[PSObject]]$appPermissions = @() 14 | 15 | if (-not ($servicePrincipals.count -gt 0)) { 16 | Write-Host 'No application authorized' -ForegroundColor Cyan 17 | return 18 | } 19 | 20 | $i = 0 21 | foreach ($servicePrincipal in $servicePrincipals) { 22 | $i++ 23 | Write-Host "Processing $($servicePrincipal.DisplayName) [$i / $($servicePrincipals.count)]" 24 | $servicePrincipalPermission = Get-AzureADservicePrincipalOAuth2PermissionGrant -ObjectId $servicePrincipal.ObjectId -All:$true 25 | 26 | $OAuthperm = @{} 27 | [System.Collections.Generic.List[PSObject]]$assignedTo = @() 28 | 29 | $resID = $userId = $null; 30 | 31 | $valid = ($servicePrincipalPermission.ExpiryTime | Select-Object -Unique | Sort-Object -Descending | Select-Object -First 1) 32 | 33 | $object = [pscustomobject][ordered]@{ 34 | ApplicationName = $servicePrincipal.DisplayName 35 | ApplicationId = $servicePrincipal.AppId 36 | Publisher = $servicePrincipal.PublisherName 37 | Homepage = $servicePrincipal.Homepage 38 | ObjectId = $servicePrincipal.ObjectId 39 | Enabled = $servicePrincipal.AccountEnabled 40 | ValidUntil = $valid 41 | Permissions = '' 42 | AuthorizedBy = '' 43 | } 44 | 45 | $servicePrincipalPermission | ForEach-Object { #CAN BE DIFFERENT FOR DIFFERENT USERS! 46 | $resID = (Get-AzureADObjectByObjectId -ObjectIds $_.ResourceId).DisplayName 47 | if ($_.PrincipalId) { 48 | $userId = "(" + (Get-AzureADObjectByObjectId -ObjectIds $_.PrincipalId).UserPrincipalName + ')' 49 | } 50 | $OAuthperm["[" + $resID + $userId + "]"] = (($_.Scope.Trim().Split(" ") | Select-Object -Unique) -join ',') 51 | } 52 | 53 | $object.Permissions = (($OAuthperm.GetEnumerator() | ForEach-Object { "$($_.Name):$($_.Value)" }) -join ';') 54 | 55 | if (($servicePrincipalPermission.ConsentType | Select-Object -Unique) -eq 'AllPrincipals') { 56 | $assignedto.Add('All users (admin consent)') 57 | } 58 | try { 59 | $assignedto.Add((Get-AzureADObjectByObjectId -ObjectIds ($servicePrincipalPermission.PrincipalId | Select-Object -Unique)).UserPrincipalName) 60 | } 61 | catch { } 62 | 63 | $object.AuthorizedBy = $assignedto[0] -join ',' 64 | 65 | $appPermissions.Add($object) 66 | } 67 | 68 | return $appPermissions 69 | } -------------------------------------------------------------------------------- /MicrosoftEntraID/Get-DsregcmdStatus.ps1: -------------------------------------------------------------------------------- 1 | $dsregcmd = dsregcmd /status 2 | 3 | $object = New-Object -TypeName PSObject 4 | 5 | $dsregcmd | Select-String -Pattern " *[A-z]+ : *" | ForEach-Object { 6 | $object | Add-Member -MemberType NoteProperty -Name (([String]$_).Trim() -split ' : ')[0] -Value (([String]$_).Trim() -split ' : ')[1] 7 | } 8 | 9 | return $object -------------------------------------------------------------------------------- /MicrosoftEntraID/Get-LapsEntraIDPassword.ps1: -------------------------------------------------------------------------------- 1 | <#.SYNOPSIS 2 | Retrieves the LAPS password for a Microsoft Entra ID device. 3 | 4 | .DESCRIPTION 5 | Gets the Windows Local Administrator Password Solution (LAPS) password for a specified device in Microsoft Entra ID (formerly Azure AD). 6 | 7 | .PARAMETER DeviceID 8 | The Microsoft Entra ID (Azure AD) Device ID for which you want to retrieve the LAPS password. This is the unique identifier assigned to the device in Microsoft Entra ID. 9 | 10 | .EXAMPLE 11 | PS> Get-LapsEntraIDPassword -DeviceID "12345678-1234-1234-1234-123456789012" 12 | Retrieves the LAPS password for the specified device ID. 13 | 14 | .EXAMPLE 15 | PS> Get-LapsEntraIDPassword -DeviceID "12345678-1234-1234-1234-123456789012" -IncludePasswords 16 | Retrieves the LAPS password for the specified device ID, including the password itself as a secure string. 17 | 18 | .EXAMPLE 19 | PS> Get-LapsEntraIDPassword -DeviceID "12345678-1234-1234-1234-123456789012" -IncludePasswords -AsPlainText 20 | Retrieves the LAPS password for the specified device ID, including the password itself, and displays the password in plain text. 21 | 22 | .EXAMPLE 23 | PS> Get-LapsEntraIDPassword -DeviceID "12345678-1234-1234-1234-123456789012" -IncludePasswords -IncludeHistory 24 | Retrieves the LAPS password for the specified device ID, including the password itself, and includes the password history. 25 | 26 | .EXAMPLE 27 | PS> Get-LapsEntraIDPassword -DeviceID "12345678-1234-1234-1234-123456789012" -IncludePasswords -IncludeHistory -AsPlainText 28 | Retrieves the LAPS password for the specified device ID, including the password itself, includes the password history, and displays the password in plain text. 29 | 30 | .NOTES 31 | Requires appropriate permissions in Microsoft Entra ID to read LAPS passwords. 32 | This cmdlet is part of the Microsoft365-Toolbox module. 33 | 34 | #> 35 | 36 | function Get-LapsEntraIDPassword { 37 | param( 38 | [Parameter(Mandatory = $true, HelpMessage = 'The Microsoft Entra ID (Azure AD) Device ID for which you want to retrieve the LAPS password.')] 39 | [string]$DeviceID, 40 | [switch]$IncludePasswords, 41 | [switch]$AsPlainText, 42 | [switch]$IncludeHistory 43 | ) 44 | 45 | #Connect to Microsoft Graph 46 | #Connect-MgGraph -Scope DeviceLocalCredential.Read.All, Device.Read.All 47 | 48 | #Define your device name here 49 | #$DeviceName = '' 50 | #Store the device id value for your target device 51 | #$DeviceId = (Get-MgDevice -All | Where-Object { $_.DisplayName -eq $DeviceName } | Select-Object DeviceId).DeviceId 52 | 53 | #Define the URI path 54 | $uri = 'v1.0/directory/deviceLocalCredentials/' + $DeviceId 55 | # ?$select=credentials will cause the server to return all credentials, ie latest plus history 56 | 57 | if ($IncludePasswords.IsPresent) { 58 | $uri = $uri + '?$select=credentials' 59 | } 60 | 61 | #Generate a new correlation ID 62 | $correlationID = [System.Guid]::NewGuid() 63 | 64 | #Build the request header 65 | $headers = @{} 66 | $headers.Add('ocp-client-name', 'Get-LapsAADPassword Windows LAPS Cmdlet') 67 | $headers.Add('ocp-client-version', '1.0') 68 | $headers.Add('client-request-id', $correlationID) 69 | 70 | #Initation the request to Microsoft Graph for the LAPS password 71 | try { 72 | $response = Invoke-MgGraphRequest -Method GET -Uri $URI -Headers $headers -OutputType Json 73 | } 74 | catch { 75 | Write-Warning "Device ID: $DeviceId $($_.Exception.Message -replace "`n", ' ' -replace "`r", ' ')" 76 | $object = [PSCustomObject][ordered]@{ 77 | DeviceName = '$null' 78 | DeviceId = $deviceID 79 | PasswordExpirationTime = $null 80 | } 81 | 82 | return $object 83 | } 84 | 85 | if ([string]::IsNullOrWhitespace($response)) { 86 | $object = [PSCustomObject][ordered]@{ 87 | DeviceName = '$null' 88 | DeviceId = $deviceID 89 | PasswordExpirationTime = $null 90 | } 91 | 92 | return $object 93 | } 94 | 95 | # Build custom PS output object 96 | $resultsJson = ConvertFrom-Json $response 97 | 98 | $lapsDeviceId = $resultsJson.deviceName 99 | 100 | $lapsDeviceId = New-Object([System.Guid]) 101 | $lapsDeviceId = [System.Guid]::Parse($resultsJson.id) 102 | 103 | # Grab password expiration time (only applies to the latest password) 104 | $lapsPasswordExpirationTime = Get-Date $resultsJson.refreshDateTime 105 | 106 | if ($IncludePasswords) { 107 | # Copy the credentials array 108 | $credentials = $resultsJson.credentials 109 | 110 | # Sort the credentials array by backupDateTime. 111 | $credentials = $credentials | Sort-Object -Property backupDateTime -Descending 112 | 113 | # Note: current password (ie, the one most recently set) is now in the zero position of the array 114 | 115 | # If history was not requested, truncate the credential array down to just the latest one 116 | if (-not $IncludeHistory) { 117 | $credentials = @($credentials[0]) 118 | } 119 | 120 | foreach ($credential in $credentials) { 121 | 122 | # Cloud returns passwords in base64, convert: 123 | if ($AsPlainText) { 124 | $password = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($credential.passwordBase64)) 125 | } 126 | else { 127 | $bytes = [System.Convert]::FromBase64String($credential.passwordBase64) 128 | 129 | $plainText = [System.Text.Encoding]::UTF8.GetString($bytes) 130 | 131 | $password = ConvertTo-SecureString $plainText -AsPlainText -Force 132 | } 133 | 134 | $lapsPasswordExpirationTime = $null 135 | 136 | $object = [PSCustomObject][ordered]@{ 137 | DeviceName = $resultsJson.deviceName 138 | DeviceId = $lapsDeviceId 139 | Account = $credential.accountName 140 | Password = $password 141 | PasswordExpirationTime = $lapsPasswordExpirationTime 142 | PasswordUpdateTime = Get-Date $credential.backupDateTime 143 | } 144 | 145 | $object 146 | } 147 | } 148 | else { 149 | # Output a single object that just displays latest password expiration time 150 | # Note, $IncludeHistory is ignored even if specified in this case 151 | $object = [PSCustomObject][ordered]@{ 152 | DeviceName = $resultsJson.deviceName 153 | DeviceId = $lapsDeviceId 154 | PasswordExpirationTime = $lapsPasswordExpirationTime 155 | } 156 | 157 | $object 158 | } 159 | } -------------------------------------------------------------------------------- /Modules/Install-Microsoft365PShellModules.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Install Microsoft 365 PowerShell Prerequisites 4 | 5 | .DESCRIPTION 6 | Downloads and installs the Msolservice v1 (deprecated by Microsoft but always useful), Azure AD deprecated by Microsoft but always useful),, Sharepoint Online, Skype Online for Windows PowerShell, PNP, etc. 7 | You can choose the Azure AD in Azure AD Preview 8 | 9 | .AUTHOR 10 | 11 | .CREATION DATE 12 | 2019-12-13 13 | 14 | .LASTMODIFIED 15 | 2023-12-15 16 | 17 | #> 18 | 19 | # PowerShell 5.0 pour PowerShell Gallery 20 | #Requires -Version 5.0 21 | #Requires -RunAsAdministrator 22 | 23 | [CmdletBinding()] 24 | param ( 25 | [Parameter()] 26 | [Boolean]$AdvancedModules 27 | ) 28 | # Register PSGallery PSprovider and set as Trusted source 29 | Register-PSRepository -Default -ErrorAction SilentlyContinue 30 | Set-PSRepository -Name PSGallery -InstallationPolicy trusted -ErrorAction SilentlyContinue 31 | 32 | # Install modules from PSGallery 33 | $modules = @( 34 | # Microsoft Graph will replace AzureAD and MSOnline by March, 30 2024 https://techcommunity.microsoft.com/t5/microsoft-entra-blog/important-azure-ad-graph-retirement-and-powershell-module/ba-p/3848270 35 | 'PowershellGet', 36 | 'Microsoft.Graph' 37 | 'Microsoft.Graph.Beta', 38 | 'ExchangeOnlineManagement', 39 | 'MicrosoftTeams', 40 | 'Microsoft.Graph.Intune', 41 | 'Microsoft.Online.SharePoint.PowerShell', 42 | 'PnP.PowerShell', 43 | 'AzureADPreview', 44 | 'MSOnline' 45 | ) 46 | 47 | if ($AdvancedModules) { 48 | # DSCParser to export or compare 365 configuration 49 | $modules += 'DSCParser' 50 | 51 | #The MSAL.PS PowerShell module wraps MSAL.NET functionality into PowerShell-friendly cmdlets and is not supported by Microsoft. By Microsoft 52 | $modules += 'MSAL.PS' 53 | 54 | # Checks the current status of connections to (and as required, prompts for login to) various Microsoft Cloud platforms. By Microsoft 55 | $modules += 'MSCloudLoginAssistant' 56 | 57 | # Tools for managing, troubleshooting, and reporting on various aspects of Microsoft Identity products and services, primarily Azure AD. By Microsoft 58 | $modules += 'MSIdentityTools' 59 | } 60 | 61 | foreach ($module in $modules) { 62 | $currentVersion = $null 63 | 64 | # Check if Azure ADPreview is installed 65 | if ($module -eq 'AzureAD') { 66 | $aadPreview = Get-InstalledModule -Name 'AzureADPreview' -ErrorAction SilentlyContinue 67 | 68 | if ($null -ne $aadPreview) { 69 | Write-Warning "Azure AD won't be installed, because Azure AD Preview is already installed (version: $($aadPreview.Version.Tostring()))" 70 | Write-Host -ForegroundColor Cyan "Azure AD Preview will be tested for upgrade" 71 | 72 | $module = 'AzureADPreview' 73 | #continue 74 | } 75 | } 76 | if ($null -ne (Get-InstalledModule -Name $module -ErrorAction SilentlyContinue)) { 77 | $currentVersion = (Get-InstalledModule -Name $module -AllVersions).Version 78 | } 79 | 80 | $moduleInfos = Find-Module -Name $module 81 | 82 | if ($null -eq $currentVersion) { 83 | Write-Host -ForegroundColor Cyan "Install from PowerShellGallery : $($moduleInfos.Name) - $($moduleInfos.Version) published on $($moduleInfos.PublishedDate)" 84 | 85 | try { 86 | Install-Module -Name $module -Force 87 | } 88 | catch { 89 | Write-Host -ForegroundColor red "$_.Exception.Message" 90 | } 91 | } 92 | elseif ($moduleInfos.Version -eq $currentVersion) { 93 | Write-Host -ForegroundColor Green "$($moduleInfos.Name) already installed in the last version" 94 | } 95 | elseif ($currentVersion.count -gt 1) { 96 | Write-Warning "$module is installed in $($currentVersion.count) versions (versions: $currentVersion)" 97 | Write-Host -ForegroundColor Cyan "Uninstall previous $module versions" 98 | 99 | try { 100 | Get-InstalledModule -Name $module -AllVersions | Where-Object { $_.Version -ne $moduleInfos.Version } | Uninstall-Module -Force 101 | } 102 | catch { 103 | Write-Host -ForegroundColor red "$_.Exception.Message" 104 | } 105 | 106 | Write-Host -ForegroundColor Cyan "Install from PowerShellGallery : $($moduleInfos.Name) - $($moduleInfos.Version) published on $($moduleInfos.PublishedDate)" 107 | 108 | try { 109 | Install-Module -Name $module -Force 110 | } 111 | catch { 112 | Write-Host -ForegroundColor red "$_.Exception.Message" 113 | } 114 | } 115 | else { 116 | Write-Host -ForegroundColor Cyan " $($moduleInfos.Name) - Update from PowerShellGallery from $currentVersion to $($moduleInfos.Version) published on $($moduleInfos.PublishedDate)" 117 | try { 118 | Update-Module -Name $module -Force 119 | } 120 | catch { 121 | Write-Host -ForegroundColor red "$_.Exception.Message" 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /OneDrive/Invoke-OneDriveKnownFoldersLinksFix.ps1: -------------------------------------------------------------------------------- 1 | $oneDriveRoot = $env:OneDriveCommercial 2 | $userProfile = $env:USERPROFILE 3 | 4 | $oneDriveDesktop = [Environment]::GetFolderPath('Desktop') 5 | $oneDriveMyDocuments = [Environment]::GetFolderPath('MyDocuments') 6 | $oneDriveMyPictures = [Environment]::GetFolderPath('MyPictures') 7 | 8 | function Invoke-OneDriveKnownFoldersLinksFix { 9 | Param( 10 | [Parameter(Mandatory = $true)] 11 | [string]$Folder, 12 | [Parameter(Mandatory = $true)] 13 | [string]$OneDriveFolder 14 | ) 15 | 16 | if ($oneDriveFolder -like "$oneDriveRoot*") { 17 | Write-Host "$folder is on OneDrive $oneDriveFolder" -ForegroundColor green 18 | $junctionsFolder = $null 19 | $junctionsFolder = Get-ChildItem "$userProfile\$folder" -Force | Where-Object -Property Attributes -Like '*ReparsePoint*' 20 | 21 | if ($null -ne $junctionsFolder) { 22 | $junctionsFolder | ForEach-Object { 23 | Write-Host "Move junction folder to $_ because it causes issues for the move (access denied on folder if we do not)" -ForegroundColor green 24 | $_ | Move-Item -Force c:\ 25 | } 26 | } 27 | 28 | Write-Host "Move $userProfile\$folder to $OneDriveFolder" -ForegroundColor green 29 | try { 30 | Move-Item "$userProfile\$folder" $OneDriveFolder -ErrorAction SilentlyContinue 31 | } 32 | catch { 33 | Write-Warning "Unable to move items from $userProfile\$folder to $OneDriveFolder. $($_.Exception.Message)" 34 | return 35 | } 36 | 37 | Write-Host "Delete $userProfile\$folder folder" -ForegroundColor green 38 | try { 39 | Remove-Item "$userProfile\$folder" -Recurse -Force 40 | } 41 | catch { 42 | Write-Warning "Unable to remove folder $userProfile\$folder. $($_.Exception.Message)" 43 | return 44 | } 45 | 46 | Write-Host "Create smybolic link $userProfile\$folder => $oneDriveFolde" -ForegroundColor green 47 | cmd /c mklink /J "$userProfile\$Folder" "$oneDriveFolder" 48 | } 49 | } 50 | 51 | Invoke-OneDriveKnownFoldersLinksFix -Folder Desktop -OneDriveFolder $oneDriveDesktop 52 | Invoke-OneDriveKnownFoldersLinksFix -Folder Images -OneDriveFolder $oneDriveMyPictures 53 | Invoke-OneDriveKnownFoldersLinksFix -Folder Documents -OneDriveFolder $oneDriveMyDocuments -------------------------------------------------------------------------------- /Outlook/AutomaticMigrationNewOutlookGPO/Create-GPODisableAutomaticNewOutlookMigration.ps1: -------------------------------------------------------------------------------- 1 | # HKEY_CURRENT_USER\Software\Policies\Microsoft\office\16.0\outlook\preferences] "NewOutlookMigrationUserSetting"=dword:00000000 2 | <# 3 | Not set (Default): If you don’t configure this policy, the user setting for automatic migration remains uncontrolled, and users can manage it themselves. By default, this setting is enabled. 4 | 1 (Enable): If you enable this policy, the user setting for automatic migration is enforced. Automatic migration to the new Outlook is allowed, and users can't change the setting. 5 | 0 (Disable): If you disable this policy, the user setting for automatic migration is turned off. Automatic migration to the new Outlook is blocked, and users can't change the setting. 6 | source : https://learn.microsoft.com/en-us/microsoft-365-apps/outlook/get-started/control-install?branch=main#opt-out-of-new-outlook-migration 7 | #> 8 | 9 | [string]$GpoName = 'USER-Disable automatic migration to New Outlook' 10 | 11 | $modules = @('GroupPolicy', 'ActiveDirectory') 12 | 13 | foreach ($module in $modules) { 14 | try { 15 | Import-Module $module -ErrorAction Stop 16 | } 17 | catch { 18 | Write-Warning "Module $module not found" 19 | return 20 | } 21 | } 22 | 23 | $DC = (Get-ADDomainController -Discover -Service ADWS).Name 24 | $domain = Get-ADDomain 25 | $domainName = $domain.DNSRoot 26 | 27 | Write-Host -ForegroundColor Cyan "$GpoName - Create GPO" 28 | 29 | try { 30 | $myGPO = New-GPO -Name $GpoName -Comment 'GPO to disable automatic migration to New Outlook migration' -Domain $domainName -Server $DC -ErrorAction Stop 31 | } 32 | catch { 33 | Write-Warning "Error creating GPO: $($_.Exception.Message)" 34 | return 35 | } 36 | 37 | # Disable Computer settings 38 | Write-Host "$gpoName - Disable Computer settings and enable User settings" -ForegroundColor Cyan 39 | 40 | $gpm = New-Object -ComObject GPMgmt.GPM 41 | $gpmConstants = $gpm.GetConstants() 42 | 43 | $domainObject = $gpm.GetDomain($domainName, '', $gpmConstants.UseAnyDC) 44 | 45 | $gpoId = (Get-GPO -Name $gpoName -Server $DC).id 46 | $gpoID = $mygpo.id 47 | $domainObject.GetGPO("{$gpoID}").SetUserEnabled($true) 48 | $domainObject.GetGPO("{$gpoID}").SetComputerEnabled($false) 49 | 50 | $keyHash = @{ 51 | 'NewOutlookMigrationUserSetting' = 0 52 | } 53 | 54 | Write-Host -ForegroundColor Cyan "$GpoName - Set registry key" 55 | foreach ($key in $keyHash.Keys) { 56 | $value = $keyHash[$key] 57 | Write-Host "$gpoName - Setting $outlookRegPath -> $key to $value" -ForegroundColor Cyan 58 | try { 59 | $null = Set-GPPrefRegistryValue -Name $GpoName -Context 'User' -Key 'HKCU\Software\Policies\Microsoft\office\16.0\outlook\preferences' -ValueName $key -Value $value -Type DWord -Action Replace -Server $DC -ErrorAction Stop 60 | } 61 | catch { 62 | Write-Warning "Error setting registry key: $($_.Exception.Message)" 63 | } 64 | } 65 | 66 | Write-Host "`nINFORMATION 1: The GPO '$GpoName' has been created. You must link it to the desired OUs." -ForegroundColor Yellow 67 | 68 | Write-Host "`nINFORMATION 2: The GPO settings apply to users. If you filter by user or user groups, ensure that 'Domain Computers' or 'Authenticated Users' have Read permissions: https://learn.microsoft.com/en-us/troubleshoot/windows-server/group-policy/cannot-apply-user-gpo-when-computer-objects-dont-have-read-permissions" -ForegroundColor Yellow 69 | 70 | Write-Host "`nINFORMATION 3: Even if you delete the GPO, the registry key will remain on the user's computer. So if you want to enable automatic migration, you must delete the registry key on the device or set the value to 1." -ForegroundColor Yellow -------------------------------------------------------------------------------- /Outlook/AutomaticMigrationNewOutlookRegistry/DisableNewOutlookAutomaticMigration.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [HKEY_CURRENT_USER\Software\Policies\Microsoft\office\16.0\outlook\preferences] 4 | "NewOutlookMigrationUserSetting"=dword:00000000 -------------------------------------------------------------------------------- /Password/Get-MgPasswordPolicies.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .CHANGELOG 3 | 4 | [1.0.0] - 2025-03-17 5 | # Initial Version 6 | 7 | #> 8 | 9 | function Get-MgPasswordPolicies { 10 | 11 | [System.Collections.Generic.List[PSObject]]$pwdPolicies = @() 12 | 13 | $domains = Get-MgDomain 14 | 15 | foreach ($domain in $domains) { 16 | 17 | if ($domain.PasswordValidityPeriodInDays -eq '2147483647') { 18 | $pwddValidityPeriodInDays = 'Password never expire' 19 | } 20 | else { 21 | $pwddValidityPeriodInDays = $domain.PasswordValidityPeriodInDays 22 | } 23 | 24 | $object = [PSCustomObject][ordered]@{ 25 | Domain = $domain.Id 26 | PasswordValidityPeriodInDays = $pwddValidityPeriodInDays 27 | PasswordNotificationWindowInDays = $domain.PasswordNotificationWindowInDays 28 | } 29 | 30 | $pwdPolicies.Add($object) 31 | } 32 | 33 | return $pwdPolicies 34 | } -------------------------------------------------------------------------------- /Password/Get-MgUserPasswordInfo.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves and processes user password information from Microsoft Graph and get information about the user's password, such as the last password change date, on-premises sync status, and password policies. 4 | 5 | .DESCRIPTION 6 | The Get-MgUserPasswordInfo script collects details such as the user's principal name, last password change date, on-premises sync status, and password policies. 7 | 8 | .PARAMETER UserPrincipalName 9 | Specifies the user principal name(s) of the user(s) for which to retrieve password information. 10 | 11 | .PARAMETER PasswordPoliciesByDomainOnly 12 | If specified, retrieves password policies for domains only, without retrieving individual user information. 13 | 14 | .EXAMPLE 15 | Get-MgUserPasswordInfo 16 | Retrieves password information for all users and outputs it (default behavior). 17 | 18 | .EXAMPLE 19 | Get-MgUserPasswordInfo -UserPrincipalName xxx@domain.com 20 | Retrieves password information for the specified user and outputs it. 21 | 22 | .EXAMPLE 23 | Get-MgUserPasswordInfo -PasswordPoliciesByDomainOnly 24 | Retrieves password policies for all domains only. 25 | 26 | .OUTPUTS 27 | PSCustomObject 28 | The script returns an array of custom PowerShell objects containing the following properties for each user: 29 | - UserPrincipalName: The user's principal name. 30 | - LastPasswordChangeDateTimeUTC: The last date and time the user's password was changed. 31 | - OnPremisesLastSyncDateTimeUTC: The last date and time the user's on-premises directory was synchronized. 32 | - OnPremisesSyncEnabled: Indicates whether on-premises synchronization is enabled for the user. 33 | - ForceChangePasswordNextSignIn: Indicates whether the user is required to change their password at the next sign-in. 34 | - ForceChangePasswordNextSignInWithMfa: Indicates whether the user is required to change their password at the next sign-in with multi-factor authentication. 35 | - PasswordPolicies: The user's password policies. Can be : Empty, 'None' or 'DisablePasswordExpiration' (the last one is especially for synced users). 36 | - PasswordNotificationWindowInDays: The number of days before the password expires that the user is notified. 37 | - PasswordValidityPeriodInDays: The number of days before the password expires. 38 | 39 | .NOTES 40 | Ensure you have the necessary permissions and modules installed to run this script, such as the Microsoft Graph PowerShell module. 41 | The script assumes that the necessary authentication to Microsoft Graph has already been handled with the Connect-MgGraph function. 42 | Connect-MgGraph -Scopes 'User.Read.All', 'Domain.Read.All' 43 | #> 44 | 45 | function Get-MgUserPasswordInfo { 46 | [CmdletBinding()] 47 | param ( 48 | [Parameter(Mandatory = $false)] 49 | [string[]]$UserPrincipalName, 50 | [Parameter(Mandatory = $false)] 51 | [switch]$PasswordPoliciesByDomainOnly 52 | ) 53 | 54 | # Import required modules 55 | $modules = @( 56 | 'Microsoft.Graph.Authentication', 57 | 'Microsoft.Graph.Users', 58 | 'Microsoft.Graph.Identity.DirectoryManagement' 59 | ) 60 | 61 | foreach ($module in $modules) { 62 | try { 63 | $null = Import-Module $module -ErrorAction Stop 64 | } 65 | catch { 66 | Write-Warning "Please install $module first" 67 | return 68 | } 69 | } 70 | 71 | function Get-DomainPasswordPolicies { 72 | Write-Host -ForegroundColor Cyan 'Retrieving password policies for all domains' 73 | $domains = Get-MgDomain -All 74 | $domainPasswordPolicies = [System.Collections.Generic.List[PSObject]]$domainPasswordPolicies = @() 75 | 76 | foreach ($domain in $domains) { 77 | 78 | $validityPeriod = if ($domain.PasswordValidityPeriodInDays -eq '2147483647') { 79 | '2147483647 (Password never expire)' 80 | } 81 | else { 82 | 83 | $domain.PasswordValidityPeriodInDays 84 | } 85 | 86 | $object = [PSCustomObject][ordered]@{ 87 | DomainName = $domain.ID 88 | AuthenticationType = $domain.AuthenticationType 89 | PasswordValidityPeriod = $validityPeriod 90 | PasswordValidityInheritedFrom = $null 91 | PasswordNotificationWindowInDays = $domain.PasswordNotificationWindowInDays 92 | } 93 | 94 | $domainPasswordPolicies.Add($object) 95 | } 96 | 97 | # Inherit password policies 98 | foreach ($domain in $domainPasswordPolicies) { 99 | $found = $false 100 | 101 | foreach ($policy in $domainPasswordPolicies) { 102 | if ($domain.DomainName.EndsWith($policy.DomainName) -and $domain.DomainName -ne $policy.DomainName -and -not $found) { 103 | $domain.PasswordNotificationWindowInDays = $policy.PasswordNotificationWindowInDays 104 | $domain.PasswordValidityPeriod = $policy.PasswordValidityPeriod 105 | $domain.PasswordValidityInheritedFrom = "$($policy.DomainName) domain" 106 | 107 | $found = $true 108 | } 109 | } 110 | } 111 | return $domainPasswordPolicies 112 | } 113 | 114 | # Retrieve domain password policies 115 | $domainPasswordPolicies = Get-DomainPasswordPolicies 116 | 117 | if ($PasswordPoliciesByDomainOnly) { 118 | Write-Host -ForegroundColor Cyan "Note that if you have some federated domains, they don't have password policies because authentication is handled by another IDP (Identity Provider)" 119 | 120 | return $domainPasswordPolicies 121 | } 122 | 123 | # Retrieve user password information 124 | if ($UserPrincipalName) { 125 | Write-Host -ForegroundColor Cyan "Retrieving password information for $($UserPrincipalName.Count) user(s)" 126 | [System.Collections.Generic.List[PSObject]]$usersList = @() 127 | foreach ($upn in $UserPrincipalName) { 128 | $user = Get-MgUser -UserId $upn -Property UserPrincipalName, LastPasswordChangeDateTime, OnPremisesLastSyncDateTime, OnPremisesSyncEnabled, PasswordProfile, PasswordPolicies 129 | 130 | $usersList.Add($user) 131 | } 132 | } 133 | else { 134 | 135 | Write-Host -ForegroundColor Cyan 'Retrieving password information for all users' 136 | $usersList = Get-MgUser -All -Property UserPrincipalName, LastPasswordChangeDateTime, OnPremisesLastSyncDateTime, OnPremisesSyncEnabled, PasswordProfile, PasswordPolicies 137 | } 138 | 139 | [System.Collections.Generic.List[PSObject]]$passwordsInfoArray = @() 140 | 141 | foreach ($user in $usersList) { 142 | $userDomain = $user.UserPrincipalName.Split('@')[1] 143 | $userDomainPolicy = $domainPasswordPolicies | Where-Object { $_.DomainName -eq $userDomain } 144 | 145 | $passwordExpired = $false 146 | 147 | if ($user.PasswordPolicies -eq 'DisablePasswordExpiration') { 148 | $userDomainPolicy.PasswordValidityPeriod = '2147483647 (Password never expire)' 149 | $userDomainPolicy.PasswordValidityInheritedFrom = 'User password policy' 150 | } 151 | 152 | if ($userDomainPolicy.PasswordValidityPeriod -ne '2147483647 (Password never expire)') { 153 | 154 | if ($user.LastPasswordChangeDateTime -lt (Get-Date).AddDays(-$userDomainPolicy.PasswordValidityPeriod)) { 155 | $passwordExpired = $true 156 | } 157 | } 158 | 159 | $object = [PSCustomObject][ordered]@{ 160 | UserPrincipalName = $user.UserPrincipalName 161 | LastPasswordChangeDateTimeUTC = $user.LastPasswordChangeDateTime 162 | OnPremisesLastSyncDateTimeUTC = $user.OnPremisesLastSyncDateTime 163 | OnPremisesSyncEnabled = $user.OnPremisesSyncEnabled 164 | ForceChangePasswordNextSignIn = $user.PasswordProfile.ForceChangePasswordNextSignIn 165 | ForceChangePasswordNextSignInWithMfa = $user.PasswordProfile.ForceChangePasswordNextSignInWithMfa 166 | PasswordPolicies = $user.PasswordPolicies 167 | Domain = $userDomain 168 | PasswordValidityInheritedFrom = $userDomainPolicy.PasswordValidityInheritedFrom 169 | PasswordValidityPeriodInDays = $userDomainPolicy.PasswordValidityPeriod 170 | PasswordNotificationWindowInDays = $userDomainPolicy.PasswordNotificationWindowInDays 171 | PasswordNextChangeDateTimeUTC = if ($userDomainPolicy.PasswordValidityPeriod -ne '2147483647 (Password never expire)') { $user.LastPasswordChangeDateTime.AddDays($userDomainPolicy.PasswordValidityPeriod) }else {} 172 | PasswordExpired = $passwordExpired 173 | } 174 | 175 | $passwordsInfoArray.Add($object) 176 | } 177 | 178 | return $passwordsInfoArray 179 | } -------------------------------------------------------------------------------- /Password/Get-MsolPasswordPolicies.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .CHANGELOG 3 | 4 | [2.0.0] - 2025-03-17 5 | # Changed 6 | - Add warning message to inform the user that the script may become obsolete due to the deprecation of MSOnline in April 2025. 7 | 8 | [1.0.0] - 2024-07-7 9 | # Initial Version 10 | 11 | #> 12 | 13 | function Get-MsolPasswordPolicies { 14 | 15 | [System.Collections.Generic.List[PSObject]]$pwdPolicies = @() 16 | 17 | Write-Warning 'This script may become obsolete due to the deprecation of MSOnline in April 2025. Ensure compatibility with newer modules such as Microsoft Graph PowerShell before use.' 18 | Write-Warning 'Prefer to use Get-MgPasswordPolicies script instead.' 19 | 20 | Get-MsolDomain | ForEach-Object { 21 | $domain = $_.Name 22 | $pwdPolicy = Get-MsolPasswordPolicy -DomainName $_.Name 23 | 24 | if ($pwdPolicy.ValidityPeriod -eq '2147483647') { 25 | $validityPeriod = 'Password never expire' 26 | } 27 | 28 | $object = [PSCustomObject][ordered]@{ 29 | Domain = $domain 30 | NotificationDays = $pwdPolicy.NotificationDays 31 | ValidityPeriod = $validityPeriod 32 | } 33 | 34 | $pwdPolicies.Add($object) 35 | } 36 | 37 | return $pwdPolicies 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft365-Toolbox 2 | 3 | Some useful scripts to get infos about Microsoft 365 tenant. 4 | 5 | -------------------------------------------------------------------------------- /Search-EmailAddressInMicrosoftCloud.ps1: -------------------------------------------------------------------------------- 1 | # https://github.com/itpro-tips/Microsoft365-Toolbox 2 | 3 | function Search-EmailAddressInMicrosoftCloud { 4 | [CmdletBinding()] 5 | Param( 6 | [string[]]$SearchEmails, 7 | [string[]]$SearchByDomain 8 | ) 9 | 10 | function AddtoHashTable { 11 | Param 12 | ( 13 | $HashTable, 14 | $Users 15 | ) 16 | foreach ($user in $users) { 17 | foreach ($emailaddress in $user.emailaddresses) { 18 | #Write-Host 'Processing' $emailaddress -ForegroundColor green 19 | $emailaddress = $emailaddress -replace 'X500:', '' 20 | $emailaddress = $emailaddress -replace 'smtp:', '' 21 | $emailaddress = $emailaddress -replace 'sip:', '' 22 | $emailaddress = $emailaddress -replace 'spo:', '' 23 | 24 | if ($SearchByDomain) { 25 | if ($emailaddress -notlike "*$SearchByDomain") { 26 | continue 27 | } 28 | } 29 | 30 | if (-not($allO365EmailAddressesHashTable.ContainsKey($emailaddress))) { 31 | $allO365EmailAddressesHashTable.add($emailaddress, ($emailaddress + '|' + $user.objectID + '|' + $user.DisplayName + '|' + $user.RecipientTypeDetails)) 32 | } 33 | else { 34 | # Write the details (objectID, RecipientType) of this account to better identification (if an email exists in some different objects type) 35 | # don't write the objectID again 36 | if ($allO365EmailAddressesHashTable[$emailaddress] -like "*$($user.objectID)*") { 37 | # if objectID and recipientTypeDetails already exists, write nothing, otherwise write only the recipienttypedetails 38 | if (-not($allO365EmailAddressesHashTable[$emailaddress] -like "*$($user.RecipientTypeDetails)*")) { 39 | $allO365EmailAddressesHashTable[$emailaddress] = $allO365EmailAddressesHashTable[$emailaddress] + '|' + $user.RecipientTypeDetails 40 | } 41 | } 42 | else { 43 | $allO365EmailAddressesHashTable[$emailaddress] = $allO365EmailAddressesHashTable[$emailaddress] + '|' + $user.DisplayName + '|' + $user.objectID + '|' + $user.RecipientTypeDetails 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | try { 51 | Import-Module exchangeonlinemanagement -ErrorAction stop 52 | } 53 | catch { 54 | Write-Warning 'First, install the official Microsoft Exchange Online Management module : Install-Module exchangeonlinemanagement' 55 | return 56 | } 57 | 58 | try { 59 | Import-Module MSOnline -ErrorAction stop 60 | } 61 | catch { 62 | Write-Warning 'First, install the official Microsoft Online module : Install-Module MSOnline' 63 | return 64 | } 65 | 66 | try { 67 | #WarningAction = silentlycontinue because of warning message when resultsize is bigger than 10 68 | $null = Get-Recipient -ResultSize 1 -ErrorAction Stop -WarningAction silentlycontinue 69 | } 70 | catch { 71 | Write-Host 'Connect Exchange Online' -ForegroundColor Green 72 | Connect-ExchangeOnline 73 | } 74 | 75 | try { 76 | $null = Get-MsolUser -MaxResults 1 -ErrorAction Stop 77 | } 78 | catch { 79 | Write-Host 'Connect MsolService Online' -ForegroundColor Green 80 | Connect-MsolService 81 | } 82 | 83 | $allO365EmailAddressesHashTable = @{} 84 | 85 | ###### Exchange Online infrastructure 86 | Write-Host 'Get All Exchange Online recipients...' -ForegroundColor Green 87 | $allExchangeRecipients = Get-Recipient * -ResultSize unlimited | Select-Object DisplayName, RecipientTypeDetails, EmailAddresses, @{Name = 'objectID'; Expression = { $_.ExternalDirectoryObjectId } } 88 | Write-Host 'Get All SoftDeletedMailbox...' -ForegroundColor Green 89 | $SoftDeleted = Get-Mailbox -SoftDeletedMailbox -ResultSize unlimited | Select-Object DisplayName, RecipientTypeDetails, EmailAddresses, @{Name = 'objectID'; Expression = { $_.ExternalDirectoryObjectId } } 90 | 91 | ##### Azure Active Directory infrastructure 92 | Write-Host 'Get All Office 365 users...' -ForegroundColor Green 93 | 94 | # Office 365 users - UPN name 95 | # Same thing but with UPN because sometimes the UPN is not the same as the SMTP proxyaddresses 96 | $Office365Users = Get-MsolUser -All 97 | 98 | $Office365UPNUsers = $Office365Users | Select-Object DisplayName, objectID, @{Name = 'EmailAddresses'; Expression = { $_.UserPrincipalName } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member' -or $null -eq $_.UserType) { 'Office365User' } else { 'GuestUser' } } } 99 | $Office365EmailAddresses = $Office365Users | Select-Object DisplayName, objectID, @{Name = 'EmailAddresses'; Expression = { $_.ProxyAddresses } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'Office365User' }else { 'GuestUser' } } } 100 | $Office365AlternateEmailAddresses = $Office365Users | Select-Object DisplayName, objectID, @{Name = 'EmailAddresses'; Expression = { $_.AlternateEmailAddresses } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member' -or $null -eq $_.UserType) { 'O365UserAlternateEmailAddress' } else { 'GuestUserAlternateEmailAddress' } } } 101 | 102 | Write-Host 'Get All Office 365 deleted users...' -ForegroundColor Green 103 | 104 | $Office365DeletedUsers = Get-MsolUser -ReturnDeletedUsers -All 105 | 106 | $Office365DeletedUsersUPN = $Office365DeletedUsers | Select-Object DisplayName, objectID, @{Name = 'EmailAddresses'; Expression = { $_.UserPrincipalName } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'DeletedOffice365User' }else { 'DeletedGuestUser' } } } 107 | $Office365DeletedUsersEmailAddresses = $Office365DeletedUsers | Select-Object DisplayName, objectID, @{Name = 'EmailAddresses'; Expression = { $_.ProxyAddresses } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'DeletedOffice365User' }else { 'DeletedGuestUser' } } } 108 | 109 | # Creating hashtable 110 | Write-Host 'Creating HashTable...' -ForegroundColor Green 111 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $allExchangeRecipients 112 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $SoftDeleted 113 | 114 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $Office365UPNUsers 115 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $Office365EmailAddresses 116 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $Office365AlternateEmailAddresses 117 | 118 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $Office365DeletedUsersUPN 119 | AddtoHashTable -HashTable $allO365EmailAddressesHashTable -Users $Office365DeletedUsersEmailAddresses 120 | 121 | if ($SearchEmails) { 122 | foreach ($SearchEmail in $SearchEmails) { 123 | 124 | if ($allO365EmailAddressesHashTable.Contains($SearchEmail)) { 125 | Write-Host $SearchEmail 'matching: ' -ForegroundColor Yellow -NoNewline 126 | $allO365EmailAddressesHashTable[$SearchEmail] 127 | } 128 | else { 129 | Write-Host $SearchEmail 'not found' -ForegroundColor Red 130 | } 131 | } 132 | } 133 | 134 | else { 135 | return $allO365EmailAddressesHashTable 136 | } 137 | } -------------------------------------------------------------------------------- /Search-EmailAddressInMicrosoftCloudv2.ps1: -------------------------------------------------------------------------------- 1 | # https://github.com/itpro-tips/Microsoft365-Toolbox 2 | 3 | function Search-EmailAddressInMicrosoftCloud { 4 | [CmdletBinding()] 5 | Param( 6 | [string[]]$SearchEmails, 7 | [string[]]$SearchByDomain 8 | ) 9 | 10 | function AddtoHashTable { 11 | Param 12 | ( 13 | $HashTable, 14 | $Users 15 | ) 16 | foreach ($user in $users) { 17 | foreach ($emailaddress in $user.emailaddresses) { 18 | #Write-Host 'Processing' $emailaddress -ForegroundColor green 19 | $emailaddress = $emailaddress -replace 'X500:', '' 20 | $emailaddress = $emailaddress -replace 'smtp:', '' 21 | $emailaddress = $emailaddress -replace 'sip:', '' 22 | $emailaddress = $emailaddress -replace 'spo:', '' 23 | 24 | if ($SearchByDomain) { 25 | if ($emailaddress -notlike "*$SearchByDomain") { 26 | continue 27 | } 28 | } 29 | 30 | if (-not($allM365EmailAddressesHashTable.ContainsKey($emailaddress))) { 31 | $allM365EmailAddressesHashTable.add($emailaddress, ($emailaddress + '|' + $user.objectID + '|' + $user.DisplayName + '|' + $user.RecipientTypeDetails)) 32 | } 33 | else { 34 | # Write the details (objectID, RecipientType) of this account to better identification (if an email exists in some different objects type) 35 | # don't write the objectID again 36 | if ($allM365EmailAddressesHashTable[$emailaddress] -like "*$($user.objectID)*") { 37 | # if objectID and recipientTypeDetails already exists, write nothing, otherwise write only the recipienttypedetails 38 | if (-not($allM365EmailAddressesHashTable[$emailaddress] -like "*$($user.RecipientTypeDetails)*")) { 39 | $allM365EmailAddressesHashTable[$emailaddress] = $allM365EmailAddressesHashTable[$emailaddress] + '|' + $user.RecipientTypeDetails 40 | } 41 | } 42 | else { 43 | $allM365EmailAddressesHashTable[$emailaddress] = $allM365EmailAddressesHashTable[$emailaddress] + '|' + $user.DisplayName + '|' + $user.objectID + '|' + $user.RecipientTypeDetails 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | $modules = @( 51 | 'ExchangeOnlineManagement' 52 | 'Microsoft.Graph.Authentication' 53 | 'Microsoft.Graph.Users' 54 | ) 55 | 56 | foreach ($module in $modules) { 57 | try { 58 | Import-Module $modules -ErrorAction stop 59 | } 60 | catch { 61 | Write-Warning "First, install the Microsoft $modules module first : Install-Module $modules" 62 | return 63 | } 64 | } 65 | 66 | try { 67 | #WarningAction = silentlycontinue because of warning message when resultsize is bigger than 10 68 | $null = Get-Recipient -ResultSize 1 -ErrorAction Stop -WarningAction silentlycontinue 69 | } 70 | catch { 71 | Write-Host 'Connect Exchange Online' -ForegroundColor Green 72 | Connect-ExchangeOnline 73 | } 74 | 75 | try { 76 | $null = Get-MsolUser -MaxResults 1 -ErrorAction Stop 77 | } 78 | catch { 79 | Write-Host 'Connect MsolService Online' -ForegroundColor Green 80 | Connect-MgGraph -Scopes 'User.Read.All' -NoWelcome 81 | } 82 | 83 | $allM365EmailAddressesHashTable = @{} 84 | 85 | ###### Exchange Online infrastructure 86 | Write-Host 'Get All Exchange Online recipients...' -ForegroundColor Green 87 | $allExchangeRecipients = Get-Recipient * -ResultSize unlimited | Select-Object DisplayName, RecipientTypeDetails, EmailAddresses, @{Name = 'objectID'; Expression = { $_.ExternalDirectoryObjectId } } 88 | 89 | Write-Host 'Get All softDeletedMailbox...' -ForegroundColor Green 90 | $softDeleted = Get-Mailbox -SoftDeletedMailbox -ResultSize unlimited | Select-Object DisplayName, RecipientTypeDetails, EmailAddresses, @{Name = 'objectID'; Expression = { $_.ExternalDirectoryObjectId } } 91 | 92 | ##### Azure Active Directory infrastructure 93 | Write-Host 'Get All Microsoft 365 users...' -ForegroundColor Green 94 | 95 | # Microsoft 365 users - UPN name 96 | # Same thing but with UPN because sometimes the UPN is not the same as the SMTP proxyaddresses 97 | $entraIDUsers = Get-MgUser -All -Property UserPrincipalName, ID, UserType, ProxyAddresses 98 | 99 | $m365UPNUsers = $entraIDUsers | Select-Object DisplayName, @{Name = 'objectID'; Expression = { $_.ID } }, @{Name = 'EmailAddresses'; Expression = { $_.UserPrincipalName } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member' -or $null -eq $_.UserType) { 'Microsoft365User' } else { 'GuestUser' } } } 100 | $m365EmailAddresses = $entraIDUsers | Select-Object DisplayName, @{Name = 'objectID'; Expression = { $_.ID } }, @{Name = 'EmailAddresses'; Expression = { $_.ProxyAddresses } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'Microsoft365User' }else { 'GuestUser' } } } 101 | $m365AlternateEmailAddresses = $entraIDUsers | Select-Object DisplayName, @{Name = 'objectID'; Expression = { $_.ID } }, @{Name = 'EmailAddresses'; Expression = { $_.OtherMails } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member' -or $null -eq $_.UserType) { 'O365UserAlternateEmailAddress' } else { 'GuestUserAlternateEmailAddress' } } } 102 | 103 | Write-Host 'Get All Microsoft 365 deleted users...' -ForegroundColor Green 104 | 105 | $entraIDDeletedUsers = Get-MgDirectoryDeletedItemAsUser -All 106 | 107 | $entraIDDeletedUsersUPN = $entraIDDeletedUsers | Select-Object DisplayName, @{Name = 'objectID'; Expression = { $_.ID } }, @{Name = 'EmailAddresses'; Expression = { $_.UserPrincipalName } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'DeletedMicrosoft365User' }else { 'DeletedGuestUser' } } } 108 | $entraIDDeletedUsersEmailAddresses = $entraIDDeletedUsers | Select-Object DisplayName, @{Name = 'objectID'; Expression = { $_.ID } }, @{Name = 'EmailAddresses'; Expression = { $_.ProxyAddresses } }, @{Name = 'RecipientTypeDetails'; Expression = { if ($_.UserType -eq 'Member') { 'DeletedMicrosoft365User' }else { 'DeletedGuestUser' } } } 109 | 110 | # Creating hashtable 111 | Write-Host 'Creating HashTable...' -ForegroundColor Green 112 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $allExchangeRecipients 113 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $softDeleted 114 | 115 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $m365UPNUsers 116 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $m365EmailAddresses 117 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $m365AlternateEmailAddresses 118 | 119 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $entraIDDeletedUsersUPN 120 | AddtoHashTable -HashTable $allM365EmailAddressesHashTable -Users $entraIDDeletedUsersEmailAddresses 121 | 122 | if ($SearchEmails) { 123 | foreach ($SearchEmail in $SearchEmails) { 124 | 125 | if ($allM365EmailAddressesHashTable.Contains($SearchEmail)) { 126 | Write-Host $SearchEmail 'matching: ' -ForegroundColor Yellow -NoNewline 127 | $allM365EmailAddressesHashTable[$SearchEmail] 128 | } 129 | else { 130 | Write-Host $SearchEmail 'not found' -ForegroundColor Red 131 | } 132 | } 133 | } 134 | 135 | else { 136 | return $allM365EmailAddressesHashTable 137 | } 138 | } -------------------------------------------------------------------------------- /SharePoint/Export-SPOAdmin.ps1: -------------------------------------------------------------------------------- 1 | New-Module { 2 | 3 | Function Invoke-Prerequisites { 4 | 5 | [OutputType()] 6 | [CmdletBinding()] 7 | Param ( 8 | [Parameter(Position = 1)] 9 | [string] $Tenant, 10 | [Parameter(Position = 2)] 11 | [string] $ClientID, 12 | [Parameter(Position = 3)] 13 | [string] $CertPath, 14 | [Parameter(Position = 4)] 15 | [string] $CertPass 16 | ) 17 | 18 | $ScriptStopwatch = [system.diagnostics.stopwatch]StartNew() 19 | 20 | If ((Get-Culture).LCID -eq 1033) { 21 | 22 | $ScriptDate = (Get-Date).tostring(MM-dd-yy) 23 | 24 | } 25 | Else { 26 | 27 | $ScriptDate = (Get-Date).tostring(dd-MM-yy) 28 | 29 | } 30 | 31 | $ScriptTenant = $Tenant 32 | 33 | $ScriptTenantUrl = https$($Tenant).sharepoint.com 34 | $ScriptAadDomain = $($Tenant).onmicrosoft.com 35 | $ScriptClientID = $ClientID 36 | $ScriptCertPass = $CertPass 37 | $ScriptCertPath = $CertPath 38 | 39 | } 40 | 41 | Function Get-Administrators { 42 | 43 | $Admins = Get-PnPSiteCollectionAdmin 44 | 45 | # Below gets users who have full control - set as administrator via admin portal # 46 | ForEach ($Admin in $Admins Where-Object { $_ -ne System Account }) { 47 | 48 | $Datum = New-Object -TypeName PSObject 49 | 50 | $Datum Add-Member -MemberType NoteProperty -Name Tenant -Value $Tenant 51 | $Datum Add-Member -MemberType NoteProperty -Name Site -Value $SiteUrl 52 | $Datum Add-Member -MemberType NoteProperty -Name Group -Value Aministrators 53 | 54 | Switch ($Admin.PrincipalType) { 55 | 56 | User { 57 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value $Admin.Title 58 | } 59 | SecurityGroup { 60 | If ($Admin.Title -ne Company Administrator) { 61 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value $($Admin.Title) - AD Group 62 | } 63 | ElseIf ($Admin.Title -eq SharePoint Service Administrator) { 64 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value $($Admin.Title) 65 | } 66 | Else { 67 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value Global Admins 68 | } 69 | } 70 | 71 | } 72 | 73 | $Datum Add-Member -MemberType NoteProperty -Name Subsite -Value No 74 | $Datum Add-Member -MemberType NoteProperty -Name Permissions -Value Unique 75 | 76 | $ScriptData += $Datum 77 | 78 | } 79 | 80 | } 81 | 82 | Function Get-OwnerNoGroup { 83 | 84 | Param( 85 | [Parameter(Mandatory = $true, Position = 0)] 86 | [string]$Subsite 87 | ) 88 | 89 | $Web = Get-PnPWeb -Includes RoleAssignments 90 | 91 | ForEach ($RA in $Web.RoleAssignments) { 92 | 93 | $RoleBindings = Get-PnPProperty -ClientObject $RA -Property RoleDefinitionBindings 94 | $PrincipalType = Get-PnPProperty -ClientObject $($RA.Member) -Property PrincipalType 95 | 96 | $RoleTypeKind = ($RoleBindings.RoleTypeKind).ToString() 97 | $PType = $PrincipalType.ToString() 98 | 99 | If ($PType -eq User -and $RoleTypeKind -eq Administrator) { 100 | 101 | $Title = Get-PnPProperty -ClientObject $($RA.Member) -Property Title 102 | 103 | $Datum = New-Object -TypeName PSObject 104 | 105 | $Datum Add-Member -MemberType NoteProperty -Name Tenant -Value $Tenant 106 | If ($Subsite -eq No) { 107 | $Datum Add-Member -MemberType NoteProperty -Name Site -Value $SiteUrl 108 | } 109 | Else { 110 | $Datum Add-Member -MemberType NoteProperty -Name Site -Value $SubsiteUrl 111 | } 112 | $Datum Add-Member -MemberType NoteProperty -Name Group -Value NA 113 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value $Title 114 | $Datum Add-Member -MemberType NoteProperty -Name Subsite -Value $Subsite 115 | $Datum Add-Member -MemberType NoteProperty -Name Permissions -Value Unique 116 | 117 | $ScriptData += $Datum 118 | 119 | } 120 | 121 | } 122 | 123 | } 124 | 125 | Function Get-OwnerFromGroup { 126 | 127 | Param( 128 | [Parameter(Mandatory = $true, Position = 0)] 129 | [string]$Subsite, 130 | [Parameter(Position = 1)] 131 | [string]$SiteUrl, 132 | [Parameter(Position = 1)] 133 | [string]$SubsiteUrl 134 | ) 135 | 136 | # HasUniqueRoleAssignments 137 | $Groups = Get-PnPGroup 138 | Where-Object { $_.Title -notlike SharingLinks. -and $_.Title -notlike Limited Access } 139 | Select-Object Title, Users, PrincipalType, Id 140 | 141 | If ($Subsite -eq No) { 142 | Write-Host Auditing $($SiteUrl) -ForegroundColor Cyan 143 | } 144 | Else { 145 | Write-Host Auditing $($SubsiteUrl) -ForegroundColor Cyan 146 | } 147 | 148 | ForEach ($Group in $Groups Where-Object { $_.PrincipalType -eq SharePointGroup }) { 149 | 150 | $GroupPermission = Get-PnPGroupPermissions -Identity $Group.Title -ErrorAction SilentlyContinue 151 | Where-Object { $_.Hidden -like False } 152 | 153 | If ($GroupPermission.RoleTypeKind -eq Administrator) { 154 | 155 | ForEach ($G in $Group Where-Object { $_.Users.Title -ne System Account }) { 156 | 157 | $Members = Get-PnPGroupMembers -Identity $G.Id Select-Object LoginName, Title, PrincipalType 158 | $Members = $Members Where-Object { $_.Title -ne System Account -and $_.PrincipalType -eq User } 159 | 160 | If ($Members) { 161 | 162 | ForEach ($Member in $Members) { 163 | 164 | If ($Site.HasUniqueRoleAssignments -eq $null -or $Site.HasUniqueRoleAssignments -eq $true) { 165 | $InheritsPermissions = Unique 166 | } 167 | Else { 168 | $InheritsPermissions = Inherited 169 | } 170 | 171 | $Datum = New-Object -TypeName PSObject 172 | 173 | $Datum Add-Member -MemberType NoteProperty -Name Tenant -Value $Tenant 174 | If ($Subsite -eq No) { 175 | $Datum Add-Member -MemberType NoteProperty -Name Site -Value $SiteUrl 176 | } 177 | Else { 178 | $Datum Add-Member -MemberType NoteProperty -Name Site -Value $SubsiteUrl 179 | } 180 | $Datum Add-Member -MemberType NoteProperty -Name Group -Value $Group.Title 181 | $Datum Add-Member -MemberType NoteProperty -Name Member -Value $Member.Title 182 | $Datum Add-Member -MemberType NoteProperty -Name Subsite -Value $Subsite 183 | $Datum Add-Member -MemberType NoteProperty -Name Permissions -Value $InheritsPermissions 184 | 185 | $ScriptData += $Datum 186 | 187 | } 188 | 189 | } 190 | 191 | } 192 | 193 | } 194 | 195 | } 196 | 197 | } 198 | 199 | Function Invoke-FilePicker { 200 | 201 | Write-Host Select your certificate .pfx file 202 | 203 | Add-Type -AssemblyName System.Windows.Forms 204 | 205 | $Dialog = New-Object System.Windows.Forms.OpenFileDialog 206 | $Dialog.InitialDirectory = $InitialDirectory 207 | $Dialog.Title = Select your certificate .pfx file 208 | $Dialog.Filter = Certificate file.pfx 209 | $Dialog.Multiselect = $false 210 | $Result = $Dialog.ShowDialog() 211 | 212 | If ($Result -eq 'OK') { 213 | 214 | Try { 215 | 216 | $ScriptCertPath = $Dialog.FileNames 217 | } 218 | 219 | Catch { 220 | 221 | $ScriptCertPath = $null 222 | Break 223 | } 224 | } 225 | 226 | Else { 227 | 228 | Write-Host Notice No file selected. -ForegroundColor Yellow 229 | Break 230 | 231 | } 232 | 233 | } 234 | 235 | Function Export-SPOAdmin { 236 | 237 | [OutputType()] 238 | [CmdletBinding()] 239 | Param ( 240 | [Parameter( 241 | Mandatory = $true, 242 | Position = 1, 243 | HelpMessage = Enter your O365 tenant name, like 'contoso' 244 | )] 245 | [ValidateNotNullorEmpty()] 246 | [string] $Tenant, 247 | [Parameter( 248 | Mandatory = $true, 249 | Position = 2, 250 | HelpMessage = Enter your Az App Client ID 251 | )] 252 | [ValidateNotNullorEmpty()] 253 | [string] $ClientID 254 | ) 255 | 256 | #ToDo Exclude SubSites Switch 257 | 258 | BEGIN { 259 | 260 | Invoke-FilePicker 261 | 262 | $ScriptCertPass = Read-Host Enter your certificate password 263 | 264 | $Params = @{ 265 | Tenant = $Tenant 266 | ClientID = $ClientID 267 | CertPath = $CertPath 268 | CertPass = $CertPass 269 | } 270 | 271 | Invoke-Prerequisites @Params 272 | 273 | $Params = @{ 274 | ClientId = $ClientID 275 | CertificatePath = $CertPath 276 | CertificatePassword = (ConvertTo-SecureString -AsPlainText $CertPass -Force) 277 | Url = $TenantUrl 278 | Tenant = $AadDomain 279 | } 280 | 281 | } 282 | PROCESS { 283 | 284 | Connect-PnPOnline @Params -WarningAction SilentlyContinue 285 | 286 | $ScriptSites = Get-PnPTenantSite -Filter Url -notlike 'portals' Where-Object -Property Template -NotIn ( 287 | SRCHCEN#0, 288 | SPSMSITEHOST#0, 289 | APPCATALOG#0, 290 | POINTPUBLISHINGHUB#0, 291 | EDISC#0, 292 | STS#-1 293 | ) 294 | 295 | $Sites = $Sites.Url 296 | 297 | # For Testing Add - $Sites = $Sites Select -First 5 -Skip 5 or similar below this comment # 298 | 299 | Disconnect-PnPOnline 300 | 301 | $ScriptData = @() 302 | 303 | ForEach ($SiteUrl in $Sites) { 304 | 305 | $Subsite = No 306 | 307 | $Params = @{ 308 | ClientId = $ClientID 309 | CertificatePath = $CertPath 310 | CertificatePassword = (ConvertTo-SecureString -AsPlainText $CertPass -Force) 311 | Url = $SiteUrl 312 | Tenant = $AadDomain 313 | } 314 | 315 | Connect-PnPOnline @Params -WarningAction SilentlyContinue 316 | 317 | # Below gets users who have full control - directly applied at the root # 318 | Get-OwnerNoGroup -Subsite $Subsite 319 | 320 | # Below gets users who have full control - inherited from a group # 321 | Get-OwnerFromGroup -Subsite $Subsite -SiteUrl $SiteUrl 322 | 323 | # Below gets users who have full control - set as administrator via admin portal # 324 | Get-Administrators 325 | 326 | $SubSites = Get-PnpSubwebs -Recurse -Includes HasUniqueRoleAssignments # need to do more with this 327 | # $SubSites = Get-PnPSubWebs -Recurse 328 | 329 | Disconnect-PnPOnline 330 | 331 | If ($SubSites) { 332 | 333 | ForEach ($Site in $SubSites) { 334 | 335 | $Subsite = Yes 336 | $SubsiteUrl = $Site.Url 337 | 338 | $Params = @{ 339 | ClientId = $ClientID 340 | CertificatePath = $CertPath 341 | CertificatePassword = (ConvertTo-SecureString -AsPlainText $CertPass -Force) 342 | Url = $SubsiteUrl 343 | Tenant = $AadDomain 344 | } 345 | 346 | Connect-PnPOnline @Params -WarningAction SilentlyContinue 347 | 348 | # Below gets users who have full control - directly applied at the root # 349 | Get-OwnerNoGroup -Subsite $Subsite 350 | 351 | # Below gets users who have full control - inherited from a group # 352 | Get-OwnerFromGroup -Subsite $Subsite -SubsiteUrl $SubsiteUrl 353 | 354 | Write-Host Subsite processed -ForegroundColor White -NoNewline 355 | Write-Host $($SubsiteUrl) -ForegroundColor DarkGreen 356 | 357 | Disconnect-PnPOnline 358 | 359 | } 360 | 361 | } 362 | 363 | Write-Host Site processed -ForegroundColor White -NoNewline 364 | Write-Host $($SiteUrl) -ForegroundColor Green 365 | 366 | } 367 | 368 | } 369 | END { 370 | 371 | If ($Data) { 372 | 373 | $Path = . 374 | $FileName = $Tenant-SPOAdmins-$Date.csv 375 | $Data Export-Csv -Path $Path$FileName -NoTypeInformation 376 | $Location = Get-Location 377 | 378 | Write-Host 379 | Write-Host File called -NoNewline 380 | Write-Host '$FileName' -ForegroundColor Green -NoNewline 381 | Write-Host exported to -NoNewline 382 | Write-Host $Location -ForegroundColor Green 383 | Write-Host 384 | 385 | } 386 | Else { 387 | 388 | Write-Host No Data to Export 389 | 390 | } 391 | 392 | $TotalSecs = [math]Round($StopWatch.Elapsed.TotalSeconds, 0) 393 | $StopWatch.Stop() 394 | 395 | If ($TotalSecs -lt 60) { 396 | 397 | Write-Host Job Took -NoNewline 398 | Write-Host $TotalSecs Seconds -NoNewline -ForegroundColor Cyan 399 | Write-Host to Complete 400 | 401 | 402 | } 403 | ElseIf ($TotalSecs -ge 60 -and $TotalSecs -lt 3600) { 404 | 405 | $Count = New-TimeSpan -Seconds $TotalSecs 406 | 407 | Write-Host Job Took -NoNewline 408 | Write-Host $($Count.Minutes) Minutes -NoNewline -ForegroundColor Cyan 409 | Write-Host and $($Count.Seconds) Seconds -NoNewline -ForegroundColor Cyan 410 | Write-Host to Complete 411 | } 412 | Else { 413 | 414 | $Count = New-TimeSpan -Seconds $TotalSecs 415 | 416 | Write-Host Job Took -NoNewline 417 | Write-Host $($Count.Hours) Hours, -NoNewline -ForegroundColor Cyan 418 | Write-Host $($Count.Minutes) Minutes and -NoNewline -ForegroundColor Cyan 419 | Write-Host $($Count.Seconds) Seconds -NoNewline -ForegroundColor Cyan 420 | Write-Host to Complete 421 | 422 | } 423 | 424 | } 425 | 426 | } 427 | 428 | Export-ModuleMember Export-SPOAdmin 429 | 430 | } Out-Null 431 | -------------------------------------------------------------------------------- /SharePoint/Get-SPOSitesDetails.ps1: -------------------------------------------------------------------------------- 1 | # Admin must be site admin to get all properties 2 | # Set-SPOUser -Site hxxx -LoginName xxx@xxx.onmicrosoft.com -IsSiteCollectionAdmin $true 3 | 4 | # TODO : use LastItemUserModifiedDate but need to connect with ctx. or with PNP to each site 5 | Function Get-SPOSitesDetails { 6 | [CmdletBinding()] 7 | Param( 8 | [boolean]$ExcludeOneDrive, 9 | [boolean]$OnlyOneDrive, 10 | [boolean]$M365GroupsDetails, 11 | [string]$SiteURL, 12 | [boolean]$SharingRightsAdminOrFullControl, 13 | [boolean]$regionalSettingsDetails 14 | ) 15 | 16 | # https://diecknet.de/en/2021/07/09/Sharepoint-Online-Timezones-by-PowerShell/ 17 | function Convert-SPOTimezoneToString( 18 | # ID of a SPO Timezone 19 | [int]$ID 20 | ) { 21 | <# 22 | .SYNOPSIS 23 | Convert a Sharepoint Online Time zone ID to a human readable string. 24 | 25 | .NOTES 26 | By Andreas Dieckmann - https://diecknet.de 27 | Timezone IDs according to https://docs.microsoft.com/en-us/dotnet/api/microsoft.sharepoint.spregionalsettings.timezones?view=sharepoint-server#Microsoft_SharePoint_SPRegionalSettings_TimeZones 28 | 29 | Licensed under MIT License 30 | Copyright 2021 Andreas Dieckmann 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 37 | 38 | .EXAMPLE 39 | Convert-SPOTimezoneToString 14 40 | (UTC-09:00) Alaska 41 | 42 | .LINK 43 | https://diecknet.de/en/2021/07/09/Sharepoint-Online-Timezones-by-PowerShell/ 44 | #> 45 | 46 | $timezoneIDs = @{ 47 | 39 = "(UTC-12:00) International Date Line West" 48 | 95 = "(UTC-11:00) Coordinated Universal Time-11" 49 | 15 = "(UTC-10:00) Hawaii" 50 | 14 = "(UTC-09:00) Alaska" 51 | 78 = "(UTC-08:00) Baja California" 52 | 13 = "(UTC-08:00) Pacific Time (US and Canada)" 53 | 38 = "(UTC-07:00) Arizona" 54 | 77 = "(UTC-07:00) Chihuahua, La Paz, Mazatlan" 55 | 12 = "(UTC-07:00) Mountain Time (US and Canada)" 56 | 55 = "(UTC-06:00) Central America" 57 | 11 = "(UTC-06:00) Central Time (US and Canada)" 58 | 37 = "(UTC-06:00) Guadalajara, Mexico City, Monterrey" 59 | 36 = "(UTC-06:00) Saskatchewan" 60 | 35 = "(UTC-05:00) Bogota, Lima, Quito" 61 | 10 = "(UTC-05:00) Eastern Time (US and Canada)" 62 | 34 = "(UTC-05:00) Indiana (East)" 63 | 88 = "(UTC-04:30) Caracas" 64 | 91 = "(UTC-04:00) Asuncion" 65 | 9 = "(UTC-04:00) Atlantic Time (Canada)" 66 | 81 = "(UTC-04:00) Cuiaba" 67 | 33 = "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan" 68 | 28 = "(UTC-03:30) Newfoundland" 69 | 8 = "(UTC-03:00) Brasilia" 70 | 85 = "(UTC-03:00) Buenos Aires" 71 | 32 = "(UTC-03:00) Cayenne, Fortaleza" 72 | 60 = "(UTC-03:00) Greenland" 73 | 90 = "(UTC-03:00) Montevideo" 74 | 103 = "(UTC-03:00) Salvador" 75 | 65 = "(UTC-03:00) Santiago" 76 | 96 = "(UTC-02:00) Coordinated Universal Time-02" 77 | 30 = "(UTC-02:00) Mid-Atlantic" 78 | 29 = "(UTC-01:00) Azores" 79 | 53 = "(UTC-01:00) Cabo Verde" 80 | 86 = "(UTC) Casablanca" 81 | 93 = "(UTC) Coordinated Universal Time" 82 | 2 = "(UTC) Dublin, Edinburgh, Lisbon, London" 83 | 31 = "(UTC) Monrovia, Reykjavik" 84 | 4 = "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna" 85 | 6 = "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague" 86 | 3 = "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris" 87 | 57 = "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb" 88 | 69 = "(UTC+01:00) West Central Africa" 89 | 83 = "(UTC+01:00) Windhoek" 90 | 79 = "(UTC+02:00) Amman" 91 | 5 = "(UTC+02:00) Athens, Bucharest, Istanbul" 92 | 80 = "(UTC+02:00) Beirut" 93 | 49 = "(UTC+02:00) Cairo" 94 | 98 = "(UTC+02:00) Damascus" 95 | 50 = "(UTC+02:00) Harare, Pretoria" 96 | 59 = "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius" 97 | 101 = "(UTC+02:00) Istanbul" 98 | 27 = "(UTC+02:00) Jerusalem" 99 | 7 = "(UTC+02:00) Minsk (old)" 100 | 104 = "(UTC+02:00) E. Europe" 101 | 100 = "(UTC+02:00) Kaliningrad (RTZ 1)" 102 | 26 = "(UTC+03:00) Baghdad" 103 | 74 = "(UTC+03:00) Kuwait, Riyadh" 104 | 109 = "(UTC+03:00) Minsk" 105 | 51 = "(UTC+03:00) Moscow, St. Petersburg, Volgograd (RTZ 2)" 106 | 56 = "(UTC+03:00) Nairobi" 107 | 25 = "(UTC+03:30) Tehran" 108 | 24 = "(UTC+04:00) Abu Dhabi, Muscat" 109 | 54 = "(UTC+04:00) Baku" 110 | 106 = "(UTC+04:00) Izhevsk, Samara (RTZ 3)" 111 | 89 = "(UTC+04:00) Port Louis" 112 | 82 = "(UTC+04:00) Tbilisi" 113 | 84 = "(UTC+04:00) Yerevan" 114 | 48 = "(UTC+04:30) Kabul" 115 | 58 = "(UTC+05:00) Ekaterinburg (RTZ 4)" 116 | 87 = "(UTC+05:00) Islamabad, Karachi" 117 | 47 = "(UTC+05:00) Tashkent" 118 | 23 = "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi" 119 | 66 = "(UTC+05:30) Sri Jayawardenepura" 120 | 62 = "(UTC+05:45) Kathmandu" 121 | 71 = "(UTC+06:00) Astana" 122 | 102 = "(UTC+06:00) Dhaka" 123 | 46 = "(UTC+06:00) Novosibirsk (RTZ 5)" 124 | 61 = "(UTC+06:30) Yangon (Rangoon)" 125 | 22 = "(UTC+07:00) Bangkok, Hanoi, Jakarta" 126 | 64 = "(UTC+07:00) Krasnoyarsk (RTZ 6)" 127 | 45 = "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi" 128 | 63 = "(UTC+08:00) Irkutsk (RTZ 7)" 129 | 21 = "(UTC+08:00) Kuala Lumpur, Singapore" 130 | 73 = "(UTC+08:00) Perth" 131 | 75 = "(UTC+08:00) Taipei" 132 | 94 = "(UTC+08:00) Ulaanbaatar" 133 | 20 = "(UTC+09:00) Osaka, Sapporo, Tokyo" 134 | 72 = "(UTC+09:00) Seoul" 135 | 70 = "(UTC+09:00) Yakutsk (RTZ 8)" 136 | 19 = "(UTC+09:30) Adelaide" 137 | 44 = "(UTC+09:30) Darwin" 138 | 18 = "(UTC+10:00) Brisbane" 139 | 76 = "(UTC+10:00) Canberra, Melbourne, Sydney" 140 | 43 = "(UTC+10:00) Guam, Port Moresby" 141 | 42 = "(UTC+10:00) Hobart" 142 | 99 = "(UTC+10:00) Magadan" 143 | 68 = "(UTC+10:00) Vladivostok, Magadan (RTZ 9)" 144 | 107 = "(UTC+11:00) Chokurdakh (RTZ 10)" 145 | 41 = "(UTC+11:00) Solomon Is., New Caledonia" 146 | 108 = "(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky (RTZ 11)" 147 | 17 = "(UTC+12:00) Auckland, Wellington" 148 | 97 = "(UTC+12:00) Coordinated Universal Time+12" 149 | 40 = "(UTC+12:00) Fiji" 150 | 92 = "(UTC+12:00) Petropavlovsk-Kamchatsky - Old" 151 | 67 = "(UTC+13:00) Nuku'alofa" 152 | 16 = "(UTC+13:00) Samoa" 153 | } 154 | 155 | $timezoneString = $timezoneIDs.Get_Item($ID) 156 | 157 | if ($null -ne $timezoneString) { 158 | return $timezoneString 159 | } 160 | else { 161 | return $ID 162 | } 163 | } 164 | 165 | if (-not(Get-Module AzureADPreview -ListAvailable)) { 166 | Write-Warning "To use Microsoft 365 groups template, you must install AzureADPreview. Please run: 167 | Uninstall-Module AzureAD 168 | Install-Module AzureADPreview 169 | 170 | Note: After the Team creation, you can switch to the non Preview version: 171 | Uninstall-Module AzureADPreview 172 | Install-Module AzureAD 173 | " 174 | 175 | return 176 | } 177 | 178 | if (-not(Get-Module MicrosoftTeams -ListAvailable)) { 179 | Write-Warning "Please install MicrosoftTeams PowerShell module: 180 | Install-Module MicrosoftTeams 181 | " 182 | return 183 | } 184 | 185 | if (-not(Get-Module Microsoft.Online.SharePoint.PowerShell -ListAvailable)) { 186 | Write-Warning "Please install SharePoint Online PowerShell module: 187 | http://www.microsoft.com/en-us/download/details.aspx?id=35588 188 | " 189 | return 190 | } 191 | 192 | # Define a new object to gather output 193 | [System.Collections.Generic.List[PSObject]]$spoSitesInfos = @() 194 | 195 | Write-Verbose 'Get SharePoint Online sites Details' 196 | if ($siteurl) { 197 | $spoSites = Get-SPOSite -Identity $SiteURL 198 | } 199 | else { 200 | if ($ExcludeOneDrive) { 201 | $spoSites = Get-SPOSite -Limit All -IncludePersonalSite $false 202 | } 203 | else { 204 | $spoSites = Get-SPOSite -Limit All -IncludePersonalSite $true 205 | } 206 | 207 | if ($OnlyOneDrive) { 208 | $spoSites = $spoSites | Where-Object { $_.Url -like '*-my.sharepoint.com/personal/*' } 209 | } 210 | } 211 | 212 | if ($M365GroupsDetails) { 213 | $directorySettings = (Get-AzureADDirectorySetting).Values 214 | if (-not($directorySettings)) { 215 | Write-Warning 'Unable to get AzureADDirectorySetting (default?)' 216 | } 217 | else { 218 | $groupCreation = ($directorySettings | Where-Object { $_.Name -eq 'EnableGroupCreation' }).Value 219 | $groupCreationAllowedGroupId = ($directorySettings | Where-Object { $_.Name -eq "GroupCreationAllowedGroupId" }).Value 220 | $allowToAddGuest = ($directorySettings | Where-Object { $_.Name -eq "AllowToAddGuests" }).Value 221 | $allowGuestsToAccessGroups = ($directorySettings | Where-Object { $_.Name -eq "AllowGuestsToAccessGroups" }).Value 222 | $allowGuestsToBeGroupOwner = ($directorySettings | Where-Object { $_.Name -eq "AllowGuestsToBeGroupOwner" }).Value 223 | 224 | if ($groupCreation) { 225 | if ($groupCreationAllowedGroupId) { 226 | Write-Host 'Microsoft 365 Groups (or Teams) creation only allows for :' 227 | Get-AzureADGroup -ObjectID $groupCreationAllowedGroupId 228 | } 229 | else { 230 | Write-Warning 'Microsoft 365 Groups (or Teams) creation only allows for all Microsoft 365 users.' 231 | } 232 | } 233 | else { 234 | Write-Host 'Microsoft 365 Groups (or Teams) creation disabled.' 235 | } 236 | 237 | if ($allowToAddGuest) { 238 | Write-Warning "Guest are enabled: 239 | `n`t Guest can be group owner : $allowGuestsToBeGroupOwner 240 | `n`t Guest can acces to group: $allowGuestsToAccessGroups" 241 | } 242 | else { 243 | Write-Host "Guest are disabled." 244 | } 245 | } 246 | 247 | $allM365Groups = Get-UnifiedGroup -ResultSize Unlimited 248 | 249 | $hash = @{} 250 | $hashWhenCreated = @{} 251 | 252 | $allM365Groups | ForEach-Object { 253 | if ($_.SharePointSiteUrl -and $_.PrimarySmtpAddress) { 254 | $hash.Add($_.SharePointSiteUrl, $_.PrimarySmtpAddress) 255 | 256 | } 257 | if ($_.SharePointSiteUrl -and $_.WhenCreatedUTC) { 258 | $hashWhenCreated.Add($_.SharePointSiteUrl, $_.WhenCreatedUTC) 259 | } 260 | } 261 | } 262 | 263 | Write-Verbose "SharePoint Online sites Count is $($spoSites.count)" 264 | 265 | foreach ($spoSite in $spoSites) { 266 | Write-Verbose "Get details for SharePoint site $($spoSite.Url)" 267 | 268 | $groupID = $null 269 | # Init variables 270 | $channelCount = $teamUsers = $TeamOwnerCount = $TeamMemberCount = $TeamGuestCount = $groupID = $siteOwner = $membersCount = $sharing = $sharingAllowedDomain = $sharingBlockedDomain = $groupID = $spoSiteAdmins = 'NULL' 271 | 272 | $ChannelCount = $teamUsers = $owners = $ownersCount = $membersCount = $guestsCount = $visibility = $archived = $team = $null 273 | 274 | $regionalSettings = $type = $null 275 | 276 | if ($M365GroupsDetails) { 277 | $teamsEnabled = $isM365Group = $false 278 | } 279 | 280 | if ($spoSite.SharingAllowedDomainList) { 281 | $sharingAllowedDomain = $spoSite.SharingAllowedDomainList -join '|' 282 | } 283 | 284 | if ($spoSite.SharingBlockedDomainList) { 285 | $sharingBlockedDomain = $spoSite.SharingBlockedDomainList -join '|' 286 | } 287 | 288 | if ($M365GroupsDetails) { 289 | $groupID = $sposite.RelatedGroupId.Guid 290 | 291 | if ($groupID -eq '00000000-0000-0000-0000-000000000000') { 292 | $type = 'SharePoint Site' 293 | } 294 | 295 | # Check if Microsoft 365 group 296 | elseif ($sposite.template -eq 'GROUP#0') { 297 | # https://office365itpros.com/2019/08/15/reporting-group-enabled-sharepoint-online-sites/ 298 | # do not working anymore because -detailed deprecated and not return groupID 299 | #$groupID = (Get-SpoSite $spoSite.Url -Detailed).GroupId.Guid 300 | 301 | #$groupID = $hash[$sposite.url][0] 302 | # $groupID = $sposite.RelatedGroupId.Guid 303 | 304 | # (Get-UnifiedGroup | Where-Object {$_.SharePointSiteUrl -eq $spoSite.url}).ExternalDirectoryObjectId 305 | 306 | # Check if the Microsoft 365 Group exists 307 | if ($null -ne $hash[$sposite.url]) { 308 | $membersCount = $M365Group.GroupMemberCount 309 | $type = 'M365 Group' 310 | 311 | # Check if Microsoft 365 group has a team 312 | try { 313 | $team = Get-Team -GroupId $GroupId -ErrorAction Stop 314 | $teamsEnabled = $true 315 | } 316 | catch { 317 | $teamsEnabled = $False 318 | } 319 | 320 | if ($teamsEnabled) { 321 | try { 322 | #Get channel details 323 | $channels = $null 324 | 325 | $channels = Get-TeamChannel -GroupId $groupId 326 | $ChannelCount = $channels.count 327 | 328 | # Get Owners, members and guests 329 | 330 | $teamUsers = Get-TeamUser -GroupId $groupId 331 | 332 | $owners = ($teamUsers | Where-Object { $_.Role -like 'owner' }).User -join '|' 333 | $ownersCount = ($teamUsers | Where-Object { $_.Role -like 'owner' }).count 334 | 335 | $membersCount = ($teamUsers | Where-Object { $_.Role -like 'member' }).count 336 | $guestsCount = ($teamUsers | Where-Object { $_.Role -like 'guest' }).count 337 | $visibility = $team.Visibility 338 | $archived = $team.Archived 339 | } 340 | catch { 341 | 342 | } 343 | } 344 | } 345 | else { 346 | $type = 'M365 group but not connected (?)' 347 | } 348 | } 349 | } 350 | 351 | if ($regionalSettingsDetails) { 352 | $regionalSettings = (Get-SPOSiteScriptFromWeb -WebUrl $sposite.url -IncludeRegionalSettings | ConvertFrom-Json).actions 353 | $lang = [globalization.cultureinfo][int]$sposite.localeid 354 | } 355 | # spo site admins need to be found by user/ cast to arry to get .count 356 | [array]$spoSiteAdmins = (Get-SPOUser -Site $spoSite.Url -Limit All | Where-Object { $_.IsSiteAdmin }).LoginName 357 | 358 | # source: https://onedrive.live.com/?authkey=%21AOu1SovQbowVNPU&cid=0CAD1DAC2D5DF9C0&id=CAD1DAC2D5DF9C0%211321&parId=CAD1DAC2D5DF9C0%21113&o=OneUp 359 | 360 | #Get all Groups from the site permissions 361 | if ($SharingRightsAdminOrFullControl) { 362 | $sitegroups = Get-SPOSiteGroup -Site $spoSite.Url -Limit 99999 363 | 364 | #Get Group info and members that have site owners permissions 365 | foreach ($sitegroup in $sitegroups) { 366 | if ($role.Contains('Site Owner')) { 367 | $roleSiteOwner = $sitegroup.Users 368 | } 369 | 370 | if ($role.Contains('Full Control')) { 371 | $roleFullControl = $sitegroup.Users 372 | } 373 | } 374 | } 375 | 376 | # Put all details into an object 377 | $params = [ordered] @{ 378 | SPTitle = $spoSite.Title 379 | GroupID = $groupId 380 | Url = $spoSite.Url 381 | StorageLimit = (($spoSite.StorageQuota) / 1024) 382 | StorageUsed = (($spoSite.StorageUsageCurrent) / 1024) 383 | Owner = $spoSite.Owner 384 | SiteAdmins = $spoSiteAdmins -join '|' 385 | SiteAdminsNumber = $spoSiteAdmins.count 386 | SiteAdminsMessage = "Please check 'My Site Secondary Admin' too https://-admin.sharepoint.com/_layouts/15/Online/PersonalSites.aspx?PersonalSitesOverridden=1" 387 | SharingCapability = $spoSite.SharingCapability 388 | SharingAllowedDomain = $spoSite.SharingAllowedDomainList 389 | SiteDefinedSharingCapability = $spoSite.SiteDefinedSharingCapability 390 | LockState = $spoSite.LockState 391 | LocaleID = $spoSite.LocaleID 392 | LocaleIDString = "$($lang.Name)|$($lang.DisplayName)" 393 | Timezone = $regionalSettings.timeZone 394 | TimezoneString = Convert-SPOTimezoneToString $regionalSettings.timeZone 395 | HourFormat = $regionalSettings.hourFormat 396 | SortOrder = $regionalSettings.sortOrder 397 | Template = $spoSite.Template 398 | ConditionalAccessPolicy = $spoSite.ConditionalAccessPolicy 399 | LastContentModifiedDate = $sposite.LastContentModifiedDate 400 | IsTeamsConnected = $sposite.IsTeamsConnected 401 | IsTeamsChannelConnected = $sposite.IsTeamsChannelConnected 402 | SensitivityLabel = $sposite.SensitivityLabel 403 | DefaultLinkPermission = $sposite.DefaultLinkPermission 404 | DefaultSharingLinkType = $sposite.DefaultSharingLinkType 405 | DefaultLinkToExistingAccess = $sposite.DefaultLinkToExistingAccess 406 | AnonymousLinkExpirationInDays = $sposite.AnonymousLinkExpirationInDays 407 | OverrideTenantAnonymousLinkExpirationPolicy = $sposite.OverrideTenantAnonymousLinkExpirationPolicy 408 | ExternalUserExpirationInDays = $sposite.ExternalUserExpirationInDays 409 | OverrideTenantExternalUserExpirationPolicy = $sposite.OverrideTenantExternalUserExpirationPolicy 410 | IsHubSite = $sposite.IsHubSite 411 | RoleFullControl = $roleFullControl 412 | RoleSiteOwner = $roleSiteOwner 413 | } 414 | 415 | # If Teams renamed, the DisplayName is not the same as the Title of the SPOsite 416 | if ($M365GroupsDetails) { 417 | $primarySMTPAddress = $hash[$spoSite.Url] 418 | $params.Add('PrimarySmtpAddress', $primarySMTPAddress) 419 | $params.Add('Type', $type) 420 | $params.Add('M365DisplayName', $team.DisplayName) 421 | $params.Add('M365WhenCreatedUTC', $hashWhenCreated[$spoSite.Url]) 422 | $params.Add('TeamDescription', $team.Description) 423 | $params.Add('TeamVisibility', $visibility) 424 | $params.Add('TeamArchived', $archived) 425 | $params.Add('TeamChannelCount', $ChannelCount) 426 | $params.Add('TeamOwners', $owners) 427 | $params.Add('TeamOwnersCount', $ownersCount) 428 | $params.Add('TeamMembersCount', $membersCount) 429 | $params.Add('TeamGuestsCount', $guestsCount) 430 | } 431 | 432 | $object = New-Object -Type PSObject -Property $params 433 | 434 | $spoSitesInfos.Add($object) 435 | } 436 | 437 | return $spoSitesInfos 438 | } -------------------------------------------------------------------------------- /SharePoint/Get-SPOSitesExternalUsers.ps1: -------------------------------------------------------------------------------- 1 | function Get-SPOSiteExternalUsers { 2 | Write-Host 'Retrieving SPO Sites...' -ForegroundColor Cyan 3 | $SPOSitesCollectionsAll = Get-SPOSite -Limit All -IncludePersonalSite $true 4 | 5 | [System.Collections.Generic.List[PSObject]]$spoSitesExternalUsersInfos = @() 6 | 7 | # get external user on each site (included personal onedrive site) 8 | foreach ($site in $SPOSitesCollectionsAll) { 9 | # used to fix the bug of get-spoexternaluser with modern sharing https://vladtalkstech.com/2018/03/bug-in-get-spoexternaluser-powershell-not-all-external-users-are-returned-anymore.html 10 | #$externalUsers = Get-SPOUser -Limit All -Site $site.Url | Where-Object { $_.LoginName -like "*urn:spo:guest*" -or $_.LoginName -like "*#ext#*" } 11 | 12 | # The Get-SPOExternalUser cmdlet has a limitation of returning first 50 users only 13 | #Read more: https://www.sharepointdiary.com/2017/11/sharepoint-online-find-all-external-users-using-powershell.html#ixzz76dzw6hws 14 | for ($i = 0; ; $i += 50) { 15 | $externalUsers = Get-SPOExternalUser -SiteUrl $site.Url -PageSize 50 -Position $i -ErrorAction SilentlyContinue 16 | 17 | if ($externalUsers.count -eq 0) { 18 | break 19 | } 20 | 21 | foreach ($externalUser in $externalUsers) { 22 | $object = [pscustomobject][ordered] @{ 23 | SiteCollectionUrl = $site.Url 24 | DisplayName = $externalUser.DisplayName 25 | OriginalEmail = $externalUser.Email 26 | AcceptedAs = $externalUser.AcceptedAs 27 | JoinDate = $externalUser.WhenCreated 28 | InvitedBy = $externalUser.InvitedBy 29 | } 30 | 31 | $spoSitesExternalUsersInfos.Add($object) 32 | } 33 | } 34 | } 35 | } --------------------------------------------------------------------------------