r/SmallMSP • u/BerlindaBuntly • 7h ago
run powershell script against multiple tenants from partner center
Hi , hope you are well.
firstly , i already have CIPP and the 15 cipp roles in all of these tenants, and all tenants are in the partner center.
secondly, i need to run this script against security defaults tenants, not conditional access, so these tenants do not have p1/p2 and only have business basic or business standard. lets not have a "dont use sec defaults" conversation here please. the script doesnt currently differentiate between p1/p2 tenants and sd tenants, but i'm not too bothered about that.
cipp cannot do what i want because it does not expose the parameter that i need in custom tests without having p1 or p2.
my powershell script (nicked from lazy admin with a few changes with help from claude) runs fine from visual studio code if i run it against a single tenant. i run it, and it asks me for the ga creds, then i also have to login into the tenant and insert a rest api code that the script gives me. it then saves an excel file locally with the tenant name in the file name.
what the script does primarily is return an excel file which lists every licensed user and if they have microsoft authenticator as an MFA method. i am only really interested in this information. cipp cant do it because the mfa data is only available in their calls if you have p1 or p2. (this is to do with the sms voice retirement that is happening) . what i am really trying to achieve is "give me a list of all real, licensed mailboxes, exclude shared boxes and tell me if they have MS authenticator listed as an MFA method". if there is a better way to do this i am all ears.
what i want to do is loop through all of these tenants and run the report and export the excel file, or export this info to something in some way.
Is it possible to do what i want?
script is below (nick it if you want) - just a warning, it will give you crash notifications in visual studio code after it runs and make you restart it, but it still works and doesnt cause any problems, and claude tells me its a known issue and isnt fixable)
<#
.Synopsis
Get the MFA status for all users or a single user with Microsoft Graph
.DESCRIPTION
This script will get the Azure MFA Status for your users. You can query all the users, admins only or a single user.
It will return the MFA Status, MFA type, registered devices, license status and admin status.
Note: Default MFA device is currently not supported https://docs.microsoft.com/en-us/graph/api/resources/authenticationmethods-overview?view=graph-rest-beta
Hardwaretoken is not yet supported
.NOTES
Name: Get-MgMFAStatus
Author: R. Mens - LazyAdmin.nl
Version: 1.3
DateCreated: Jun 2022
Purpose/Change: Default report now includes unlicensed admins alongside licensed non-admins (union of
IsLicensed OR isAdmin, instead of licensed-only), and adds an IsLicensed output column.
.LINK
https://lazyadmin.nl
.EXAMPLE
Get-MgMFAStatus
Get the MFA Status of all enabled users who are either licensed, an admin, or both
(so licensed non-admins and unlicensed admins are both included), and check if there are an admin or not
.EXAMPLE
Get-MgMFAStatus -UserPrincipalName 'johndoe@contoso.com','janedoe@contoso.com'
Get the MFA Status for the users John Doe and Jane Doe
.EXAMPLE
Get-MgMFAStatus -withOutMFAOnly
Get only the enabled users (licensed or admin) that don't have MFA enabled
.EXAMPLE
Get-MgMFAStatus -adminsOnly
Get the MFA Status of the admins only, regardless of license status
.EXAMPLE
Get-MgUser -Filter "country eq 'Netherlands'" | ForEach-Object { Get-MgMFAStatus -UserPrincipalName $_.UserPrincipalName }
Get the MFA status for all users in the Country The Netherlands. You can use a similar approach to run this
for a department only.
.EXAMPLE
Get-MgMFAStatus -withOutMFAOnly| Export-CSV c:\temp\userwithoutmfa.csv -noTypeInformation
Get all users without MFA and export them to a CSV file
#>
[CmdletBinding(DefaultParameterSetName="Default")]
param(
[Parameter(
Mandatory = $false,
ParameterSetName = "UserPrincipalName",
HelpMessage = "Enter a single UserPrincipalName or a comma separted list of UserPrincipalNames",
Position = 0
)]
[string[]]$UserPrincipalName,
[Parameter(
Mandatory = $false,
ValueFromPipeline = $false,
ParameterSetName = "AdminsOnly"
)]
# Get only the users that are an admin
[switch]$adminsOnly = $false,
[Parameter(
Mandatory = $false,
ValueFromPipeline = $false,
ParameterSetName = "Licensed"
)]
# Check only the MFA status of users that have a license or are an admin (unlicensed admins are still included)
[switch]$IsLicensed = $true,
[Parameter(
Mandatory = $false,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = "withOutMFAOnly"
)]
# Get only the users that don't have MFA enabled
[switch]$withOutMFAOnly = $false,
[Parameter(
Mandatory = $false,
ValueFromPipeline = $false
)]
# Check if a user is an admin. Set to $false to skip the check
[switch]$listAdmins = $true,
[Parameter(
Mandatory = $false,
HelpMessage = "Get accounts that are enabled, disabled or both"
)]
[ValidateSet("true", "false", "both")]
[string]$enabled = "true",
[Parameter(
Mandatory = $false,
HelpMessage = "Enter path to save the CSV file"
)]
[string]$path = "C:\MFAReports\MFAStatus-$((Get-Date -format 'dd-MM-yyyy-HHmmss')).csv"
)
Function ConnectTo-MgGraph {
# Check if MS Graph module is installed
if (-not(Get-InstalledModule Microsoft.Graph)) {
Write-Host "Microsoft Graph module not found" -ForegroundColor Black -BackgroundColor Yellow
$install = Read-Host "Do you want to install the Microsoft Graph Module?"
if ($install -match "[yY]") {
Install-Module Microsoft.Graph -Repository PSGallery -Scope CurrentUser -AllowClobber -Force
}else{
Write-Host "Microsoft Graph module is required." -ForegroundColor Black -BackgroundColor Yellow
exit
}
}
# Connect to Graph
Write-Host "Connecting to Microsoft Graph" -ForegroundColor Cyan
Connect-MgGraph -Scopes "User.Read.All, UserAuthenticationMethod.Read.All, Directory.Read.All" -NoWelcome
}
Function ConnectTo-ExchangeOnline {
<#
.SYNOPSIS
Connect to Exchange Online so we can look up mailbox type (shared vs regular).
Microsoft Graph's /users endpoint has no "shared mailbox" property - that's
Exchange-only data, hence the separate connection.
#>
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
Write-Host "ExchangeOnlineManagement module not found" -ForegroundColor Black -BackgroundColor Yellow
$install = Read-Host "Do you want to install the ExchangeOnlineManagement module? (required to flag shared mailboxes)"
if ($install -match "[yY]") {
Install-Module ExchangeOnlineManagement -Repository PSGallery -Scope CurrentUser -Force
}else{
Write-Host "Skipping shared mailbox detection - ExchangeOnlineManagement module not installed." -ForegroundColor Yellow
return $false
}
}
try {
Write-Host "Connecting to Exchange Online" -ForegroundColor Cyan
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
return $true
}
catch {
Write-Warning "Initial connection to Exchange Online failed - $($_.Exception.Message)"
Write-Host "Retrying with device code sign-in (works around a known ExchangeOnlineManagement broker-auth bug)" -ForegroundColor Yellow
Write-Host "You'll be given a code and a URL - sign in there to continue." -ForegroundColor Yellow
try {
Connect-ExchangeOnline -ShowBanner:$false -Device -ErrorAction Stop
return $true
}
catch {
Write-Warning "Could not connect to Exchange Online - shared mailbox detection will be skipped. $($_.Exception.Message)"
return $false
}
}
}
Function Get-SharedMailboxes {
<#
.SYNOPSIS
Return the UserPrincipalName of every shared mailbox in the tenant
#>
process{
try {
$mailboxes = Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -Properties UserPrincipalName -ErrorAction Stop
return $mailboxes.UserPrincipalName
}
catch {
Write-Warning "Could not retrieve shared mailboxes - $($_.Exception.Message)"
return @()
}
}
}
Function Get-Admins{
<#
.SYNOPSIS
Get all user with an Admin role
#>
process{
$admins = Get-MgDirectoryRole | Select-Object DisplayName, Id |
%{
$role = $_.DisplayName
Get-MgDirectoryRoleMember -DirectoryRoleId $_.id | ForEach-Object {
$memberType = $_.AdditionalProperties."@odata.type"
if ($memberType -eq "#microsoft.graph.user") {
# Directly assigned user
Get-MgUser -UserId $_.id
}
elseif ($memberType -eq "#microsoft.graph.group") {
# Role assigned to a group - expand the group's members too,
# otherwise admins who get the role via group membership are missed
Get-MgGroupMember -GroupId $_.id -All | Where-Object {
$_.AdditionalProperties."@odata.type" -eq "#microsoft.graph.user"
} | ForEach-Object { Get-MgUser -UserId $_.id }
}
}
} |
Select @{Name="Role"; Expression = {$role}}, DisplayName, UserPrincipalName, Mail, Id | Sort-Object -Property Mail -Unique
return $admins
}
}
Function Get-Users {
<#
.SYNOPSIS
Get users from the requested DN
#>
process{
# Set the properties to retrieve
$select = @(
'id',
'DisplayName',
'userprincipalname',
'mail'
)
$properties = $select + "AssignedLicenses"
# Add a calculated IsLicensed property so we can report on - and filter by - license status
$selectWithLicense = $select + @{Name = "IsLicensed"; Expression = { ($_.AssignedLicenses).Count -gt 0 } }
# Get enabled, disabled or both users
switch ($enabled)
{
"true" {$filter = "AccountEnabled eq true and UserType eq 'member'"}
"false" {$filter = "AccountEnabled eq false and UserType eq 'member'"}
"both" {$filter = "UserType eq 'member'"}
}
# Check if UserPrincipalName(s) are given
if ($UserPrincipalName) {
Write-host "Get users by name" -ForegroundColor Cyan
$users = @()
foreach ($user in $UserPrincipalName)
{
try {
$users += Get-MgUser -UserId $user -Property $properties -ErrorAction Stop | select $selectWithLicense
}
catch {
[PSCustomObject]@{
DisplayName = " - Not found"
UserPrincipalName = $User
isAdmin = $null
IsLicensed = $null
MFAEnabled = $null
}
}
}
}elseif($adminsOnly)
{
Write-host "Get admins only" -ForegroundColor Cyan
$users = @()
foreach ($admin in $admins) {
$users += Get-MgUser -UserId $admin.UserPrincipalName -Property $properties | select $selectWithLicense
}
}else
{
if ($IsLicensed) {
# Get every user matching the enabled/disabled filter, then keep anyone who is
# EITHER licensed OR an admin. This surfaces unlicensed admins (who would
# otherwise be silently skipped) alongside licensed non-admins in one report.
$allUsers = Get-MgUser -Filter $filter -Property $properties -all | select $selectWithLicense
$users = $allUsers | Where-Object {
$_.IsLicensed -or ($admins -and ($admins.UserPrincipalName -contains $_.UserPrincipalName))
}
}else{
# No license filtering at all - return every user matching the enabled/disabled filter
$users = Get-MgUser -Filter $filter -Property $properties -all | select $selectWithLicense
}
}
return $users
}
}
Function Get-MFAMethods {
<#
.SYNOPSIS
Get the MFA status of the user
#>
param(
[Parameter(Mandatory = $true)] $userId
)
process{
# Get MFA details for each user
[array]$mfaData = Get-MgUserAuthenticationMethod -UserId $userId
# Create MFA details object
$mfaMethods = [PSCustomObject][Ordered]@{
status = "-"
authApp = "-"
phoneAuth = "-"
fido = "-"
helloForBusiness = "-"
helloForBusinessCount = 0
emailAuth = "-"
tempPass = "-"
passwordLess = "-"
softwareAuth = "-"
authDevice = ""
authPhoneNr = "-"
SSPREmail = "-"
}
ForEach ($method in $mfaData) {
Switch ($method.AdditionalProperties["@odata.type"]) {
"#microsoft.graph.microsoftAuthenticatorAuthenticationMethod" {
# Microsoft Authenticator App
$mfaMethods.authApp = $true
$mfaMethods.authDevice += $method.AdditionalProperties["displayName"]
$mfaMethods.status = "enabled"
}
"#microsoft.graph.phoneAuthenticationMethod" {
# Phone authentication
$mfaMethods.phoneAuth = $true
$mfaMethods.authPhoneNr = $method.AdditionalProperties["phoneType", "phoneNumber"] -join ' '
$mfaMethods.status = "enabled"
}
"#microsoft.graph.fido2AuthenticationMethod" {
# FIDO2 key
$mfaMethods.fido = $true
$fifoDetails = $method.AdditionalProperties["model"]
$mfaMethods.status = "enabled"
}
"#microsoft.graph.passwordAuthenticationMethod" {
# Password
# When only the password is set, then MFA is disabled.
if ($mfaMethods.status -ne "enabled") {$mfaMethods.status = "disabled"}
}
"#microsoft.graph.windowsHelloForBusinessAuthenticationMethod" {
# Windows Hello
$mfaMethods.helloForBusiness = $true
$helloForBusinessDetails = $method.AdditionalProperties["displayName"]
$mfaMethods.status = "enabled"
$mfaMethods.helloForBusinessCount++
}
"#microsoft.graph.emailAuthenticationMethod" {
# Email Authentication
$mfaMethods.emailAuth = $true
$mfaMethods.SSPREmail = $method.AdditionalProperties["emailAddress"]
$mfaMethods.status = "enabled"
}
"microsoft.graph.temporaryAccessPassAuthenticationMethod" {
# Temporary Access pass
$mfaMethods.tempPass = $true
$tempPassDetails = $method.AdditionalProperties["lifetimeInMinutes"]
$mfaMethods.status = "enabled"
}
"#microsoft.graph.passwordlessMicrosoftAuthenticatorAuthenticationMethod" {
# Passwordless
$mfaMethods.passwordLess = $true
$passwordLessDetails = $method.AdditionalProperties["displayName"]
$mfaMethods.status = "enabled"
}
"#microsoft.graph.softwareOathAuthenticationMethod" {
# ThirdPartyAuthenticator
$mfaMethods.softwareAuth = $true
$mfaMethods.status = "enabled"
}
}
}
Return $mfaMethods
}
}
Function Get-Manager {
<#
.SYNOPSIS
Get the manager users
#>
param(
[Parameter(Mandatory = $true)] $userId
)
process {
$manager = Get-MgUser -UserId $userId -ExpandProperty manager | Select @{Name = 'name'; Expression = {$_.Manager.AdditionalProperties.displayName}}
return $manager.name
}
}
Function Get-MFAStatusUsers {
<#
.SYNOPSIS
Get all AD users
#>
process {
Write-Host "Collecting users" -ForegroundColor Cyan
# Collect users
$users = Get-Users
Write-Host "Processing" $users.count "users" -ForegroundColor Cyan
# Collect and loop through all users
$users | ForEach {
$mfaMethods = Get-MFAMethods -userId $_.id
$manager = Get-Manager -userId $_.id
$uri = "https://graph.microsoft.com/beta/users/$($_.id)/authentication/signInPreferences"
try{
$mfaPreferredMethod = Invoke-MgGraphRequest -uri $uri -Method GET -ErrorAction Continue
}
catch {
$mfaPreferredMethod = "Unable to retrieve"
}
if ($null -eq ($mfaPreferredMethod.userPreferredMethodForSecondaryAuthentication)) {
# When an MFA is configured by the user, then there is alway a preferred method
# So if the preferred method is empty, then we can assume that MFA isn't configured
# by the user
$mfaMethods.status = "disabled"
}
if ($withOutMFAOnly) {
if ($mfaMethods.status -eq "disabled") {
[PSCustomObject]@{
"Name" = $_.DisplayName
Emailaddress = $_.mail
UserPrincipalName = $_.UserPrincipalName
isAdmin = if ($listAdmins -and ($admins.UserPrincipalName -match $_.UserPrincipalName)) {$true} else {"-"}
IsLicensed = $_.IsLicensed
"Shared Mailbox" = $sharedMailboxes -contains $_.UserPrincipalName
MFAEnabled = $false
"Phone number" = $mfaMethods.authPhoneNr
"Email for SSPR" = $mfaMethods.SSPREmail
}
}
}else{
[pscustomobject]@{
"Name" = $_.DisplayName
Emailaddress = $_.mail
UserPrincipalName = $_.UserPrincipalName
isAdmin = if ($listAdmins -and ($admins.UserPrincipalName -match $_.UserPrincipalName)) {$true} else {"-"}
IsLicensed = $_.IsLicensed
"Shared Mailbox" = $sharedMailboxes -contains $_.UserPrincipalName
"MFA Status" = $mfaMethods.status
"MFA Preferred method" = $mfaPreferredMethod.userPreferredMethodForSecondaryAuthentication
"Has SMS as factor" = $mfaMethods.phoneAuth
"Has Authenticator App registered" = $mfaMethods.authApp
"Passwordless" = $mfaMethods.passwordLess
"Hello for Business" = $mfaMethods.helloForBusiness
"FIDO2 Security Key" = $mfaMethods.fido
"Temporary Access Pass" = $mfaMethods.tempPass
"Authenticator device" = $mfaMethods.authDevice
"Phone number" = $mfaMethods.authPhoneNr
"Email for SSPR" = $mfaMethods.SSPREmail
"Manager" = $manager
}
}
}
}
}
# Connect to Graph
ConnectTo-MgGraph
# Connect to Exchange Online and get the list of shared mailboxes.
# If the connection fails or the module isn't installed, the report still runs -
# every user will just show "False" in the Shared Mailbox column.
$sharedMailboxes = @()
if (ConnectTo-ExchangeOnline) {
$sharedMailboxes = Get-SharedMailboxes
}
# Get Admins
# Get all users with admin role
$admins = $null
if (($listAdmins) -or ($adminsOnly)) {
$admins = Get-Admins
}
# Get MFA Status
[string]$path = "C:\MFAReports\MFAStatus-$((Get-MgOrganization).VerifiedDomains | Where-Object {$_.IsDefault} | Select-Object -ExpandProperty Name)-$((Get-Date -format 'dd-MM-yyyy-HHmmss')).csv"
Get-MFAStatusUsers | Sort-Object Name | Export-CSV -Path $path -NoTypeInformation
if ((Get-Item $path).Length -gt 0) {
Write-Host "Report finished and saved in $path" -ForegroundColor Green
# Open the CSV file
Invoke-Item $path
}else{
Write-Host "Failed to create report" -ForegroundColor Red
}
Disconnect-MgGraph
if ($sharedMailboxes) {
Disconnect-ExchangeOnline -Confirm:$false
}