Private/Utils.ps1

<#
.SYNOPSIS
    Converts a StoreId to a RestId.
.DESCRIPTION
    This internal helper function takes a folder StoreId and converts it to a RestId that can be used with the Microsoft Graph API.
.PARAMETER StoreId
    The StoreId of the folder.
.PARAMETER UserId
    The Id of the user who owns the folder.
.NOTES
    This function is not intended to be called directly.
#>

function Convert-StoreIdToRestId {
    [CmdletBinding()]
    param(
        [string]$StoreId,
        [string]$UserId
    )

    # convert from base64 to bytes
    $folderIdBytes = [Convert]::FromBase64String($StoreId)

    # convert byte array to string, remove '-' and ignore first byte
    $folderIdHexString = [System.BitConverter]::ToString($folderIdBytes).Replace('-','')
    $folderIdHexStringLength = $folderIdHexString.Length

    # get hex entry id string by removing first and last byte
    $entryIdHexString = $folderIdHexString.SubString(2,($folderIdHexStringLength-4))

    # convert to byte array - two chars represents one byte
    $entryIdBytes = [byte[]]::new($entryIdHexString.Length / 2)

    for($i=0; $i -lt $entryIdHexString.Length; $i+=2){
        $entryIdTwoChars = $entryIdHexString.Substring($i, 2)
        $entryIdBytes[$i/2] = [convert]::ToByte($entryIdTwoChars, 16)
    }

    # convert bytes to base64 string
    $entryIdBase64 = [Convert]::ToBase64String($entryIdBytes)

    # count how many '=' contained in base64 entry id
    $equalCharCount = $entryIdBase64.Length - $entryIdBase64.Replace('=','').Length

    # trim '=', replace '/' with '-', replace '+' with '_' and add number of '=' at the end
    $EntryId = $entryIdBase64.TrimEnd('=').Replace('/','_').Replace('+','-')+$equalCharCount

    # Now convert the entryId to be a RestId using the translateExchangeIds API
    $Body = @{}
    [array]$InputId = $EntryId
    $Body.Add("inputIds", $InputId)
    $Body.Add("sourceIdType", "entryId")
    $Body.Add("targetIdType", "restid")

    $Data = Invoke-MgTranslateUserExchangeId -UserId $UserId -BodyParameter $Body
    return $Data.targetId
}

<#
.SYNOPSIS
    Gets a list of administrative users.
.DESCRIPTION
    This internal helper function gets a list of users with the 'Global Administrator' or 'User Administrator' roles.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365Admin {
    [CmdletBinding()]
    param()

    $AdminRoleHolders = [System.Collections.Generic.List[Object]]::new()
    [array]$AdminRoles = Get-MgDirectoryRole | Select-Object DisplayName, Id | Sort-Object DisplayName
    $AdminRoles = $AdminRoles | Where-Object {$_.DisplayName -eq "Global Administrator" -or $_.DisplayName -eq "User Administrator"}
    ForEach ($Role in $AdminRoles) {
        [array]$RoleMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $Role.Id | Where-Object {$_.AdditionalProperties."@odata.type" -eq "#microsoft.graph.user"}
        ForEach ($Member in $RoleMembers) {
            $UserDetails = Get-MgUser -UserId $Member.Id
            $ReportLine  = [PSCustomObject] @{
                User   = $UserDetails.UserPrincipalName
                Id     = $UserDetails.Id
                Role   = $Role.DisplayName
                RoleId = $Role.Id
                Mail   = $UserDetails.Mail
            }
            $AdminRoleHolders.Add($ReportLine)
        }
    }
    return $AdminRoleHolders | Sort-Object User -Unique
}

<#
.SYNOPSIS
    Recursively gets all files and folders from a user's OneDrive.
.DESCRIPTION
    This internal helper function recursively gets all files and folders from a user's OneDrive.
.PARAMETER UserId
    The Id of the user.
.PARAMETER FolderId
    The Id of the folder to start from.
.PARAMETER RelativePath
    The relative path of the folder.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365UserOneDriveFilesRecursive {
    [CmdletBinding()]
    param(
        [string]$UserId,
        [string]$FolderId,
        [string]$RelativePath = ''
    )

    $Items = Get-MgUserDriveItemChild -UserId $UserId -DriveId $UserId -DriveItemId $FolderId
    foreach ($Item in $Items) {
        if ($Item.Folder) {
            $NewPath = Join-Path -Path $RelativePath -ChildPath $Item.Name
            Get-O365UserOneDriveFilesRecursive -UserId $UserId -FolderId $Item.Id -RelativePath $NewPath
        }
        else {
            $Item | Add-Member -MemberType NoteProperty -Name "RelativePath" -Value $RelativePath -PassThru
        }
    }
}

<#
.SYNOPSIS
    Downloads a file from OneDrive.
.DESCRIPTION
    This internal helper function downloads a file from OneDrive to a local path.
.PARAMETER UserId
    The Id of the user.
.PARAMETER DriveItemId
    The Id of the file to download.
.PARAMETER LocalPath
    The local path to save the file to.
.PARAMETER RelativePath
    The relative path of the file.
.NOTES
    This function is not intended to be called directly.
#>

function Download-O365File {
    [CmdletBinding()]
    param(
        [string]$UserId,
        [string]$DriveItemId,
        [string]$LocalPath,
        [string]$RelativePath
    )

    $File = Get-MgUserDriveItem -UserId $UserId -DriveId $UserId -DriveItemId $DriveItemId
    $DownloadUrl = $File.AdditionalProperties."@microsoft.graph.downloadUrl"

    $FullDirectoryPath = Join-Path -Path $LocalPath -ChildPath $RelativePath
    if (-not (Test-Path -Path $FullDirectoryPath)) {
        New-Item -ItemType Directory -Path $FullDirectoryPath
    }

    $OutputPath = Join-Path -Path $FullDirectoryPath -ChildPath $File.Name
    Invoke-RestMethod -Uri $DownloadUrl -OutFile $OutputPath
}

<#
.SYNOPSIS
    Removes a user from all Microsoft 365 groups.
.DESCRIPTION
    This internal helper function removes a user from all Microsoft 365 groups they are a member of.
.PARAMETER UserId
    The Id of the user.
.NOTES
    This function is not intended to be called directly.
#>

function Remove-O365UserFromGroups {
    [CmdletBinding()]
    param(
        [string]$UserId
    )

    $Groups = Get-MgUserMemberOf -UserId $UserId
    foreach ($Group in $Groups) {
        if ($Group.AdditionalProperties."@odata.type" -eq "#microsoft.graph.group") {
            Write-Verbose "Removing user from group: $($Group.DisplayName)"
            Remove-MgGroupMemberByRef -GroupId $Group.Id -DirectoryObjectId $UserId
        }
    }
}

<#
.SYNOPSIS
    Gets admins who haven't signed in for a specified number of days.
.DESCRIPTION
    This internal helper function gets a list of administrative users who have not signed in for a specified number of days.
.PARAMETER Days
    The number of days to check for stale sign-ins.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365StaleAdminSignIns {
    [CmdletBinding()]
    param(
        [int]$Days = 30
    )

    $Admins = Get-O365Admin
    $StaleAdmins = [System.Collections.Generic.List[Object]]::new()
    foreach ($Admin in $Admins) {
        $lastSignIn = (Get-MgUser -UserId $Admin.Id -Property 'signInActivity').SignInActivity.LastSignInDateTime
        if ($lastSignIn -and $lastSignIn -lt (Get-Date).AddDays(-$Days)) {
            $StaleAdmins.Add($Admin)
        }
    }
    return $StaleAdmins
}

<#
.SYNOPSIS
    Gets guest users who haven't signed in for a specified number of days.
.DESCRIPTION
    This internal helper function gets a list of guest users who have not signed in for a specified number of days.
.PARAMETER Days
    The number of days to check for stale sign-ins.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365InactiveGuestUsers {
    [CmdletBinding()]
    param(
        [int]$Days = 30
    )

    $Guests = Get-MgUser -Filter "userType eq 'Guest'" -All -Property 'signInActivity'
    $InactiveGuests = [System.Collections.Generic.List[Object]]::new()
    foreach ($Guest in $Guests) {
        $lastSignIn = $Guest.SignInActivity.LastSignInDateTime
        if ($lastSignIn -and $lastSignIn -lt (Get-Date).AddDays(-$Days)) {
            $InactiveGuests.Add($Guest)
        }
    }
    return $InactiveGuests
}

<#
.SYNOPSIS
    Gets users who have not registered for MFA.
.DESCRIPTION
    This internal helper function gets a list of users who have not registered for MFA.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365UsersWithoutMFA {
    [CmdletBinding()]
    param()

    $Users = Get-MgUser -All -Property 'displayName,userPrincipalName'
    $UsersWithoutMFA = [System.Collections.Generic.List[Object]]::new()
    foreach ($User in $Users) {
        $MfaMethods = Get-MgUserAuthenticationMethod -UserId $User.Id
        if ($MfaMethods.Count -eq 0) {
            $UsersWithoutMFA.Add($User)
        }
    }
    return $UsersWithoutMFA
}

<#
.SYNOPSIS
    Gets mailboxes with forwarding rules to external domains.
.DESCRIPTION
    This internal helper function gets a list of mailboxes that have forwarding rules to external domains.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365MailboxForwardingRules {
    [CmdletBinding()]
    param()

    $Mailboxes = Get-Mailbox -ResultSize Unlimited
    $ForwardingMailboxes = [System.Collections.Generic.List[Object]]::new()
    $AcceptedDomains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName
    foreach ($Mailbox in $Mailboxes) {
        if ($Mailbox.ForwardingSmtpAddress) {
            $Domain = $Mailbox.ForwardingSmtpAddress.Split('@')[1]
            if ($Domain -notin $AcceptedDomains) {
                $ForwardingMailboxes.Add($Mailbox)
            }
        }
    }
    return $ForwardingMailboxes
}

<#
.SYNOPSIS
    Gets service principals with no sign-ins.
.DESCRIPTION
    This internal helper function gets a list of service principals that have not been signed into.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O356UnsignedInServicePrincipals {
    [CmdletBinding()]
    param()

    $ServicePrincipals = Get-MgServicePrincipal -All
    $UnsignedInServicePrincipals = [System.Collections.Generic.List[Object]]::new()
    foreach ($SP in $ServicePrincipals) {
        $SignIn = Get-MgAuditLogSignIn -Filter "servicePrincipalId eq '$($SP.Id)'" -Top 1
        if (-not $SignIn) {
            $UnsignedInServicePrincipals.Add($SP)
        }
    }
    return $UnsignedInServicePrincipals
}

<#
.SYNOPSIS
    Adds a user to the same groups as a template user.
.DESCRIPTION
    This internal helper function adds a user to the same Microsoft 365 groups as a template user.
.PARAMETER UserId
    The Id of the user to add to the groups.
.PARAMETER TemplateUserId
    The Id of the template user.
.NOTES
    This function is not intended to be called directly.
#>

function Add-O365UserToGroupsFromTemplate {
    [CmdletBinding()]
    param(
        [string]$UserId,
        [string]$TemplateUserId
    )

    $Groups = Get-MgUserMemberOf -UserId $TemplateUserId
    foreach ($Group in $Groups) {
        if ($Group.AdditionalProperties."@odata.type" -eq "#microsoft.graph.group") {
            Write-Verbose "Adding user to group: $($Group.DisplayName)"
            Add-MgGroupMember -GroupId $Group.Id -DirectoryObjectId $UserId
        }
    }
}

<#
.SYNOPSIS
    Gets users with a specific license who have not signed in for a certain number of days.
.DESCRIPTION
    This internal helper function gets a list of users with a specific license who have not signed in for a certain number of days.
.PARAMETER SkuId
    The SKU ID of the license to check.
.PARAMETER Days
    The number of days to check for stale sign-ins.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365UnderutilizedLicense {
    [CmdletBinding()]
    param(
        [string]$SkuId,
        [int]$Days = 30
    )

    $Users = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $SkuId)" -All -Property 'displayName,userPrincipalName,signInActivity'
    $UnderutilizedUsers = [System.Collections.Generic.List[Object]]::new()
    foreach ($User in $Users) {
        $lastSignIn = $User.SignInActivity.LastSignInDateTime
        if ($lastSignIn -and $lastSignIn -lt (Get-Date).AddDays(-$Days)) {
            $UnderutilizedUsers.Add($User)
        }
    }
    return $UnderutilizedUsers
}

<#
.SYNOPSIS
    Gets all SharePoint Online sites.
.DESCRIPTION
    This internal helper function gets all SharePoint Online sites.
.NOTES
    This function is not intended to be called directly.
#>

function Get-SPOAllSites {
    [CmdletBinding()]
    param()

    return Get-SPOSite -Limit All
}

<#
.SYNOPSIS
    Gets all externally shared items for a given site.
.DESCRIPTION
    This internal helper function gets all externally shared files and folders for a given site.
.PARAMETER SiteUrl
    The URL of the site to check.
.NOTES
    This function is not intended to be called directly.
#>

function Get-SPOExternallySharedItems {
    [CmdletBinding()]
    param(
        [string]$SiteUrl
    )

    Connect-PnPOnline -Url $SiteUrl -Interactive
    $ExternallySharedItems = [System.Collections.Generic.List[Object]]::new()
    $Lists = Get-PnPList
    foreach ($List in $Lists) {
        $Items = Get-PnPListItem -List $List.Title
        foreach ($Item in $Items) {
            if ($Item.HasUniqueRoleAssignments) {
                $RoleAssignments = Get-PnPProperty -ClientObject $Item -Property RoleAssignments
                foreach ($RoleAssignment in $RoleAssignments) {
                    $Member = Get-PnPProperty -ClientObject $RoleAssignment -Property Member
                    if ($Member.PrincipalType -eq 'User' -and $Member.LoginName -match '#ext#') {
                        $ExternallySharedItems.Add($Item)
                    }
                }
            }
        }
    }
    return $ExternallySharedItems
}

<#
.SYNOPSIS
    Gets the current global tenant settings (External Sharing, Guest Access).
.DESCRIPTION
    This internal helper function retrieves the current global tenant settings related to external sharing and guest access.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365GlobalTenantSettings {
    [CmdletBinding()]
    param()

    # Placeholder for actual cmdlet calls
    # In a real scenario, this would involve cmdlets like Get-MsolDirSyncFeatures, Get-SPOTenant, Get-AzureADPolicy, etc.
    # For now, we will return a dummy object.
    
    return [PSCustomObject]@{
        ExternalSharingEnabled = $true # Placeholder
        GuestAccessAllowed = $true # Placeholder
    }
}

<#
.SYNOPSIS
    Sets the global tenant settings (External Sharing, Guest Access).
.DESCRIPTION
    This internal helper function sets the global tenant settings related to external sharing and guest access.
.PARAMETER ExternalSharingEnabled
    Set to $true to enable external sharing, $false to disable.
.PARAMETER GuestAccessAllowed
    Set to $true to allow guest access, $false to deny.
.NOTES
    This function is not intended to be called directly.
#>

function Set-O365GlobalTenantSettings {
    [CmdletBinding()]
    param(
        [bool]$ExternalSharingEnabled,
        [bool]$GuestAccessAllowed
    )

    Write-Verbose "Setting ExternalSharingEnabled to $ExternalSharingEnabled"
    Write-Verbose "Setting GuestAccessAllowed to $GuestAccessAllowed"
    # Placeholder for actual cmdlet calls
    # For example: Set-SPOTenant -ExternalSharing $ExternalSharingEnabled; Set-MsolDirSyncFeatures -Feature GuestAccess -Enable $GuestAccessAllowed
}

<#
.SYNOPSIS
    Gets the current MFA status for all users.
.DESCRIPTION
    This internal helper function retrieves the current MFA status for all users in the tenant.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365UserMFAStatus {
    [CmdletBinding()]
    param()

    # Placeholder for actual cmdlet calls
    # In a real scenario, this would involve Get-MsolUser or checking authentication methods via Graph.
    # For now, we return a dummy object indicating some users are not MFA enabled.
    return [PSCustomObject]@{
        AllUsersMFAEnabled = $false # Placeholder
        ExceptionUsers = @('user1@contoso.com', 'user2@contoso.com') # Placeholder
    }
}

<#
.SYNOPSIS
    Sets the MFA status for users.
.DESCRIPTION
    This internal helper function sets the MFA status for users.
.PARAMETER AllUsersMFAEnabled
    Set to $true to enable MFA for all users, $false to disable.
.PARAMETER ExceptionUsers
    An array of user UPNs to exclude from the MFA enforcement.
.NOTES
    Directly enabling/disabling MFA for all users via PowerShell is complex and often controlled by Conditional Access policies or security defaults.
    This function is a placeholder and does not implement direct remediation for "AllUsersMFAEnabled" to avoid complexity and deprecated methods.
#>

function Set-O365UserMFAStatus {
    [CmdletBinding()]
    param(
        [bool]$AllUsersMFAEnabled,
        [string[]]$ExceptionUsers
    )

    Write-Verbose "Setting AllUsersMFAEnabled to $AllUsersMFAEnabled (Placeholder)"
    Write-Verbose "ExceptionUsers: $ExceptionUsers (Placeholder)"
    # Placeholder for actual cmdlet calls
}

<#
.SYNOPSIS
    Gets the current mailbox forwarding settings for all mailboxes.
.DESCRIPTION
    This internal helper function retrieves the current mailbox forwarding settings for all mailboxes.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365MailboxForwardingSettings {
    [CmdletBinding()]
    param()

    # Placeholder
    return [PSCustomObject]@{
        ExternalForwardingAllowed = $false # Placeholder
        InternalOnlyForwarding = $true # Placeholder
    }
}

<#
.SYNOPSIS
    Sets the mailbox forwarding settings.
.DESCRIPTION
    This internal helper function sets the mailbox forwarding settings.
.PARAMETER ExternalForwardingAllowed
    Set to $true to allow external forwarding, $false to deny.
.PARAMETER InternalOnlyForwarding
    Set to $true to allow internal-only forwarding, $false to deny.
.NOTES
    This function is not intended to be called directly.
#>

function Set-O365MailboxForwardingSettings {
    [CmdletBinding()]
    param(
        [bool]$ExternalForwardingAllowed,
        [bool]$InternalOnlyForwarding
    )

    Write-Verbose "Setting ExternalForwardingAllowed to $ExternalForwardingAllowed (Placeholder)"
    Write-Verbose "Setting InternalOnlyForwarding to $InternalOnlyForwarding (Placeholder)"
    # Placeholder for actual cmdlet calls
}

<#
.SYNOPSIS
    Gets the current Teams governance settings.
.DESCRIPTION
    This internal helper function retrieves the current Teams governance settings, such as guest access to create/update channels.
.NOTES
    This function is not intended to be called directly.
#>

function Get-O365TeamsGovernanceSettings {
    [CmdletBinding()]
    param()

    # Placeholder for actual cmdlet calls
    # For now, we will return a dummy object.
    return [PSCustomObject]@{
        AllowGuestCreateUpdateChannels = $false # Placeholder
    }
}

<#
.SYNOPSIS
    Sets the Teams governance settings.
.DESCRIPTION
    This internal helper function sets the Teams governance settings, such as guest access to create/update channels.
.PARAMETER AllowGuestCreateUpdateChannels
    Set to $true to allow guest users to create/update channels, $false to deny.
.NOTES
    This function is not intended to be called directly.
#>

function Set-O365TeamsGovernanceSettings {
    [CmdletBinding()]
    param(
        [bool]$AllowGuestCreateUpdateChannels
    )

    Write-Verbose "Setting AllowGuestCreateUpdateChannels to $AllowGuestCreateUpdateChannels (Placeholder)"
    # Placeholder for actual cmdlet calls
}